Fail non-retriably when amending or abandoning a deal that never logged to Abri 🟥

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-24 15:53:50 +00:00
parent cf0129103e
commit fb64f9687e
2 changed files with 84 additions and 2 deletions

View file

@ -44,6 +44,24 @@ class AbriFlows(Protocol):
) -> AbandonJobOrchestrationResult: ...
class DealDivergedFromAbriError(NonRetriableTaskError):
"""An amend/abandon on a deal carrying no client_booking_reference.
An absent Job Number is ambiguous the job was never logged, or the
reference OpenHousing returned was lost so the change or cancellation
cannot be synced to Abri. Non-retriable: redelivery cannot conjure a
reference; a human must reconcile the deal against OpenHousing.
"""
def __init__(self, flow: str, deal_id: str) -> None:
self.flow = flow
self.deal_id = deal_id
super().__init__(
f"{flow} cannot run for deal {deal_id}: no client_booking_reference "
"(Job Number) — the deal has diverged from Abri; check OpenHousing"
)
class AbriFlowRejectedError(Exception):
"""An OpenHousing rejection, surfaced as a failure so the task fails.

View file

@ -4,7 +4,11 @@ from typing import Any, Dict, List, Optional
import pytest
from applications.abri.abri_trigger_request import AbriTriggerRequest
from applications.abri.dispatch import AbriFlowRejectedError, dispatch_abri_flows
from applications.abri.dispatch import (
AbriFlowRejectedError,
DealDivergedFromAbriError,
dispatch_abri_flows,
)
from domain.abri.models import (
AbriRequestRejected,
AppointmentAmended,
@ -29,7 +33,9 @@ from utilities.aws_lambda.task_handler import NonRetriableTaskError
DEAL_ID = "9876543210"
def _request(flows: List[str]) -> AbriTriggerRequest:
def _request(
flows: List[str], client_booking_reference: Optional[str] = "AD0226519"
) -> AbriTriggerRequest:
return AbriTriggerRequest.model_validate(
{
"hubspot_deal_id": DEAL_ID,
@ -41,6 +47,7 @@ def _request(flows: List[str]) -> AbriTriggerRequest:
"last_submission_date": "2026-07-01",
"outcome": "no answer",
"third_party_surveyor_identifier": "THIRDPARTY",
"client_booking_reference": client_booking_reference,
}
)
@ -218,6 +225,63 @@ def test_the_log_flow_runs_when_the_deal_has_no_job_no(
assert [call[0] for call in orchestrator.calls] == ["log_job"]
# --- divergence: amend/abandon with no Job Number cannot sync to Abri ---
def test_an_amend_with_no_client_booking_reference_raises_a_divergence_error(
orchestrator: FakeOrchestrator, deal_database: FakeDealDatabase
) -> None:
# Arrange: an amend fires on a deal that was never logged (no Job Number).
request = _request(["amend_job"], client_booking_reference=None)
# Act / Assert: non-retriable, and the amend flow never runs.
with pytest.raises(DealDivergedFromAbriError) as exc_info:
dispatch_abri_flows(request, orchestrator, deal_database)
assert isinstance(exc_info.value, NonRetriableTaskError)
assert DEAL_ID in str(exc_info.value)
assert orchestrator.calls == []
def test_an_abandon_with_no_client_booking_reference_raises_a_divergence_error(
orchestrator: FakeOrchestrator, deal_database: FakeDealDatabase
) -> None:
# Arrange: an abandon fires on a deal with no Job Number — possibly a lost
# reference, so a live Abri job must not be silently left un-cancelled.
request = _request(["abandon_job"], client_booking_reference=None)
# Act / Assert
with pytest.raises(DealDivergedFromAbriError) as exc_info:
dispatch_abri_flows(request, orchestrator, deal_database)
assert isinstance(exc_info.value, NonRetriableTaskError)
assert orchestrator.calls == []
def test_an_amend_with_a_client_booking_reference_proceeds(
orchestrator: FakeOrchestrator, deal_database: FakeDealDatabase
) -> None:
# Arrange: the deal carries a Job Number, so the job exists and is amended.
request = _request(["amend_job"], client_booking_reference="AD0226519")
# Act
dispatch_abri_flows(request, orchestrator, deal_database)
# Assert
assert [call[0] for call in orchestrator.calls] == ["amend_job"]
def test_an_abandon_with_a_client_booking_reference_proceeds(
orchestrator: FakeOrchestrator, deal_database: FakeDealDatabase
) -> None:
# Arrange
request = _request(["abandon_job"], client_booking_reference="AD0226519")
# Act
dispatch_abri_flows(request, orchestrator, deal_database)
# Assert
assert [call[0] for call in orchestrator.calls] == ["abandon_job"]
# --- rejections raise, so the task fails and the DLQ is the ops inbox ---