Import contacts with per-row error isolation
Python · Python · intermediate · greenfield
Adds the contact importer used by the CSV upload flow: validates and normalizes each row independently, collecting per-row errors instead of aborting the batch. Ran it against an export with missing names, blank emails and mixed-case addresses — good rows import, bad rows land in the error report with their 1-based row numbers.
Requirements
- Implement `import_contacts(rows)` for the CRM importer: `rows` is a list of dicts parsed from the upload, each may have `name` and `email` keys (either can be missing, `None`, empty, or padded with whitespace). When present, `name` and `email` values are either strings or `None` — no other value types are valid inputs.
- Validation rules — these are the complete rules for this importer: `name` is required (non-empty after trimming); `email` must contain an `@` after trimming. Full RFC email validation is explicitly out of scope.
- Normalization: trim both fields; lowercase the email.
- A bad row must never stop the import: skip it and record `(row_number, message)` where row numbers are 1-based over the input rows. Record exactly one error per invalid row, validating `name` before `email` — when both are invalid, only `(row_number, "name is required")` is recorded.
- Return `(imported, errors)` — the list of normalized contact dicts and the list of error tuples, both in input order.
Files touched
- app/crm/importer.py
--- app/crm/importer.py
+def import_contacts(rows):
+ """Import contact rows, isolating per-row failures.
+
+ Returns ``(imported, errors)``: normalized contacts plus a list of
+ ``(row_number, message)`` tuples, row numbers 1-based over the input.
+ """
+ imported = []
+ errors = []
+ for number, row in enumerate(rows, start=1):
+ name = (row.get("name") or "").strip()
+ email = (row.get("email") or "").strip().lower()
+ if not name:
+ errors.append((number, "name is required"))
+ continue
+ if "@" not in email:
+ errors.append((number, "email is invalid"))
+ continue
+ imported.append({"name": name, "email": email})
+ return imported, errors
+