Improve campaign personalization and contact duplicate handling.
Add merge tags for email/SMS, paginated removable recipients, PCM event panels for postcard campaigns, and a portal modal when phone/address matches an existing contact.
This commit is contained in:
+84
-47
@@ -60,6 +60,9 @@ def find_matching_contact(
|
||||
email: str = "",
|
||||
phone: str = "",
|
||||
postal_address: dict | None = None,
|
||||
match_email: bool = True,
|
||||
match_phone: bool = True,
|
||||
match_address: bool = True,
|
||||
) -> tuple[Contact | None, str]:
|
||||
"""
|
||||
Resolve an existing contact.
|
||||
@@ -68,20 +71,21 @@ def find_matching_contact(
|
||||
Returns (contact, reason) where reason is email|phone|address|"".
|
||||
"""
|
||||
email_norm = (email or "").strip().lower()
|
||||
if email_norm:
|
||||
if match_email and email_norm:
|
||||
hit = Contact.objects.filter(email__iexact=email_norm).first()
|
||||
if hit:
|
||||
return hit, "email"
|
||||
|
||||
phone_digits = normalize_phone_digits(phone)
|
||||
if len(phone_digits) >= _PHONE_MIN_DIGITS:
|
||||
tail = phone_digits[-10:]
|
||||
for row in Contact.objects.exclude(phone="").only("id", "phone").iterator():
|
||||
other = normalize_phone_digits(row.phone)
|
||||
if len(other) >= _PHONE_MIN_DIGITS and other[-10:] == tail:
|
||||
return Contact.objects.get(pk=row.pk), "phone"
|
||||
if match_phone:
|
||||
phone_digits = normalize_phone_digits(phone)
|
||||
if len(phone_digits) >= _PHONE_MIN_DIGITS:
|
||||
tail = phone_digits[-10:]
|
||||
for row in Contact.objects.exclude(phone="").only("id", "phone").iterator():
|
||||
other = normalize_phone_digits(row.phone)
|
||||
if len(other) >= _PHONE_MIN_DIGITS and other[-10:] == tail:
|
||||
return Contact.objects.get(pk=row.pk), "phone"
|
||||
|
||||
if Contact.postal_address_has_content(postal_address):
|
||||
if match_address and Contact.postal_address_has_content(postal_address):
|
||||
line1 = (postal_address.get("line1") or "").strip()
|
||||
zip_code = (postal_address.get("zip") or "").strip()
|
||||
candidates = Contact.objects.exclude(postal_address={})
|
||||
@@ -110,45 +114,18 @@ def _merge_notes(existing: str, addition: str) -> str:
|
||||
return f"{existing}\n{addition}".strip()
|
||||
|
||||
|
||||
def upsert_contact(
|
||||
def _apply_contact_fields(
|
||||
existing: Contact,
|
||||
*,
|
||||
email: str,
|
||||
first_name: str = "",
|
||||
last_name: str = "",
|
||||
phone: str = "",
|
||||
postal_address: dict | None = None,
|
||||
source: str = Contact.Source.CONTACT_FORM,
|
||||
notes_append: str = "",
|
||||
) -> tuple[Contact, bool, str]:
|
||||
"""
|
||||
Find or create a contact, merging on email / phone / address.
|
||||
|
||||
Returns (contact, created, match_reason).
|
||||
When merged onto a different email, the submitted email is noted in ``notes``.
|
||||
"""
|
||||
email_norm = (email or "").strip().lower()
|
||||
phone = (phone or "").strip()
|
||||
postal = postal_address if isinstance(postal_address, dict) else {}
|
||||
has_postal = Contact.postal_address_has_content(postal)
|
||||
|
||||
existing, reason = find_matching_contact(
|
||||
email=email_norm,
|
||||
phone=phone,
|
||||
postal_address=postal if has_postal else None,
|
||||
)
|
||||
|
||||
if existing is None:
|
||||
contact = Contact.objects.create(
|
||||
email=email_norm or None,
|
||||
first_name=(first_name or "").strip(),
|
||||
last_name=(last_name or "").strip(),
|
||||
phone=phone,
|
||||
postal_address=postal if has_postal else {},
|
||||
source=source,
|
||||
notes=(notes_append or "").strip(),
|
||||
)
|
||||
return contact, True, ""
|
||||
|
||||
email_norm: str,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
phone: str,
|
||||
postal: dict,
|
||||
has_postal: bool,
|
||||
source: str,
|
||||
notes_append: str,
|
||||
) -> None:
|
||||
changed: list[str] = []
|
||||
if first_name and first_name.strip():
|
||||
existing.first_name = first_name.strip()
|
||||
@@ -199,4 +176,64 @@ def upsert_contact(
|
||||
fields = sorted(set(changed) | {"updated_at"})
|
||||
existing.save(update_fields=fields)
|
||||
|
||||
|
||||
def upsert_contact(
|
||||
*,
|
||||
email: str,
|
||||
first_name: str = "",
|
||||
last_name: str = "",
|
||||
phone: str = "",
|
||||
postal_address: dict | None = None,
|
||||
source: str = Contact.Source.CONTACT_FORM,
|
||||
notes_append: str = "",
|
||||
merge_phone_address: bool = True,
|
||||
merge_into: Contact | None = None,
|
||||
) -> tuple[Contact, bool, str]:
|
||||
"""
|
||||
Find or create a contact, merging on email / phone / address.
|
||||
|
||||
Returns (contact, created, match_reason).
|
||||
|
||||
``merge_phone_address=False`` only merges on exact email.
|
||||
``merge_into`` forces merge into that contact row.
|
||||
"""
|
||||
email_norm = (email or "").strip().lower()
|
||||
phone = (phone or "").strip()
|
||||
postal = postal_address if isinstance(postal_address, dict) else {}
|
||||
has_postal = Contact.postal_address_has_content(postal)
|
||||
|
||||
if merge_into is not None:
|
||||
existing, reason = merge_into, "manual"
|
||||
else:
|
||||
existing, reason = find_matching_contact(
|
||||
email=email_norm,
|
||||
phone=phone,
|
||||
postal_address=postal if has_postal else None,
|
||||
match_phone=merge_phone_address,
|
||||
match_address=merge_phone_address,
|
||||
)
|
||||
|
||||
if existing is None:
|
||||
contact = Contact.objects.create(
|
||||
email=email_norm or None,
|
||||
first_name=(first_name or "").strip(),
|
||||
last_name=(last_name or "").strip(),
|
||||
phone=phone,
|
||||
postal_address=postal if has_postal else {},
|
||||
source=source,
|
||||
notes=(notes_append or "").strip(),
|
||||
)
|
||||
return contact, True, ""
|
||||
|
||||
_apply_contact_fields(
|
||||
existing,
|
||||
email_norm=email_norm,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
phone=phone,
|
||||
postal=postal,
|
||||
has_postal=has_postal,
|
||||
source=source,
|
||||
notes_append=notes_append,
|
||||
)
|
||||
return existing, False, reason
|
||||
|
||||
Reference in New Issue
Block a user