move trigger check to HubspotDealDiffer

This commit is contained in:
Daniel Roth 2026-07-07 13:15:40 +00:00
parent d9b441962f
commit 3934e2d359
5 changed files with 68 additions and 93 deletions

View file

@ -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"),
}

View file

@ -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 backend.app.db.models.hubspot_deal_data import HubspotDealData
from etl.hubspot.project_data import ProjectData from etl.hubspot.project_data import ProjectData
@ -189,6 +189,46 @@ class HubspotDealDiffer:
return False 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 @staticmethod
def check_for_abri_job_logging( def check_for_abri_job_logging(
new_deal: Dict[str, str], new_deal: Dict[str, str],

View file

@ -6,7 +6,7 @@ from backend.app.config import get_settings
from etl.hubspot.hubspotClient import HubspotClient from etl.hubspot.hubspotClient import HubspotClient
from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb from etl.hubspot.hubspotDataTodB import CompanyData, HubspotDataToDb
from etl.hubspot.project_data import ProjectData 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_deal_differ import HubspotDealDiffer
from etl.hubspot.hubspot_trigger_orchestrator_trigger_request import ( from etl.hubspot.hubspot_trigger_orchestrator_trigger_request import (
HubspotTriggerOrchestratorTriggerRequest, HubspotTriggerOrchestratorTriggerRequest,
@ -128,14 +128,14 @@ def handler(body: dict[str, Any], context: Any) -> None:
sqs_client, hubspot_deal, listing, hubspot_deal_id sqs_client, hubspot_deal, listing, hubspot_deal_id
) )
# None means no Abri flow fired this scrape; a dict is the message abri_message: Optional[Dict[str, Any]] = (
# naming the flows that did. HubspotDealDiffer.check_abri_triggers_and_construct_message(
abri_message = abri_trigger_message_for_scrape( hubspot_deal_id=hubspot_deal_id,
hubspot_deal_id=hubspot_deal_id, new_deal=hubspot_deal,
new_deal=hubspot_deal, new_project=project,
new_project=project, new_listing=listing,
new_listing=listing, old_deal=db_deal,
old_deal=db_deal, )
) )
if abri_message is not None: if abri_message is not None:
logger.info( logger.info(

View file

@ -3,13 +3,11 @@ from typing import Any, Dict
import uuid import uuid
from backend.app.db.models.hubspot_deal_data import HubspotDealData 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) BASE_TIME = datetime(2025, 12, 1, 12, 0, 0)
ABRI_PROJECT_CODE = ( ABRI_PROJECT_CODE = "[Abri Stock Condition - Privately Funded - 062026 - 1247536318670]"
"[Abri Stock Condition - Privately Funded - 062026 - 1247536318670]"
)
DEAL_ID = "9876543210" 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: def test_a_first_confirmed_survey_date_builds_a_log_job_message() -> None:
# Arrange # Arrange
old_deal = make_old_deal( old_deal = make_old_deal(project_code=ABRI_PROJECT_CODE, confirmed_survey_date=None)
project_code=ABRI_PROJECT_CODE, confirmed_survey_date=None
)
new_deal = make_new_deal( new_deal = make_new_deal(
confirmed_survey_date="2026-06-24", confirmed_survey_time="14:30" confirmed_survey_date="2026-06-24", confirmed_survey_time="14:30"
) )
# Act # Act
message = abri_trigger_message_for_scrape( message = check_for_abri_triggers_and_construct_message(
hubspot_deal_id=DEAL_ID, hubspot_deal_id=DEAL_ID,
new_deal=new_deal, new_deal=new_deal,
new_project=None, new_project=None,
@ -80,7 +76,7 @@ def test_a_changed_confirmed_survey_date_builds_an_amend_job_message() -> None:
) )
# Act # Act
message = abri_trigger_message_for_scrape( message = check_for_abri_triggers_and_construct_message(
hubspot_deal_id=DEAL_ID, hubspot_deal_id=DEAL_ID,
new_deal=new_deal, new_deal=new_deal,
new_project=None, 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") new_deal = make_new_deal(expected_commencement_date="2026-07-01")
# Act # Act
message = abri_trigger_message_for_scrape( message = check_for_abri_triggers_and_construct_message(
hubspot_deal_id=DEAL_ID, hubspot_deal_id=DEAL_ID,
new_deal=new_deal, new_deal=new_deal,
new_project=None, new_project=None,
@ -135,7 +131,7 @@ def test_several_flows_firing_in_one_scrape_share_one_message() -> None:
) )
# Act # Act
message = abri_trigger_message_for_scrape( message = check_for_abri_triggers_and_construct_message(
hubspot_deal_id=DEAL_ID, hubspot_deal_id=DEAL_ID,
new_deal=new_deal, new_deal=new_deal,
new_project=None, 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") new_deal = make_new_deal(outcome="left voicemail")
# Act # Act
message = abri_trigger_message_for_scrape( message = check_for_abri_triggers_and_construct_message(
hubspot_deal_id=DEAL_ID, hubspot_deal_id=DEAL_ID,
new_deal=new_deal, new_deal=new_deal,
new_project=None, 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: def test_no_message_for_a_non_abri_deal() -> None:
# Arrange: a survey date first set, but on another project's deal. # Arrange: a survey date first set, but on another project's deal.
old_deal = make_old_deal(project_code="Other", confirmed_survey_date=None) old_deal = make_old_deal(project_code="Other", confirmed_survey_date=None)
new_deal = make_new_deal( new_deal = make_new_deal(project_code="Other", confirmed_survey_date="2026-06-24")
project_code="Other", confirmed_survey_date="2026-06-24"
)
# Act # Act
message = abri_trigger_message_for_scrape( message = check_for_abri_triggers_and_construct_message(
hubspot_deal_id=DEAL_ID, hubspot_deal_id=DEAL_ID,
new_deal=new_deal, new_deal=new_deal,
new_project=None, 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") new_deal = make_new_deal(number_of_attempts="3", outcome="no answer")
# Act # Act
message = abri_trigger_message_for_scrape( message = check_for_abri_triggers_and_construct_message(
hubspot_deal_id=DEAL_ID, hubspot_deal_id=DEAL_ID,
new_deal=new_deal, new_deal=new_deal,
new_project=None, 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: 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 # Arrange: validation in the abri lambda is the visibility surface for
# a fired flow that lacks its place_ref; the scraper still sends. # a fired flow that lacks its place_ref; the scraper still sends.
old_deal = make_old_deal( old_deal = make_old_deal(project_code=ABRI_PROJECT_CODE, confirmed_survey_date=None)
project_code=ABRI_PROJECT_CODE, confirmed_survey_date=None
)
new_deal = make_new_deal(confirmed_survey_date="2026-06-24") new_deal = make_new_deal(confirmed_survey_date="2026-06-24")
# Act # Act
message = abri_trigger_message_for_scrape( message = check_for_abri_triggers_and_construct_message(
hubspot_deal_id=DEAL_ID, hubspot_deal_id=DEAL_ID,
new_deal=new_deal, new_deal=new_deal,
new_project=None, new_project=None,

View file

@ -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 1. fetches the deal, listing and project from the real HubSpot portal
(exactly what the scraper lambda sees), (exactly what the scraper lambda sees),
2. reads the deal's row from the real (dev) deal database, 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), 4. validates the real message contract (AbriTriggerRequest),
5. runs the real dispatch through the real AbriOrchestrator, so the fake 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 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.abri_trigger_request import AbriTriggerRequest
from applications.abri.dispatch import dispatch_abri_flows from applications.abri.dispatch import dispatch_abri_flows
from backend.app.db.models.hubspot_deal_data import HubspotDealData 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 etl.hubspot.hubspotClient import HubspotClient
from infrastructure.abri.abri_client import AbriClient from infrastructure.abri.abri_client import AbriClient
from infrastructure.abri.config import AbriConfig from infrastructure.abri.config import AbriConfig
@ -196,7 +198,7 @@ def main() -> None:
f"job_no={deal_row.client_booking_reference}" 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, hubspot_deal_id=DEAL_ID,
new_deal=hubspot_deal, new_deal=hubspot_deal,
new_project=project, new_project=project,