mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
136 lines
4.8 KiB
Python
136 lines
4.8 KiB
Python
from dataclasses import dataclass
|
|
from typing import Dict, List, Optional, Tuple, Union
|
|
|
|
from domain.abri.models import AbriRequestRejected, PlaceRef, Tenant
|
|
from infrastructure.hubspot.deal_contacts_client import HubspotDealContactsClient
|
|
from infrastructure.hubspot.errors import HubspotRequestError
|
|
from infrastructure.abri.abri_client import AbriClient
|
|
|
|
|
|
def _vulnerability_description(tenant: Tenant) -> Optional[str]:
|
|
"""The tenant's distinct vulnerabilities, verbatim and newline-joined.
|
|
|
|
Case-insensitively distinct within the tenant, keeping the first-seen
|
|
casing and document order.
|
|
"""
|
|
distinct: List[str] = []
|
|
seen: set[str] = set()
|
|
for vulnerability in tenant.vulnerabilities:
|
|
if vulnerability.lower() in seen:
|
|
continue
|
|
seen.add(vulnerability.lower())
|
|
distinct.append(vulnerability)
|
|
return "\n".join(distinct) if distinct else None
|
|
|
|
|
|
def _contact_properties(tenant: Tenant) -> Dict[str, str]:
|
|
candidates: Dict[str, Optional[str]] = {
|
|
"firstname": tenant.forenames,
|
|
"lastname": tenant.surname,
|
|
"mobilephone": tenant.mobile,
|
|
"phone": tenant.telephone,
|
|
"secondary_phone_number": tenant.secondary_number,
|
|
"vulnerability_description": _vulnerability_description(tenant),
|
|
}
|
|
properties = {name: value for name, value in candidates.items() if value}
|
|
# Always written, so "not vulnerable" is distinguishable from "not synced".
|
|
properties["is_vulnerable"] = "true" if tenant.vulnerabilities else "false"
|
|
return properties
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TenantDataSyncSummary:
|
|
"""PII-free record of what a sync run did: safe to log and persist."""
|
|
|
|
tenancy_reference: str
|
|
contact_ids: Tuple[str, ...]
|
|
vulnerable_contact_count: int
|
|
|
|
|
|
TenantDataSyncResult = Union[TenantDataSyncSummary, AbriRequestRejected]
|
|
|
|
|
|
class TenantDataSyncError(Exception):
|
|
"""A sync that stopped at the first failed HubSpot write.
|
|
|
|
Carries a PII-free progress report so the operator knows the deal's
|
|
half-state before cleaning up and re-running; contacts created but not
|
|
associated are orphaned in HubSpot and need manual cleanup.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
deal_id: str,
|
|
tenant_index: int,
|
|
contact_ids_created: Tuple[str, ...],
|
|
contact_ids_associated: Tuple[str, ...],
|
|
) -> None:
|
|
self.deal_id = deal_id
|
|
self.tenant_index = tenant_index
|
|
self.contact_ids_created = contact_ids_created
|
|
self.contact_ids_associated = contact_ids_associated
|
|
super().__init__(
|
|
f"{message} (deal: {deal_id}, tenant index: {tenant_index}, "
|
|
f"contacts created: {list(contact_ids_created)}, "
|
|
f"associated: {list(contact_ids_associated)})"
|
|
)
|
|
|
|
@property
|
|
def orphaned_contact_ids(self) -> Tuple[str, ...]:
|
|
return tuple(
|
|
contact_id
|
|
for contact_id in self.contact_ids_created
|
|
if contact_id not in self.contact_ids_associated
|
|
)
|
|
|
|
|
|
class AbriTenantDataSyncOrchestrator:
|
|
"""Syncs an Abri tenancy's signatories to a HubSpot deal.
|
|
|
|
Tenant data flows straight from Abri to HubSpot; nothing is persisted
|
|
in Domna's database or logs.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
abri_client: AbriClient,
|
|
hubspot: HubspotDealContactsClient,
|
|
) -> None:
|
|
self._abri = abri_client
|
|
self._hubspot = hubspot
|
|
|
|
def run(self, place_ref: PlaceRef, deal_id: str) -> TenantDataSyncResult:
|
|
tenancy = self._abri.get_tenant_data(place_ref)
|
|
|
|
if isinstance(tenancy, AbriRequestRejected):
|
|
return tenancy
|
|
|
|
contact_ids: List[str] = []
|
|
associated_ids: List[str] = []
|
|
for tenant_index, tenant in enumerate(tenancy.tenants):
|
|
try:
|
|
contact_id = self._hubspot.create_contact(_contact_properties(tenant))
|
|
contact_ids.append(contact_id)
|
|
self._hubspot.associate_contact_to_deal(
|
|
deal_id=deal_id, contact_id=contact_id
|
|
)
|
|
associated_ids.append(contact_id)
|
|
except HubspotRequestError as error:
|
|
# Fail fast at the first failed write; `error` is already
|
|
# scrubbed, so chaining it keeps tracebacks PII-free.
|
|
raise TenantDataSyncError(
|
|
message=str(error),
|
|
deal_id=deal_id,
|
|
tenant_index=tenant_index,
|
|
contact_ids_created=tuple(contact_ids),
|
|
contact_ids_associated=tuple(associated_ids),
|
|
) from error
|
|
|
|
return TenantDataSyncSummary(
|
|
tenancy_reference=tenancy.tenancy_reference,
|
|
contact_ids=tuple(contact_ids),
|
|
vulnerable_contact_count=sum(
|
|
1 for tenant in tenancy.tenants if tenant.vulnerabilities
|
|
),
|
|
)
|