Dispatch the abandon flow last for a fired deal-change 🟥

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-07 15:45:07 +00:00
parent db5ff971d8
commit ecdcf12050

View file

@ -5,11 +5,19 @@ import pytest
from applications.abri.abri_trigger_request import AbriTriggerRequest
from applications.abri.dispatch import AbriFlowRejectedError, dispatch_abri_flows
from domain.abri.models import AbriRequestRejected, AppointmentAmended, PlaceRef
from domain.abri.models import (
AbriRequestRejected,
AppointmentAmended,
JobAbandoned,
PlaceRef,
)
from orchestration.abri_orchestrator import (
AbandonJobOrchestrationResult,
AmendJobOrchestrationResult,
AppointmentChange,
ConfirmedSurveyBooking,
DealAbandonment,
JobNoNotYetRecordedError,
LogJobOrchestrationResult,
LogJobSummary,
LogJobWriteBackError,
@ -30,6 +38,7 @@ def _request(flows: List[str]) -> AbriTriggerRequest:
"deal_name": "49 Admers Crescent",
"confirmed_survey_date": "2026-06-24",
"confirmed_survey_time": "14:30",
"outcome": "no answer",
}
)
@ -53,6 +62,10 @@ class FakeOrchestrator:
contact_ids=("101", "102"),
vulnerable_contact_count=1,
)
self.abandon_outcome: AbandonJobOrchestrationResult = JobAbandoned(
job_no="AC0439951"
)
self.abandon_error: Optional[Exception] = None
def amend_job(self, change: AppointmentChange) -> AmendJobOrchestrationResult:
self.calls.append(("amend_job", change))
@ -70,6 +83,14 @@ class FakeOrchestrator:
self.calls.append(("sync_tenant_data", place_ref, deal_id))
return self.sync_outcome
def abandon_job(
self, abandonment: DealAbandonment
) -> AbandonJobOrchestrationResult:
self.calls.append(("abandon_job", abandonment))
if self.abandon_error is not None:
raise self.abandon_error
return self.abandon_outcome
class FakeDealDatabase:
"""In-memory stand-in for the deal-database gateway."""
@ -233,6 +254,70 @@ def test_a_log_write_back_failure_raises_a_non_retriable_error(
assert [call[0] for call in orchestrator.calls] == ["log_job"]
# --- abandon runs last, after amend, log and tenant sync ---
def test_abandon_runs_last_in_the_fixed_dispatch_order(
orchestrator: FakeOrchestrator, deal_database: FakeDealDatabase
) -> None:
# Arrange
request = _request(["abandon_job", "sync_tenant_data", "log_job", "amend_job"])
# Act
dispatch_abri_flows(request, orchestrator, deal_database)
# Assert
assert [call[0] for call in orchestrator.calls] == [
"amend_job",
"log_job",
"sync_tenant_data",
"abandon_job",
]
def test_the_abandon_flow_receives_the_deal_id_and_outcome_from_the_message(
orchestrator: FakeOrchestrator, deal_database: FakeDealDatabase
) -> None:
# Arrange
request = _request(["abandon_job"])
# Act
dispatch_abri_flows(request, orchestrator, deal_database)
# Assert
assert orchestrator.calls == [
("abandon_job", DealAbandonment(deal_id=DEAL_ID, outcome="no answer"))
]
def test_an_abandon_rejection_raises_so_the_task_fails(
orchestrator: FakeOrchestrator, deal_database: FakeDealDatabase
) -> None:
# Arrange
request = _request(["abandon_job"])
orchestrator.abandon_outcome = AbriRequestRejected(
code="RelayError", message="Job_no AC0439951 is invalid. Job not found."
)
# Act / Assert
with pytest.raises(AbriFlowRejectedError) as exc_info:
dispatch_abri_flows(request, orchestrator, deal_database)
assert "abandon_job" in str(exc_info.value)
assert "RelayError" in str(exc_info.value)
def test_an_abandon_before_the_job_no_is_recorded_propagates_the_retriable_error(
orchestrator: FakeOrchestrator, deal_database: FakeDealDatabase
) -> None:
# Arrange
request = _request(["abandon_job"])
orchestrator.abandon_error = JobNoNotYetRecordedError(deal_id=DEAL_ID)
# Act / Assert
with pytest.raises(JobNoNotYetRecordedError):
dispatch_abri_flows(request, orchestrator, deal_database)
# --- the task output is a PII-free summary of what ran ---
@ -252,3 +337,16 @@ def test_dispatch_returns_a_pii_free_summary_of_what_ran(
"log_job": "skipped: job AC0439951 already logged",
"sync_tenant_data": "2 contacts created (1 vulnerable)",
}
def test_the_summary_records_an_abandoned_job(
orchestrator: FakeOrchestrator, deal_database: FakeDealDatabase
) -> None:
# Arrange
request = _request(["abandon_job"])
# Act
summary = dispatch_abri_flows(request, orchestrator, deal_database)
# Assert
assert summary == {"abandon_job": "job AC0439951 abandoned"}