Model/infrastructure/hubspot/deal_contacts_client.py
Daniel Roth ed08a6adfe HubSpot clients share one scrubbed-retry call policy 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:02:22 +00:00

53 lines
2.1 KiB
Python

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)