diff --git a/schemas/contact.py b/schemas/contact.py index 753982048..a9302daaf 100644 --- a/schemas/contact.py +++ b/schemas/contact.py @@ -150,6 +150,7 @@ class CreateContact(BaseCreateModel, ValidateContact): organization: str | None = None role: Role contact_type: ContactType = "Primary" + nma_pk_owners: str | None = None # description: str | None = None # email: str | None = None # phone: str | None = None diff --git a/tests/transfers/test_contact_with_multiple_wells.py b/tests/transfers/test_contact_with_multiple_wells.py index 835aafb3f..40b4b26ea 100644 --- a/tests/transfers/test_contact_with_multiple_wells.py +++ b/tests/transfers/test_contact_with_multiple_wells.py @@ -14,15 +14,18 @@ # limitations under the License. # =============================================================================== -from db import ThingContactAssociation, Thing, Notes +from types import SimpleNamespace +from uuid import uuid4 + +from db import ThingContactAssociation, Thing, Notes, Contact from db.engine import session_ctx -from transfers.contact_transfer import ContactTransfer +from transfers.contact_transfer import ContactTransfer, _add_first_contact from transfers.well_transfer import WellTransferer def _run_contact_transfer(pointids: list[str]): wt = WellTransferer(pointids=pointids) - wt.transfer() + wt.transfer_parallel() ct = ContactTransfer(pointids=pointids) ct.transfer() @@ -87,4 +90,131 @@ def test_owner_comment_absent_skips_notes(): assert note_count == 0 +def test_ownerkey_fallback_name_when_name_and_org_missing(water_well_thing): + with session_ctx() as sess: + thing = sess.get(Thing, water_well_thing.id) + row = SimpleNamespace( + FirstName=None, + LastName=None, + OwnerKey="Fallback OwnerKey Name", + Email=None, + CtctPhone=None, + Phone=None, + CellPhone=None, + StreetAddress=None, + Address2=None, + City=None, + State=None, + Zip=None, + MailingAddress=None, + MailCity=None, + MailState=None, + MailZipCode=None, + PhysicalAddress=None, + PhysicalCity=None, + PhysicalState=None, + PhysicalZipCode=None, + ) + + # Should not raise "Either name or organization must be provided." + contact = _add_first_contact( + sess, row=row, thing=thing, organization=None, added=[] + ) + sess.flush() + + assert contact is not None + assert contact.name == "Fallback OwnerKey Name" + assert contact.organization is None + + +def test_ownerkey_dedupes_when_fallback_name_differs(water_well_thing): + owner_key = f"OwnerKey-{uuid4()}" + with session_ctx() as sess: + first_thing = sess.get(Thing, water_well_thing.id) + second_thing = Thing( + name=f"Second Well {uuid4()}", + thing_type="water well", + release_status="draft", + ) + sess.add(second_thing) + sess.flush() + + complete_row = SimpleNamespace( + FirstName="Casey", + LastName="Owner", + OwnerKey=owner_key, + Email=None, + CtctPhone=None, + Phone=None, + CellPhone=None, + StreetAddress=None, + Address2=None, + City=None, + State=None, + Zip=None, + MailingAddress=None, + MailCity=None, + MailState=None, + MailZipCode=None, + PhysicalAddress=None, + PhysicalCity=None, + PhysicalState=None, + PhysicalZipCode=None, + ) + fallback_row = SimpleNamespace( + FirstName=None, + LastName=None, + OwnerKey=owner_key, + Email=None, + CtctPhone=None, + Phone=None, + CellPhone=None, + StreetAddress=None, + Address2=None, + City=None, + State=None, + Zip=None, + MailingAddress=None, + MailCity=None, + MailState=None, + MailZipCode=None, + PhysicalAddress=None, + PhysicalCity=None, + PhysicalState=None, + PhysicalZipCode=None, + ) + + added = [] + first_contact = _add_first_contact( + sess, row=complete_row, thing=first_thing, organization=None, added=added + ) + assert first_contact is not None + assert first_contact.name == "Casey Owner" + + second_contact = _add_first_contact( + sess, row=fallback_row, thing=second_thing, organization=None, added=added + ) + sess.flush() + + # Reused existing contact; no duplicate fallback-name contact created. + assert second_contact is None + contacts = ( + sess.query(Contact) + .filter( + Contact.nma_pk_owners == owner_key, + Contact.contact_type == "Primary", + ) + .all() + ) + assert len(contacts) == 1 + assert contacts[0].name == "Casey Owner" + + assoc_count = ( + sess.query(ThingContactAssociation) + .filter(ThingContactAssociation.contact_id == contacts[0].id) + .count() + ) + assert assoc_count == 2 + + # ============= EOF ============================================= diff --git a/transfers/contact_transfer.py b/transfers/contact_transfer.py index 0acedb57f..dc649fc06 100644 --- a/transfers/contact_transfer.py +++ b/transfers/contact_transfer.py @@ -232,7 +232,7 @@ def _add_first_contact( role = "Owner" release_status = "private" - name = _make_name(row.FirstName, row.LastName) + name = _safe_make_name(row.FirstName, row.LastName, row.OwnerKey, organization) contact_data = { "thing_id": thing.id, @@ -326,6 +326,19 @@ def _add_first_contact( return contact +def _safe_make_name( + first: str | None, last: str | None, ownerkey: str, organization: str | None +) -> str | None: + name = _make_name(first, last) + if name is None and organization is None: + logger.warning( + f"Missing both first and last name and organization for OwnerKey {ownerkey}; " + f"using OwnerKey as fallback name." + ) + return ownerkey + return name + + def _add_second_contact( session: Session, row: pd.Series, thing: Thing, organization: str, added: list ) -> None: @@ -463,14 +476,31 @@ def _make_contact_and_assoc( session: Session, data: dict, thing: Thing, added: list ) -> tuple[Contact, bool]: new_contact = True - if (data["name"], data["organization"]) in added: + contact = None + + # Prefer OwnerKey-based dedupe so fallback names don't split the same owner + # into multiple contacts when some rows have real names and others do not. + owner_key = data.get("nma_pk_owners") + contact_type = data.get("contact_type") + if owner_key and contact_type: + contact = ( + session.query(Contact) + .filter_by(nma_pk_owners=owner_key, contact_type=contact_type) + .first() + ) + if contact is not None: + new_contact = False + + if contact is None and (data["name"], data["organization"]) in added: contact = ( session.query(Contact) .filter_by(name=data["name"], organization=data["organization"]) .first() ) - new_contact = False - else: + if contact is not None: + new_contact = False + + if contact is None: from schemas.contact import CreateContact