Map every unsuccessful outcome to a reason code (best-guess, client to confirm)

Complete the outcome-to-code table so all five UNSUCCESSFUL_OUTCOMES map:
cancelled -> REQT (new enum member) and no show -> NOACCESS join the
existing three. Only no answer -> CARDED is firm; the rest are best-guesses
for client confirmation, noted inline with alternatives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-08 14:28:47 +00:00
parent 13e3e78749
commit 9a669dfcea
3 changed files with 16 additions and 6 deletions

View file

@ -31,14 +31,21 @@ class UnmappableOutcomeError(Exception):
super().__init__(f"no abandonment reason code for outcome {outcome!r}")
# Only UNSUCCESSFUL_OUTCOMES can reach here (they trip the abandonment trigger),
# keyed lower-cased to match how the differ compares. "cancelled"/"no show" are
# deliberately unmapped for now - they never match the real "Cancelled / No Show"
# dropdown value, so they cannot occur, and would raise loudly if they ever did.
# Tenant Refusal -> CRA is the client's steer but is ambiguous (REFSURV/REFUSAL
# also fit) - confirmation pending, see the reason-code reference below.
# 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" -> REQT (Requested by Tenant; alt MYABRI92/OPTI)
# - "no show" -> NOACCESS (No access; alt CARDED/NOADULT)
# - "tenant refusal" -> CRA (Customer Refused Access; alt REFSURV/REFUSAL)
# - "not viable" -> NOACCESS (No access; alt JOBD/ERROR)
# NB "cancelled"/"no show" are our internal split of HubSpot's single
# "Cancelled / No Show" dropdown value - the client may prefer one code for both.
# See the reason-code reference below.
_REASON_BY_OUTCOME: dict[UnsuccessfulOutcome, AbandonReason] = {
"no answer": AbandonReason.CARDED,
"cancelled": AbandonReason.REQT,
"no show": AbandonReason.NOACCESS,
"tenant refusal": AbandonReason.CRA,
"not viable": AbandonReason.NOACCESS,
}

View file

@ -35,6 +35,7 @@ class AbandonReason(str, Enum):
CARDED = "CARDED"
CRA = "CRA"
NOACCESS = "NOACCESS"
REQT = "REQT"
@dataclass(frozen=True)

View file

@ -32,6 +32,8 @@ def test_neither_date_leaves_the_abandonment_undated() -> None:
("No Answer", AbandonReason.CARDED),
("NO ANSWER", AbandonReason.CARDED),
(" no answer ", AbandonReason.CARDED),
("Cancelled", AbandonReason.REQT),
("No Show", AbandonReason.NOACCESS),
("Tenant Refusal", AbandonReason.CRA),
(" tenant refusal ", AbandonReason.CRA),
("Not Viable", AbandonReason.NOACCESS),