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]