Model/domain/abri/models.py
Daniel Roth edd4ec00de Make the OpenHousing resource mandatory, dropping the default fallback
The deal's third_party_surveyor_identifier is now required by the log
and amend flows: the trigger contract rejects messages without it
(blank treated as missing, since a blank resource makes OpenHousing
silently drop the appointment), and LogJobRequest/AmendJobRequest carry
it as a required field. The ABRI_RELAY_DEFAULT_RESOURCE fallback and
its config/terraform/workflow wiring are removed as dead code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:13:28 +00:00

132 lines
3.3 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
# The bookable OpenHousing resource. Mandatory: a missing or blank
# resource does not error at the relay — OpenHousing logs the job but
# silently drops the appointment.
resource: str
@dataclass(frozen=True)
class JobLogged:
job_no: str
# Informational only (never read downstream); absent on some successes.
logged_info: Optional[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
# The bookable OpenHousing resource. Mandatory: a missing or blank
# resource does not error at the relay — OpenHousing silently drops
# the appointment.
resource: str
@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]