mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Abri tenancy signatory becomes an associated HubSpot deal contact 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
78fcba45e5
commit
76d0b4bac9
3 changed files with 106 additions and 7 deletions
|
|
@ -1,6 +1,8 @@
|
|||
from typing import 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]
|
||||
|
||||
# HubSpot-defined deal -> contact association ("deal_to_contact" in v3 terms).
|
||||
DEAL_TO_CONTACT_ASSOCIATION_TYPE_ID = 3
|
||||
|
|
@ -19,7 +21,23 @@ class HubspotDealContactsClient:
|
|||
self._client: Client = sdk_client
|
||||
|
||||
def create_contact(self, properties: Dict[str, str]) -> str:
|
||||
raise NotImplementedError
|
||||
contact = 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:
|
||||
raise NotImplementedError
|
||||
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,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import xml.etree.ElementTree as ET
|
||||
from datetime import date
|
||||
from typing import List, Tuple, Union
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
import requests
|
||||
|
||||
|
|
@ -14,6 +14,8 @@ from domain.abri.models import (
|
|||
LogJobRequest,
|
||||
LogJobResult,
|
||||
PlaceRef,
|
||||
TenancyData,
|
||||
Tenant,
|
||||
)
|
||||
from infrastructure.abri.config import AbriConfig
|
||||
from infrastructure.abri.envelope import serialise_relay_request
|
||||
|
|
@ -28,6 +30,14 @@ def _format_appointment_date(appointment_date: date) -> str:
|
|||
return appointment_date.strftime("%d/%m/%Y")
|
||||
|
||||
|
||||
def _select_phone_number(entries: List[ET.Element]) -> Optional[str]:
|
||||
for entry in entries:
|
||||
number = (entry.text or "").strip()
|
||||
if number:
|
||||
return number
|
||||
return None
|
||||
|
||||
|
||||
class AbriClient:
|
||||
def __init__(self, config: AbriConfig) -> None:
|
||||
self._config = config
|
||||
|
|
@ -76,7 +86,15 @@ class AbriClient:
|
|||
return self._parse_appointment_amended(outcome)
|
||||
|
||||
def get_tenant_data(self, place_ref: PlaceRef) -> GetTenantDataResult:
|
||||
raise NotImplementedError
|
||||
outcome = self._exchange(
|
||||
request_type="getSCSTenantData",
|
||||
parameters=[("place_ref", place_ref)],
|
||||
)
|
||||
|
||||
if isinstance(outcome, AbriRequestRejected):
|
||||
return outcome
|
||||
|
||||
return self._parse_tenancy_data(outcome)
|
||||
|
||||
def _exchange(
|
||||
self, request_type: str, parameters: List[Tuple[str, str]]
|
||||
|
|
@ -164,6 +182,37 @@ class AbriClient:
|
|||
appointment_time=appointment_time,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_tenancy_data(root: ET.Element) -> TenancyData:
|
||||
tenancy = root.find("Tenancy")
|
||||
|
||||
if tenancy is None:
|
||||
raise AbriResponseParseError(
|
||||
"Tenancy element missing from relay response"
|
||||
)
|
||||
|
||||
tenancy_reference = (tenancy.text or "").strip()
|
||||
|
||||
return TenancyData(
|
||||
tenancy_reference=tenancy_reference,
|
||||
tenants=tuple(
|
||||
AbriClient._parse_tenant(tenant)
|
||||
for tenant in tenancy.findall("Tenant")
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_tenant(element: ET.Element) -> Tenant:
|
||||
# Data minimisation: person_title, main-contact and the per-person
|
||||
# reference (the element text) are deliberately not read.
|
||||
return Tenant(
|
||||
forenames=(element.get("forenames") or "").strip(),
|
||||
surname=(element.get("surname") or "").strip(),
|
||||
mobile=_select_phone_number(element.findall("Mobile")),
|
||||
telephone=_select_phone_number(element.findall("Telephone")),
|
||||
vulnerabilities=(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_rejection(root: ET.Element) -> AbriRequestRejected:
|
||||
success = root.findtext("success")
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Tuple, Union
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
from domain.abri.models import AbriRequestRejected, PlaceRef
|
||||
from domain.abri.models import AbriRequestRejected, PlaceRef, Tenant
|
||||
from etl.hubspot.deal_contacts_client import HubspotDealContactsClient
|
||||
from infrastructure.abri.abri_client import AbriClient
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
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."""
|
||||
|
|
@ -34,4 +47,23 @@ class TenantDataSyncOrchestrator:
|
|||
self._hubspot = hubspot
|
||||
|
||||
def run(self, place_ref: PlaceRef, deal_id: str) -> TenantDataSyncResult:
|
||||
raise NotImplementedError
|
||||
tenancy = self._abri.get_tenant_data(place_ref)
|
||||
|
||||
if isinstance(tenancy, AbriRequestRejected):
|
||||
return tenancy
|
||||
|
||||
contact_ids: List[str] = []
|
||||
for tenant in tenancy.tenants:
|
||||
contact_id = self._hubspot.create_contact(_contact_properties(tenant))
|
||||
self._hubspot.associate_contact_to_deal(
|
||||
deal_id=deal_id, contact_id=contact_id
|
||||
)
|
||||
contact_ids.append(contact_id)
|
||||
|
||||
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
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue