app/services/recipient_service.py:37: def summary(self) -> dict: app/services/recipient_service.py-38- return { app/services/recipient_service.py-39- "total_imported": self.rows_seen, app/services/recipient_service.py-40- "valid_recipients": len(self.valid), app/services/recipient_service.py-41- "invalid_recipients": len(self.invalid), app/services/recipient_service.py-42- "duplicate_recipients": len(self.duplicates), app/services/recipient_service.py-43- "suppressed_recipients": len(self.suppressed), app/services/recipient_service.py-44- "bounced_recipients": len(self.bounced), app/services/recipient_service.py-45- "errors": self.errors, app/services/recipient_service.py-46- } app/services/recipient_service.py-47- app/services/recipient_service.py-48- app/services/recipient_service.py-49-def is_valid_email(addr: str) -> bool: @router.post("/import-preview") async def import_preview(file: UploadFile = File(...), user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db)): """Parse + validate an upload WITHOUT saving. Shows the full validation report.""" content = await file.read() if len(content) > settings.MAX_UPLOAD_MB * 1024 * 1024: raise HTTPException(413, f"File too large (max {settings.MAX_UPLOAD_MB} MB)") fname = (file.filename or "").lower() if fname.endswith(".json"): raise HTTPException(400, "JSON recipient files are not accepted. Use CSV or TXT.") if not (fname.endswith(".csv") or fname.endswith(".txt")): raise HTTPException(400, "Only .csv and .txt files are accepted for recipients") if fname.endswith(".txt"): lines = recipient_service.parse_txt(content.decode("utf-8", errors="replace")) records = [] for ln in lines: parts = [p.strip() for p in ln.split(",")] rec = {"email": parts[0]} if len(parts) > 1: rec["first_name"] = parts[1] if len(parts) > 2: rec["last_name"] = parts[2] records.append(rec) headers = ["email", "first_name", "last_name"] parse_errors = [] else: records, headers, parse_errors = recipient_service.parse_csv( content.decode("utf-8", errors="replace")) sup, bounced = await _suppression_and_bounces(db) existing = set((await db.execute(select(Recipient.email))).scalars().all()) rpt = recipient_service.validate_rows(records, sup, bounced, existing, settings.MAX_RECIPIENTS_PER_CAMPAIGN) return {"filename": file.filename, "headers": headers, "summary": rpt.summary(), "valid_sample": rpt.valid[:25], "invalid_sample": rpt.invalid[:25], "duplicates_sample": rpt.duplicates[:10], "suppressed_sample": rpt.suppressed[:10], "parse_errors": parse_errors[:50], "parsed_records": len(records)}