Model/domain/abri/models.py
Daniel Roth 4ac4dd8683 Collapse cancelled/no show to HubSpot's single 'Cancelled / No Show' value
'Cancelled' and 'no show' were separate strings in the outcome vocabulary,
but HubSpot only ever emits the one dropdown value 'Cancelled / No Show' -
so neither phantom string could ever match. Replace both with the real
value (mapped to NOACCESS, best-guess pending client), which also fixes the
long-standing differ mismatch: the abandonment trigger now actually fires on
Cancelled / No Show. Drops the now-unused REQT enum member.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:33:59 +00:00

124 lines
2.9 KiB
Python

from dataclasses import dataclass
from datetime import date
from enum import Enum
from typing import Literal, NewType, Optional, Tuple, Union
PlaceRef = NewType("PlaceRef", str)
SlotCode = Literal["AM", "PM", "AD"]
# HubSpot deal outcomes (lower-cased) that mean a survey was unsuccessful and,
# once the attempt threshold is reached, abandon the job. The single source of
# truth shared by the abandonment trigger (etl HubspotDealDiffer) and the
# reason-code mapping (domain.abri.abandonment).
UnsuccessfulOutcome = Literal[
"no answer", "cancelled / no show", "tenant refusal", "not viable"
]
UNSUCCESSFUL_OUTCOMES: tuple[UnsuccessfulOutcome, ...] = (
"no answer",
"cancelled / no show",
"tenant refusal",
"not viable",
)
class AbandonReason(str, Enum):
"""OpenHousing's coded reason a job was abandoned.
Values are Abri's own reason codes. Only the members reachable from a
HubSpot abandonment outcome are defined; the outcome-to-code mapping lives
in domain.abri.abandonment.
"""
CARDED = "CARDED"
CRA = "CRA"
NOACCESS = "NOACCESS"
@dataclass(frozen=True)
class LogJobRequest:
client_ref: str
place_ref: PlaceRef
appointment_date: date
appointment_time: SlotCode
short_description: str
long_description: str
@dataclass(frozen=True)
class JobLogged:
job_no: str
logged_info: str
@dataclass(frozen=True)
class AbriRequestRejected:
code: str
message: str
LogJobResult = Union[JobLogged, AbriRequestRejected]
@dataclass(frozen=True)
class AmendJobRequest:
job_no: str
appointment_date: date
appointment_time: SlotCode
resource: Optional[str] = None
@dataclass(frozen=True)
class AppointmentAmended:
job_no: str
appointment_date: str
appointment_time: str
AmendJobResult = Union[AppointmentAmended, AbriRequestRejected]
@dataclass(frozen=True)
class Tenant:
"""A tenancy signatory, minimised to the fields the HubSpot sync needs.
Person title, per-person references, the main-contact flag and tenancy
dates are deliberately dropped at parse time (data minimisation); mobile
and telephone are the single best number of each kind, pre-selected by
priority, and secondary_number is the best of the numbers they left
unused.
"""
forenames: str
surname: str
mobile: Optional[str]
telephone: Optional[str]
secondary_number: Optional[str]
vulnerabilities: Tuple[str, ...]
@dataclass(frozen=True)
class TenancyData:
"""Tenancy signatories for a place; the reference is PII-safe."""
tenancy_reference: str
tenants: Tuple[Tenant, ...]
GetTenantDataResult = Union[TenancyData, AbriRequestRejected]
@dataclass(frozen=True)
class AbandonJobRequest:
job_no: str
reason: AbandonReason
date_abandoned: date
@dataclass(frozen=True)
class JobAbandoned:
job_no: str
AbandonJobResult = Union[JobAbandoned, AbriRequestRejected]