From 8aa28415466d5840511e832ab7e514f3cdc34bd7 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 7 Jul 2026 15:46:00 +0000 Subject: [PATCH] =?UTF-8?q?Dispatch=20the=20abandon=20flow=20last=20for=20?= =?UTF-8?q?a=20fired=20deal-change=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- applications/abri/dispatch.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/applications/abri/dispatch.py b/applications/abri/dispatch.py index f69981162..00f9223cc 100644 --- a/applications/abri/dispatch.py +++ b/applications/abri/dispatch.py @@ -1,11 +1,13 @@ """Dispatches the flows named in a trigger message through the orchestrator. The scraper decides which flows fire; this is a dumb dispatcher. Flows run -in a fixed, retry-safe order — amend, then log, then tenant sync — so that -redelivery re-runs idempotent flows before non-idempotent ones: amend is -naturally idempotent, log is guarded by the job-number check, and tenant -contact creation (not idempotent) runs last so its failure can never cause -a duplicate job log, and a log failure can never duplicate contacts. +in a fixed, retry-safe order — amend, then log, then tenant sync, then +abandon — so that redelivery re-runs idempotent flows before non-idempotent +ones: amend is naturally idempotent, log is guarded by the job-number check, +and tenant contact creation (not idempotent) runs before abandon so its +failure can never cause a duplicate job log, and a log failure can never +duplicate contacts. Abandon runs last so a cancellation can never interfere +with a log, amend or tenant sync in the same batch. """ from typing import Dict, Protocol @@ -13,9 +15,11 @@ from typing import Dict, Protocol from applications.abri.abri_trigger_request import AbriTriggerRequest from domain.abri.models import AbriRequestRejected, PlaceRef from orchestration.abri_orchestrator import ( + AbandonJobOrchestrationResult, AmendJobOrchestrationResult, AppointmentChange, ConfirmedSurveyBooking, + DealAbandonment, DealDatabaseGateway, LogJobOrchestrationResult, LogJobWriteBackError, @@ -35,6 +39,10 @@ class AbriFlows(Protocol): self, place_ref: PlaceRef, deal_id: str ) -> TenantDataSyncResult: ... + def abandon_job( + self, abandonment: DealAbandonment + ) -> AbandonJobOrchestrationResult: ... + class AbriFlowRejectedError(Exception): """An OpenHousing rejection, surfaced as a failure so the task fails. @@ -66,6 +74,8 @@ def dispatch_abri_flows( summary["log_job"] = _run_log(request, flows, deal_database) if "sync_tenant_data" in request.flows: summary["sync_tenant_data"] = _run_tenant_sync(request, flows) + if "abandon_job" in request.flows: + summary["abandon_job"] = _run_abandon(request, flows) return summary @@ -122,6 +132,21 @@ def _run_log( return f"job {outcome.job_no} logged" +def _run_abandon(request: AbriTriggerRequest, flows: AbriFlows) -> str: + # Runs last so a cancellation can never interfere with a log, amend or + # tenant sync in the same batch. A missing job_no raises the retriable + # not-yet-recorded error, which propagates so the task fails and redelivers. + outcome = flows.abandon_job( + DealAbandonment( + deal_id=request.hubspot_deal_id, + outcome=request.outcome, + ) + ) + if isinstance(outcome, AbriRequestRejected): + raise AbriFlowRejectedError(flow="abandon_job", rejection=outcome) + return f"job {outcome.job_no} abandoned" + + def _run_tenant_sync(request: AbriTriggerRequest, flows: AbriFlows) -> str: if request.place_ref is None: raise ValueError("sync_tenant_data fired without a place_ref")