From c01a68f7cf6e58115671c7fe81b8b1631143a40b Mon Sep 17 00:00:00 2001 From: Pyrex Clifford Date: Thu, 12 Feb 2026 21:03:24 +0100 Subject: [PATCH] Handle split name aliases in lead importer --- app/services/lead_importer.py | 22 +++++++++- tests/test_campaign.py | 82 ++++++++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/app/services/lead_importer.py b/app/services/lead_importer.py index 4f234e1..e1923fe 100644 --- a/app/services/lead_importer.py +++ b/app/services/lead_importer.py @@ -14,6 +14,8 @@ _FIELD_ALIASES = { "name": {"name", "full_name", "lead_name", "contact_name"}, + "first_name": {"first_name", "firstname", "given_name"}, + "last_name": {"last_name", "lastname", "family_name"}, "email": {"email", "email_address", "mail"}, "company": {"company", "company_name", "organization", "org"}, "position": {"position", "title", "job_title", "role"}, @@ -131,13 +133,29 @@ def _parse_xlsx(self, payload: bytes) -> list[dict[str, str]]: return records def _normalize_row(self, row: dict[str, str]) -> dict[str, str]: - lowered = {(k or "").strip().lower(): (v if isinstance(v, str) else str(v or "")) for k, v in row.items()} + lowered = { + (k or "").strip().lower(): (v if isinstance(v, str) else str(v or "")) + for k, v in row.items() + } + + def _clean(value: str) -> str: + return " ".join(value.split()) + normalized: dict[str, str] = {} for target, aliases in _FIELD_ALIASES.items(): value = "" for alias in aliases: if alias in lowered and lowered[alias].strip(): - value = lowered[alias].strip() + value = _clean(lowered[alias]) break normalized[target] = value + + if not normalized["name"]: + first_name = normalized["first_name"] + last_name = normalized["last_name"] + if first_name and last_name: + normalized["name"] = _clean(f"{first_name} {last_name}") + elif first_name: + normalized["name"] = first_name + return normalized diff --git a/tests/test_campaign.py b/tests/test_campaign.py index 30c3464..ce9fb90 100644 --- a/tests/test_campaign.py +++ b/tests/test_campaign.py @@ -34,7 +34,9 @@ def test_import_and_outreach_flow(): sent = service.send_outreach_batch(limit=10) assert sent == 2 - service.process_incoming_reply("alice@acme.com", "Yes, interested. can we schedule?") + service.process_incoming_reply( + "alice@acme.com", "Yes, interested. can we schedule?" + ) metrics = service.metrics() assert metrics.replied == 1 @@ -59,13 +61,79 @@ def test_xlsx_import_and_invalid_rows_are_tracked(): assert result.inserted == 1 assert result.invalid_rows == 0 - dup_result = importer.import_rows([ - {"name": "Dana", "email": "dana@org.com"}, - {"name": "", "email": "missing@org.com"}, - ]) + dup_result = importer.import_rows( + [ + {"name": "Dana", "email": "dana@org.com"}, + {"name": "", "email": "missing@org.com"}, + ] + ) assert dup_result.skipped_existing == 1 assert dup_result.invalid_rows == 1 + +def test_import_uses_explicit_full_name_alias_with_whitespace_cleanup(): + db = _db() + importer = LeadImporter(db) + + result = importer.import_rows( + [ + { + "contact_name": " Mary Jane Watson ", + "email_address": "mary@org.com", + } + ] + ) + + assert result.inserted == 1 + lead = db.query(Lead).filter(Lead.email == "mary@org.com").one() + assert lead.name == "Mary Jane Watson" + + +def test_import_builds_name_from_split_name_fields(): + db = _db() + importer = LeadImporter(db) + + result = importer.import_rows( + [ + { + "firstname": " John ", + "lastname": " Doe ", + "email": "john@org.com", + }, + { + "given_name": "Ana", + "email": "ana@org.com", + }, + ] + ) + + assert result.inserted == 2 + john = db.query(Lead).filter(Lead.email == "john@org.com").one() + ana = db.query(Lead).filter(Lead.email == "ana@org.com").one() + assert john.name == "John Doe" + assert ana.name == "Ana" + + +def test_import_prefers_explicit_name_over_split_name_fields(): + db = _db() + importer = LeadImporter(db) + + result = importer.import_rows( + [ + { + "full_name": "Primary Name", + "first_name": "Secondary", + "last_name": "Person", + "email": "primary@org.com", + } + ] + ) + + assert result.inserted == 1 + lead = db.query(Lead).filter(Lead.email == "primary@org.com").one() + assert lead.name == "Primary Name" + + def test_unsubscribe(): db = _db() importer = LeadImporter(db) @@ -84,7 +152,9 @@ def test_negative_reply_marks_lead_opted_out(): service.seed_templates("book calls", "SaaS") service.send_outreach_batch(limit=1) - service.process_incoming_reply("nora@org.com", "Thanks, but I am not interested. Please remove me.") + service.process_incoming_reply( + "nora@org.com", "Thanks, but I am not interested. Please remove me." + ) lead = db.query(Lead).filter(Lead.email == "nora@org.com").one() assert lead.opt_out is True