A scrape that fires Abri triggers builds one message naming the fired flows 🟥

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-07 10:12:41 +00:00
parent a33dd43271
commit 2e72f32d52
2 changed files with 265 additions and 0 deletions

View file

@ -0,0 +1,29 @@
"""Builds the one-per-scrape SQS message for the Abri OpenHousing lambda.
The scraper decides which flows fire (the differ predicates live in one
tested place); the lambda is a dumb dispatcher. When any predicate fires,
one message carries every fired flow plus the deal fields they need, so
flows for the same deal cannot interleave across concurrent lambda
invocations.
Abandonment (no-access) detection stays unwired until the CancelJob flow
exists check_for_abri_deal_abandonment is deliberately not called here.
"""
from typing import Any, Dict, List, Optional
from backend.app.db.models.hubspot_deal_data import HubspotDealData
from etl.hubspot.hubspot_deal_differ import HubspotDealDiffer
from etl.hubspot.project_data import ProjectData
from etl.hubspot.utils import parse_hs_date
def build_abri_trigger_message(
hubspot_deal_id: str,
new_deal: Dict[str, str],
new_project: Optional[ProjectData],
new_listing: Optional[Dict[str, str]],
old_deal: HubspotDealData,
) -> Optional[Dict[str, Any]]:
"""The Abri trigger message for this scrape, or None if no flow fired."""
raise NotImplementedError

View file

@ -0,0 +1,236 @@
from datetime import datetime, timezone
from typing import Any, Dict
import uuid
from backend.app.db.models.hubspot_deal_data import HubspotDealData
from etl.hubspot.abri_flow_triggers import build_abri_trigger_message
BASE_TIME = datetime(2025, 12, 1, 12, 0, 0)
DEAL_ID = "9876543210"
LISTING = {"listing_id": "55", "owner_property_id": "1007165", "national_uprn": "1"}
def make_old_deal(**overrides: Any) -> HubspotDealData:
return HubspotDealData(
id=overrides.get("id", uuid.uuid4()),
deal_id=DEAL_ID,
created_at=BASE_TIME,
updated_at=BASE_TIME,
**{k: v for k, v in overrides.items() if k != "id"},
)
def make_new_deal(**overrides: str) -> Dict[str, str]:
return {
"hs_object_id": DEAL_ID,
"dealname": "49 Admers Crescent",
"project_code": "Abri Condition",
**overrides,
}
# ==========================
# SINGLE-FLOW FIRING
# ==========================
def test_a_first_confirmed_survey_date_builds_a_log_job_message() -> None:
# Arrange
old_deal = make_old_deal(
project_code="Abri Condition", confirmed_survey_date=None
)
new_deal = make_new_deal(
confirmed_survey_date="2026-06-24", confirmed_survey_time="14:30"
)
# Act
message = build_abri_trigger_message(
hubspot_deal_id=DEAL_ID,
new_deal=new_deal,
new_project=None,
new_listing=LISTING,
old_deal=old_deal,
)
# Assert
assert message == {
"hubspot_deal_id": DEAL_ID,
"flows": ["log_job"],
"place_ref": "1007165",
"deal_name": "49 Admers Crescent",
"confirmed_survey_date": "2026-06-24",
"confirmed_survey_time": "14:30",
}
def test_a_changed_confirmed_survey_date_builds_an_amend_job_message() -> None:
# Arrange
old_deal = make_old_deal(
project_code="Abri Condition",
confirmed_survey_date=datetime(2026, 6, 18, tzinfo=timezone.utc),
)
new_deal = make_new_deal(
confirmed_survey_date="2026-06-24", confirmed_survey_time="14:30"
)
# Act
message = build_abri_trigger_message(
hubspot_deal_id=DEAL_ID,
new_deal=new_deal,
new_project=None,
new_listing=LISTING,
old_deal=old_deal,
)
# Assert
assert message is not None
assert message["flows"] == ["amend_job"]
assert message["confirmed_survey_date"] == "2026-06-24"
def test_a_first_expected_commencement_date_builds_a_tenant_sync_message() -> None:
# Arrange
old_deal = make_old_deal(
project_code="Abri Condition", expected_commencement_date=None
)
new_deal = make_new_deal(expected_commencement_date="2026-07-01")
# Act
message = build_abri_trigger_message(
hubspot_deal_id=DEAL_ID,
new_deal=new_deal,
new_project=None,
new_listing=LISTING,
old_deal=old_deal,
)
# Assert
assert message is not None
assert message["flows"] == ["sync_tenant_data"]
assert message["place_ref"] == "1007165"
# ==========================
# ONE MESSAGE PER SCRAPE
# ==========================
def test_several_flows_firing_in_one_scrape_share_one_message() -> None:
# Arrange: date changed and commencement date first set together.
old_deal = make_old_deal(
project_code="Abri Condition",
confirmed_survey_date=datetime(2026, 6, 18, tzinfo=timezone.utc),
expected_commencement_date=None,
)
new_deal = make_new_deal(
confirmed_survey_date="2026-06-24",
confirmed_survey_time="14:30",
expected_commencement_date="2026-07-01",
)
# Act
message = build_abri_trigger_message(
hubspot_deal_id=DEAL_ID,
new_deal=new_deal,
new_project=None,
new_listing=LISTING,
old_deal=old_deal,
)
# Assert
assert message is not None
assert message["flows"] == ["amend_job", "sync_tenant_data"]
# ==========================
# NOTHING FIRES
# ==========================
def test_no_message_when_no_abri_trigger_fires() -> None:
# Arrange: something changed, but nothing Abri cares about.
old_deal = make_old_deal(project_code="Abri Condition", outcome=None)
new_deal = make_new_deal(outcome="left voicemail")
# Act
message = build_abri_trigger_message(
hubspot_deal_id=DEAL_ID,
new_deal=new_deal,
new_project=None,
new_listing=LISTING,
old_deal=old_deal,
)
# Assert
assert message is None
def test_no_message_for_a_non_abri_deal() -> None:
# Arrange: a survey date first set, but on another project's deal.
old_deal = make_old_deal(project_code="Other", confirmed_survey_date=None)
new_deal = make_new_deal(
project_code="Other", confirmed_survey_date="2026-06-24"
)
# Act
message = build_abri_trigger_message(
hubspot_deal_id=DEAL_ID,
new_deal=new_deal,
new_project=None,
new_listing=LISTING,
old_deal=old_deal,
)
# Assert
assert message is None
def test_abandonment_alone_stays_unwired_and_builds_no_message() -> None:
# Arrange: the abandonment predicate would fire, but CancelJob does not
# exist yet, so no flow may be dispatched for it.
old_deal = make_old_deal(
project_code="Abri Condition", number_of_attempts="2", outcome=None
)
new_deal = make_new_deal(number_of_attempts="3", outcome="no answer")
# Act
message = build_abri_trigger_message(
hubspot_deal_id=DEAL_ID,
new_deal=new_deal,
new_project=None,
new_listing=LISTING,
old_deal=old_deal,
)
# Assert
assert message is None
# ==========================
# PAYLOAD ROBUSTNESS
# ==========================
def test_a_missing_listing_still_builds_the_message_without_a_place_ref() -> None:
# Arrange: validation in the abri lambda is the visibility surface for
# a fired flow that lacks its place_ref; the scraper still sends.
old_deal = make_old_deal(
project_code="Abri Condition", confirmed_survey_date=None
)
new_deal = make_new_deal(confirmed_survey_date="2026-06-24")
# Act
message = build_abri_trigger_message(
hubspot_deal_id=DEAL_ID,
new_deal=new_deal,
new_project=None,
new_listing=None,
old_deal=old_deal,
)
# Assert
assert message is not None
assert message["place_ref"] is None
assert message["confirmed_survey_time"] is None