From 9a669dfcea9459ba0bee7388c1fc4fcc70e4abac Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 8 Jul 2026 14:28:47 +0000 Subject: [PATCH] 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 --- domain/abri/abandonment.py | 19 +++++++++++++------ domain/abri/models.py | 1 + tests/domain/abri/test_abandonment.py | 2 ++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/domain/abri/abandonment.py b/domain/abri/abandonment.py index c112949f7..ab2bce2cc 100644 --- a/domain/abri/abandonment.py +++ b/domain/abri/abandonment.py @@ -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, } diff --git a/domain/abri/models.py b/domain/abri/models.py index 0c32a78dc..9eef5887b 100644 --- a/domain/abri/models.py +++ b/domain/abri/models.py @@ -35,6 +35,7 @@ class AbandonReason(str, Enum): CARDED = "CARDED" CRA = "CRA" NOACCESS = "NOACCESS" + REQT = "REQT" @dataclass(frozen=True) diff --git a/tests/domain/abri/test_abandonment.py b/tests/domain/abri/test_abandonment.py index 9ccd1cf03..eb60e736f 100644 --- a/tests/domain/abri/test_abandonment.py +++ b/tests/domain/abri/test_abandonment.py @@ -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),