From 489b0ba30eb730139916901da7b585627428c3e9 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 8 May 2026 13:05:38 +0000 Subject: [PATCH] =?UTF-8?q?Add=20MagicPlan=20SQS=20trigger=20to=20HubSpot?= =?UTF-8?q?=20orchestrator=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/config.py | 1 + .../hubspot_trigger_orchestrator/__init__.py | 0 .../tests/__init__.py | 0 .../tests/test_orchestrator.py | 148 ++++++++++++++++++ etl/hubspot/scripts/scraper/main.py | 21 +++ 5 files changed, 170 insertions(+) create mode 100644 backend/hubspot_trigger_orchestrator/__init__.py create mode 100644 backend/hubspot_trigger_orchestrator/tests/__init__.py create mode 100644 backend/hubspot_trigger_orchestrator/tests/test_orchestrator.py diff --git a/backend/app/config.py b/backend/app/config.py index e939d6e4..21f12902 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -39,6 +39,7 @@ class Settings(BaseSettings): ENGINE_SQS_URL: str = "changeme" CATEGORISATION_SQS_URL: str = "changeme" PASHUB_TO_ARA_SQS_URL: str = "changeme" + MAGICPLAN_SQS_URL: str = "changeme" POSTCODE_SPLITTER_SQS_URL: str = "changeme" COMBINER_SQS_URL: str = "changeme" diff --git a/backend/hubspot_trigger_orchestrator/__init__.py b/backend/hubspot_trigger_orchestrator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/hubspot_trigger_orchestrator/tests/__init__.py b/backend/hubspot_trigger_orchestrator/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/hubspot_trigger_orchestrator/tests/test_orchestrator.py b/backend/hubspot_trigger_orchestrator/tests/test_orchestrator.py new file mode 100644 index 00000000..6d18c4b4 --- /dev/null +++ b/backend/hubspot_trigger_orchestrator/tests/test_orchestrator.py @@ -0,0 +1,148 @@ +import json +import uuid +from typing import Any, Dict, Optional +from unittest.mock import MagicMock, patch + +import pytest + +from backend.app.db.models.hubspot_deal_data import HubspotDealData +from etl.hubspot.scripts.scraper.main import handler + +COORDINATION_COMPLETE = "(v1) ioe/mtp complete" +DEAL_NAME = "123 Main Street" +UPRN = "12345678" +DEAL_ID = "999" +MAGICPLAN_QUEUE_URL = "https://sqs.eu-west-2.amazonaws.com/123/magic-plan-dev" + + +def make_hubspot_deal( + coordination_status: Optional[str] = None, **kwargs: Any +) -> Dict[str, Any]: + deal: Dict[str, Any] = { + "hs_object_id": DEAL_ID, + "dealname": DEAL_NAME, + "pashub_link": None, + **kwargs, + } + if coordination_status is not None: + deal["coordination_status__stage_1_"] = coordination_status + return deal + + +def make_db_deal(coordination_status: Optional[str] = None, **kwargs: Any) -> HubspotDealData: + return HubspotDealData( + id=uuid.uuid4(), + deal_id=DEAL_ID, + coordination_status=coordination_status, + **kwargs, + ) + + +def run_handler( + hubspot_deal: Dict[str, Any], + db_deal: Optional[HubspotDealData], + listing: Optional[dict], +) -> MagicMock: + mock_sqs = MagicMock() + mock_sqs.send_message.return_value = {"MessageId": "test-id"} + + with ( + patch("etl.hubspot.scripts.scraper.main.HubspotDataToDb") as mock_db_cls, + patch("etl.hubspot.scripts.scraper.main.HubspotClient") as mock_hs_cls, + patch("etl.hubspot.scripts.scraper.main.boto3") as mock_boto3, + patch("etl.hubspot.scripts.scraper.main.get_settings") as mock_settings, + ): + mock_db_cls.return_value.find_deal_with_deal_id.return_value = db_deal + mock_db_cls.return_value.upsert_deal.return_value = None + mock_hs_cls.return_value.get_deal_and_company_and_listing.return_value = ( + hubspot_deal, + None, + listing, + ) + mock_boto3.client.return_value = mock_sqs + mock_settings.return_value.MAGICPLAN_SQS_URL = MAGICPLAN_QUEUE_URL + mock_settings.return_value.PASHUB_TO_ARA_SQS_URL = "https://sqs.test/pashub" + + handler.__wrapped__({"hubspot_deal_id": DEAL_ID}, "") + + return mock_sqs + + +# ======================= +# NEW DEAL PATH +# ======================= + + +def test_new_deal_in_coordination_complete__sends_sqs_message() -> None: + # Arrange + hubspot_deal = make_hubspot_deal(coordination_status=COORDINATION_COMPLETE) + listing = {"national_uprn": UPRN} + + # Act + mock_sqs = run_handler(hubspot_deal=hubspot_deal, db_deal=None, listing=listing) + + # Assert + mock_sqs.send_message.assert_called_once_with( + QueueUrl=MAGICPLAN_QUEUE_URL, + MessageBody=json.dumps({"address": DEAL_NAME, "uprn": UPRN}), + ) + + +def test_new_deal_not_in_coordination_complete__no_sqs_message() -> None: + # Arrange + hubspot_deal = make_hubspot_deal(coordination_status="in progress") + + # Act + mock_sqs = run_handler(hubspot_deal=hubspot_deal, db_deal=None, listing=None) + + # Assert + mock_sqs.send_message.assert_not_called() + + +def test_new_deal_with_no_listing__uprn_is_none_in_message() -> None: + # Arrange + hubspot_deal = make_hubspot_deal(coordination_status=COORDINATION_COMPLETE) + + # Act + mock_sqs = run_handler(hubspot_deal=hubspot_deal, db_deal=None, listing=None) + + # Assert + mock_sqs.send_message.assert_called_once_with( + QueueUrl=MAGICPLAN_QUEUE_URL, + MessageBody=json.dumps({"address": DEAL_NAME, "uprn": None}), + ) + + +# ======================= +# EXISTING DEAL PATH +# ======================= + + +def test_existing_deal_transitions_to_coordination_complete__sends_sqs_message() -> None: + # Arrange + db_deal = make_db_deal(coordination_status="in progress") + hubspot_deal = make_hubspot_deal(coordination_status=COORDINATION_COMPLETE) + listing = {"national_uprn": UPRN} + + # Act + mock_sqs = run_handler(hubspot_deal=hubspot_deal, db_deal=db_deal, listing=listing) + + # Assert + mock_sqs.send_message.assert_called_once_with( + QueueUrl=MAGICPLAN_QUEUE_URL, + MessageBody=json.dumps({"address": DEAL_NAME, "uprn": UPRN}), + ) + + +def test_existing_deal_already_in_coordination_complete_unrelated_change__no_sqs_message() -> None: + # Arrange + db_deal = make_db_deal(coordination_status=COORDINATION_COMPLETE, dealname="Old Name") + hubspot_deal = make_hubspot_deal( + coordination_status=COORDINATION_COMPLETE, dealname="New Name" + ) + + # Act + mock_sqs = run_handler(hubspot_deal=hubspot_deal, db_deal=db_deal, listing=None) + + # Assert + mock_sqs.send_message.assert_not_called() diff --git a/etl/hubspot/scripts/scraper/main.py b/etl/hubspot/scripts/scraper/main.py index 3ed208a2..cd76e26f 100644 --- a/etl/hubspot/scripts/scraper/main.py +++ b/etl/hubspot/scripts/scraper/main.py @@ -56,6 +56,13 @@ def handler(body: dict[str, Any], context: Any) -> None: f"Triggering Pas Hub file fetcher for HubSpot deal ID {hubspot_deal_id}" ) _trigger_pashub_fetcher(sqs_client, hubspot_deal_id, hubspot_deal) + + coordination_status = (hubspot_deal.get("coordination_status__stage_1_") or "").lower() + if coordination_status in HubspotDealDiffer.COORDINATION_COMPLETE: + logger.info( + f"Triggering MagicPlan fetcher for HubSpot deal ID {hubspot_deal_id}" + ) + _trigger_magicplan_fetcher(sqs_client, hubspot_deal, listing) else: # Deal already in db, check whether anything has changed logger.info( @@ -97,9 +104,23 @@ def handler(body: dict[str, Any], context: Any) -> None: f"Not Triggering PasHub file fetcher for HubSpot deal ID {hubspot_deal_id}" ) + if HubspotDealDiffer.check_for_magicplan_trigger( + new_deal=hubspot_deal, old_deal=db_deal + ): + logger.info( + f"Triggering MagicPlan fetcher for HubSpot deal ID {hubspot_deal_id}" + ) + _trigger_magicplan_fetcher(sqs_client, hubspot_deal, listing) + print("done") +def _trigger_magicplan_fetcher( + sqs_client: Any, hubspot_deal: Dict[str, str], listing: Optional[dict[str, str]] +) -> None: + raise NotImplementedError + + def _trigger_pashub_fetcher( sqs_client: Any, deal_id: str, hubspot_deal: Dict[str, str] ) -> None: