From 3934e2d3595f2740705b2c7830561755d281adaf Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 7 Jul 2026 13:15:40 +0000 Subject: [PATCH] move trigger check to HubspotDealDiffer --- etl/hubspot/abri_flow_triggers.py | 59 -------------------- etl/hubspot/hubspot_deal_differ.py | 42 +++++++++++++- etl/hubspot/scripts/scraper/main.py | 18 +++--- etl/hubspot/tests/test_abri_flow_triggers.py | 34 +++++------ scripts/smoke_test_abri_logjob_flow.py | 8 ++- 5 files changed, 68 insertions(+), 93 deletions(-) delete mode 100644 etl/hubspot/abri_flow_triggers.py diff --git a/etl/hubspot/abri_flow_triggers.py b/etl/hubspot/abri_flow_triggers.py deleted file mode 100644 index ccc1358b7..000000000 --- a/etl/hubspot/abri_flow_triggers.py +++ /dev/null @@ -1,59 +0,0 @@ -"""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 abri_trigger_message_for_scrape( - 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.""" - flows: List[str] = [] - if HubspotDealDiffer.check_for_abri_job_amendment( - new_deal=new_deal, new_project=new_project, old_deal=old_deal - ): - flows.append("amend_job") - if HubspotDealDiffer.check_for_abri_job_logging( - new_deal=new_deal, new_project=new_project, old_deal=old_deal - ): - flows.append("log_job") - if HubspotDealDiffer.check_for_abri_tenant_data_fetch( - new_deal=new_deal, new_project=new_project, old_deal=old_deal - ): - flows.append("sync_tenant_data") - - if not flows: - return None - - confirmed_survey_date = parse_hs_date(new_deal.get("confirmed_survey_date")) - listing = new_listing or {} - return { - "hubspot_deal_id": hubspot_deal_id, - "flows": flows, - "place_ref": listing.get("owner_property_id"), - "deal_name": new_deal.get("dealname"), - "confirmed_survey_date": ( - confirmed_survey_date.date().isoformat() - if confirmed_survey_date is not None - else None - ), - "confirmed_survey_time": new_deal.get("confirmed_survey_time"), - } diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 6717be0e1..e5b32ce29 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional from backend.app.db.models.hubspot_deal_data import HubspotDealData from etl.hubspot.project_data import ProjectData @@ -189,6 +189,46 @@ class HubspotDealDiffer: return False + @staticmethod + def check_abri_triggers_and_construct_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]]: + flows: List[str] = [] + if HubspotDealDiffer.check_for_abri_job_amendment( + new_deal=new_deal, new_project=new_project, old_deal=old_deal + ): + flows.append("amend_job") + if HubspotDealDiffer.check_for_abri_job_logging( + new_deal=new_deal, new_project=new_project, old_deal=old_deal + ): + flows.append("log_job") + if HubspotDealDiffer.check_for_abri_tenant_data_fetch( + new_deal=new_deal, new_project=new_project, old_deal=old_deal + ): + flows.append("sync_tenant_data") + + if not flows: + return None + + confirmed_survey_date = parse_hs_date(new_deal.get("confirmed_survey_date")) + listing = new_listing or {} + return { + "hubspot_deal_id": hubspot_deal_id, + "flows": flows, + "place_ref": listing.get("owner_property_id"), + "deal_name": new_deal.get("dealname"), + "confirmed_survey_date": ( + confirmed_survey_date.date().isoformat() + if confirmed_survey_date is not None + else None + ), + "confirmed_survey_time": new_deal.get("confirmed_survey_time"), + } + @staticmethod def check_for_abri_job_logging( new_deal: Dict[str, str], diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 78ce9cf65..c824852cd 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -6,7 +6,7 @@ from backend.app.config import get_settings from etl.hubspot.hubspotClient import HubspotClient from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb from etl.hubspot.project_data import ProjectData -from etl.hubspot.abri_flow_triggers import abri_trigger_message_for_scrape +from etl.hubspot.abri_flow_triggers import check_for_abri_triggers_and_construct_message from etl.hubspot.hubspot_deal_differ import HubspotDealDiffer from etl.hubspot.hubspot_trigger_orchestrator_trigger_request import ( HubspotTriggerOrchestratorTriggerRequest, @@ -128,14 +128,14 @@ def handler(body: dict[str, Any], context: Any) -> None: sqs_client, hubspot_deal, listing, hubspot_deal_id ) - # None means no Abri flow fired this scrape; a dict is the message - # naming the flows that did. - abri_message = abri_trigger_message_for_scrape( - hubspot_deal_id=hubspot_deal_id, - new_deal=hubspot_deal, - new_project=project, - new_listing=listing, - old_deal=db_deal, + abri_message: Optional[Dict[str, Any]] = ( + HubspotDealDiffer.check_abri_triggers_and_construct_message( + hubspot_deal_id=hubspot_deal_id, + new_deal=hubspot_deal, + new_project=project, + new_listing=listing, + old_deal=db_deal, + ) ) if abri_message is not None: logger.info( diff --git a/etl/hubspot/tests/test_abri_flow_triggers.py b/etl/hubspot/tests/test_abri_flow_triggers.py index a586ab05c..c6868b70b 100644 --- a/etl/hubspot/tests/test_abri_flow_triggers.py +++ b/etl/hubspot/tests/test_abri_flow_triggers.py @@ -3,13 +3,11 @@ from typing import Any, Dict import uuid from backend.app.db.models.hubspot_deal_data import HubspotDealData -from etl.hubspot.abri_flow_triggers import abri_trigger_message_for_scrape +from etl.hubspot.abri_flow_triggers import check_for_abri_triggers_and_construct_message BASE_TIME = datetime(2025, 12, 1, 12, 0, 0) -ABRI_PROJECT_CODE = ( - "[Abri Stock Condition - Privately Funded - 062026 - 1247536318670]" -) +ABRI_PROJECT_CODE = "[Abri Stock Condition - Privately Funded - 062026 - 1247536318670]" DEAL_ID = "9876543210" @@ -42,15 +40,13 @@ def make_new_deal(**overrides: str) -> Dict[str, str]: def test_a_first_confirmed_survey_date_builds_a_log_job_message() -> None: # Arrange - old_deal = make_old_deal( - project_code=ABRI_PROJECT_CODE, confirmed_survey_date=None - ) + old_deal = make_old_deal(project_code=ABRI_PROJECT_CODE, confirmed_survey_date=None) new_deal = make_new_deal( confirmed_survey_date="2026-06-24", confirmed_survey_time="14:30" ) # Act - message = abri_trigger_message_for_scrape( + message = check_for_abri_triggers_and_construct_message( hubspot_deal_id=DEAL_ID, new_deal=new_deal, new_project=None, @@ -80,7 +76,7 @@ def test_a_changed_confirmed_survey_date_builds_an_amend_job_message() -> None: ) # Act - message = abri_trigger_message_for_scrape( + message = check_for_abri_triggers_and_construct_message( hubspot_deal_id=DEAL_ID, new_deal=new_deal, new_project=None, @@ -102,7 +98,7 @@ def test_a_first_expected_commencement_date_builds_a_tenant_sync_message() -> No new_deal = make_new_deal(expected_commencement_date="2026-07-01") # Act - message = abri_trigger_message_for_scrape( + message = check_for_abri_triggers_and_construct_message( hubspot_deal_id=DEAL_ID, new_deal=new_deal, new_project=None, @@ -135,7 +131,7 @@ def test_several_flows_firing_in_one_scrape_share_one_message() -> None: ) # Act - message = abri_trigger_message_for_scrape( + message = check_for_abri_triggers_and_construct_message( hubspot_deal_id=DEAL_ID, new_deal=new_deal, new_project=None, @@ -159,7 +155,7 @@ def test_no_message_when_no_abri_trigger_fires() -> None: new_deal = make_new_deal(outcome="left voicemail") # Act - message = abri_trigger_message_for_scrape( + message = check_for_abri_triggers_and_construct_message( hubspot_deal_id=DEAL_ID, new_deal=new_deal, new_project=None, @@ -174,12 +170,10 @@ def test_no_message_when_no_abri_trigger_fires() -> 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" - ) + new_deal = make_new_deal(project_code="Other", confirmed_survey_date="2026-06-24") # Act - message = abri_trigger_message_for_scrape( + message = check_for_abri_triggers_and_construct_message( hubspot_deal_id=DEAL_ID, new_deal=new_deal, new_project=None, @@ -200,7 +194,7 @@ def test_abandonment_alone_stays_unwired_and_builds_no_message() -> None: new_deal = make_new_deal(number_of_attempts="3", outcome="no answer") # Act - message = abri_trigger_message_for_scrape( + message = check_for_abri_triggers_and_construct_message( hubspot_deal_id=DEAL_ID, new_deal=new_deal, new_project=None, @@ -220,13 +214,11 @@ def test_abandonment_alone_stays_unwired_and_builds_no_message() -> None: 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_PROJECT_CODE, confirmed_survey_date=None - ) + old_deal = make_old_deal(project_code=ABRI_PROJECT_CODE, confirmed_survey_date=None) new_deal = make_new_deal(confirmed_survey_date="2026-06-24") # Act - message = abri_trigger_message_for_scrape( + message = check_for_abri_triggers_and_construct_message( hubspot_deal_id=DEAL_ID, new_deal=new_deal, new_project=None, diff --git a/scripts/smoke_test_abri_logjob_flow.py b/scripts/smoke_test_abri_logjob_flow.py index 8f0de3246..36f0f40e8 100644 --- a/scripts/smoke_test_abri_logjob_flow.py +++ b/scripts/smoke_test_abri_logjob_flow.py @@ -7,7 +7,7 @@ canned LogJob success carrying a fake job_no. Concretely, the script 1. fetches the deal, listing and project from the real HubSpot portal (exactly what the scraper lambda sees), 2. reads the deal's row from the real (dev) deal database, - 3. runs the real differ predicates via abri_trigger_message_for_scrape, + 3. runs the real differ predicates via abri_message_if_triggered, 4. validates the real message contract (AbriTriggerRequest), 5. runs the real dispatch through the real AbriOrchestrator, so the fake job_no is written to the deal's client_booking_reference property in @@ -129,7 +129,9 @@ def main() -> None: from applications.abri.abri_trigger_request import AbriTriggerRequest from applications.abri.dispatch import dispatch_abri_flows from backend.app.db.models.hubspot_deal_data import HubspotDealData - from etl.hubspot.abri_flow_triggers import abri_trigger_message_for_scrape + from etl.hubspot.abri_flow_triggers import ( + check_for_abri_triggers_and_construct_message, + ) from etl.hubspot.hubspotClient import HubspotClient from infrastructure.abri.abri_client import AbriClient from infrastructure.abri.config import AbriConfig @@ -196,7 +198,7 @@ def main() -> None: f"job_no={deal_row.client_booking_reference}" ) - message = abri_trigger_message_for_scrape( + message = check_for_abri_triggers_and_construct_message( hubspot_deal_id=DEAL_ID, new_deal=hubspot_deal, new_project=project,