Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions app/services/lead_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -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
82 changes: 76 additions & 6 deletions tests/test_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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
Expand Down