mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
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:
parent
a33dd43271
commit
2e72f32d52
2 changed files with 265 additions and 0 deletions
29
etl/hubspot/abri_flow_triggers.py
Normal file
29
etl/hubspot/abri_flow_triggers.py
Normal 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
|
||||
236
etl/hubspot/tests/test_abri_flow_triggers.py
Normal file
236
etl/hubspot/tests/test_abri_flow_triggers.py
Normal 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
|
||||
Loading…
Add table
Reference in a new issue