from typing import Any, Callable, Dict from hubspot.client import Client # type: ignore[reportMissingTypeStubs] from hubspot.crm.associations.v4 import AssociationSpec # type: ignore[reportMissingTypeStubs] from hubspot.crm.contacts import SimplePublicObjectInputForCreate # type: ignore[reportMissingTypeStubs] from infrastructure.hubspot.scrubbed_calls import scrubbed_call_with_retry # HubSpot-defined deal -> contact association ("deal_to_contact" in v3 terms). DEAL_TO_CONTACT_ASSOCIATION_TYPE_ID = 3 class HubspotDealContactsClient: """Focused HubSpot client for the tenant-data sync flow. Deliberately separate from the legacy HubspotClient: the SDK client is constructor-injected, and SDK errors are re-raised scrubbed of response bodies (HubSpot validation errors echo submitted property values, which here are tenant PII). """ def __init__(self, sdk_client: Client) -> None: self._client: Client = sdk_client def create_contact(self, properties: Dict[str, str]) -> str: contact = self._call( lambda: self._client.crm.contacts.basic_api.create( # type: ignore[reportUnknownMemberType] simple_public_object_input_for_create=SimplePublicObjectInputForCreate( properties=properties ) ) ) return str(contact.id) # type: ignore[reportUnknownMemberType] def associate_contact_to_deal(self, deal_id: str, contact_id: str) -> None: self._call( lambda: self._client.crm.associations.v4.basic_api.create( # type: ignore[reportUnknownMemberType] object_type="deals", object_id=deal_id, to_object_type="contacts", to_object_id=contact_id, association_spec=[ AssociationSpec( association_category="HUBSPOT_DEFINED", association_type_id=DEAL_TO_CONTACT_ASSOCIATION_TYPE_ID, ) ], ) ) @staticmethod def _call(fn: Callable[[], Any]) -> Any: return scrubbed_call_with_retry(fn)