mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
'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>
104 lines
4 KiB
Python
104 lines
4 KiB
Python
from datetime import date
|
|
from typing import Optional
|
|
|
|
from domain.abri.models import AbandonReason, UnsuccessfulOutcome
|
|
|
|
|
|
def abandonment_date(
|
|
confirmed_survey_date: Optional[date],
|
|
last_submission_date: Optional[date],
|
|
) -> Optional[date]:
|
|
"""The real-world date a deal's job is abandoned on.
|
|
|
|
The confirmed survey date of the failed third attempt, since that is when
|
|
the job actually lapsed; if no survey date was ever confirmed, the date the
|
|
third attempt was last submitted stands in. Returns None when the deal
|
|
carries neither date, leaving the caller to decide how to surface the gap.
|
|
"""
|
|
return confirmed_survey_date or last_submission_date
|
|
|
|
|
|
class UnmappableOutcomeError(Exception):
|
|
"""A deal outcome with no abandonment reason code to send to Abri.
|
|
|
|
abandon_reason is mandatory on a canceljob, so an empty outcome or one
|
|
outside the mapped set (which the abandonment trigger should never let
|
|
through) fails loudly rather than inventing a code.
|
|
"""
|
|
|
|
def __init__(self, outcome: Optional[str]) -> None:
|
|
self.outcome = outcome
|
|
super().__init__(f"no abandonment reason code for outcome {outcome!r}")
|
|
|
|
|
|
# Every UNSUCCESSFUL_OUTCOMES entry maps to a code (they trip the abandonment
|
|
# trigger and abandon_reason is mandatory), keyed lower-cased to match how the
|
|
# differ compares. Only "no answer" -> CARDED is firm; the rest are best-guesses
|
|
# pending client confirmation:
|
|
# - "cancelled / no show" -> NOACCESS (No access; alt REQT/MYABRI92 if read as
|
|
# a tenant cancellation rather than a no-show)
|
|
# - "tenant refusal" -> CRA (Customer Refused Access; alt REFSURV/REFUSAL)
|
|
# - "not viable" -> NOACCESS (No access; alt JOBD/ERROR)
|
|
# See the reason-code reference below.
|
|
_REASON_BY_OUTCOME: dict[UnsuccessfulOutcome, AbandonReason] = {
|
|
"no answer": AbandonReason.CARDED,
|
|
"cancelled / no show": AbandonReason.NOACCESS,
|
|
"tenant refusal": AbandonReason.CRA,
|
|
"not viable": AbandonReason.NOACCESS,
|
|
}
|
|
|
|
|
|
def abandon_reason_for_outcome(outcome: Optional[str]) -> AbandonReason:
|
|
"""The OpenHousing abandonment reason code for a HubSpot deal outcome.
|
|
|
|
The single mapping point from HubSpot's outcome to a coded reason, matched
|
|
case- and whitespace-insensitively. Raises UnmappableOutcomeError for an
|
|
empty or unrecognised outcome, since abandon_reason is mandatory.
|
|
"""
|
|
normalized = (outcome or "").strip().lower()
|
|
for mapped_outcome, reason in _REASON_BY_OUTCOME.items():
|
|
if mapped_outcome == normalized:
|
|
return reason
|
|
raise UnmappableOutcomeError(outcome)
|
|
|
|
|
|
# Reason Code - Description
|
|
# 1STCARD - 1st Card - Gas Service only
|
|
# 2NDCARD - 2nd Card - Gas Service only
|
|
# 2NDMAN - 2nd man required
|
|
# ADDIT - Additional Work Required
|
|
# APTB - Appointment Bought Forward [Note: I recognise this should read "brought" - they provided the list in a low res image and I'm pretty sure it says Bought not Brought]
|
|
# ARCH - Job Archived
|
|
# ASB - Anti Social Behaviour
|
|
# ASSIGNED - Assigned to Different Operative
|
|
# BULKABAN - Bulk Abandonment Request to IT
|
|
# CARDED - Carded - Tenant not in
|
|
# CONTRACT - Order raised using incorrect contract
|
|
# CORON - Coronavirus
|
|
# COVI - COVID-19
|
|
# CPAL - CP Arrived Late
|
|
# CRA - Customer Refused Access
|
|
# DUPLICAT [not a typo] - Duplicate
|
|
# ERROR - Issued in error
|
|
# ESCA - Escalated to CAT1
|
|
# HOLIDAY - Holiday
|
|
# HS - Health & Safety
|
|
# JOBD - Job Out of Date
|
|
# M - Minor
|
|
# MANAGE - Management decision
|
|
# MATERIAL - Materials not available
|
|
# MATREQ - Materials required
|
|
# MYABRI92 - Tenant Cancelled On My Abri
|
|
# NO ROOM - Room does not exist - invalid data
|
|
# NOACCESS - No access
|
|
# NOADULT - No Adult Present
|
|
# OPTI - Cancelled in Optimise
|
|
# OUTLOOK - Changed in Outlook
|
|
# PASSED - Passed to other department
|
|
# PLAN - Part of Planned Programme
|
|
# RDCANX - Radian Direct Cancelled
|
|
# RECALLMO - Recall Mobile Visit
|
|
# REFSURV - Refused to Surveyor
|
|
# REFUSAL - Agreed Resident Refusal
|
|
# REQT - Requested by Tenant
|
|
# RSCANX - Radian Services Canceled
|