From aca507cc47d49664a950fe2a062a02fc6a36c5ad Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:03:42 +0000 Subject: [PATCH 01/14] =?UTF-8?q?Abri=20deal=20abandonment=20triggers=20af?= =?UTF-8?q?ter=20three=20attempts=20with=20a=20negative=20outcome=20?= =?UTF-8?q?=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/hubspot_deal_differ.py | 8 +++++ etl/hubspot/tests/test_hubspot_deal_differ.py | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index fbdc69b61..65e4cb056 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -220,6 +220,14 @@ class HubspotDealDiffer: return parse_hs_date(new_deal.get("expected_commencement_date")) is not None + @staticmethod + def check_for_abri_deal_abandonment( + new_deal: Dict[str, str], + new_project: Optional[ProjectData], + old_deal: HubspotDealData, + ) -> bool: + raise NotImplementedError + @staticmethod def check_for_magicplan_trigger( new_deal: Dict[str, str], old_deal: HubspotDealData diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index 660481dd2..f5ddd7538 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -867,6 +867,40 @@ def test_abri_tenant_data_fetch__non_abri_project_id_with_abri_code__returns_fal assert result is False +# ==================================== +# ABRI DEAL ABANDONMENT TRIGGER TESTS +# ==================================== + + +def test_abri_deal_abandonment__third_attempt_with_negative_outcome__returns_true() -> ( + None +): + deal_id = uuid.uuid4() + + # Arrange + old_deal = make_old_deal( + id=deal_id, + project_code="Abri Condition", + outcome=None, + ) + new_deal = make_new_deal( + deal_id, + project_code="Abri Condition", + number_of_attempts="3", + outcome="No Answer", + ) + + # Act + result = HubspotDealDiffer.check_for_abri_deal_abandonment( + new_deal=new_deal, + new_project=None, + old_deal=old_deal, + ) + + # Assert + assert result is True + + # ======================= # DB UPDATE TRIGGER TESTS # ======================= From 262f22764a2bf85dc26646bd43328324891314da Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:03:57 +0000 Subject: [PATCH 02/14] =?UTF-8?q?Abri=20deal=20abandonment=20triggers=20af?= =?UTF-8?q?ter=20three=20attempts=20with=20a=20negative=20outcome=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/hubspot_deal_differ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 65e4cb056..2ab4994d4 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -226,7 +226,7 @@ class HubspotDealDiffer: new_project: Optional[ProjectData], old_deal: HubspotDealData, ) -> bool: - raise NotImplementedError + return True @staticmethod def check_for_magicplan_trigger( From 33848a76fe1c0323207da0fddb0376c0f03769f0 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:04:45 +0000 Subject: [PATCH 03/14] =?UTF-8?q?Abri=20deal=20abandonment=20ignores=20dea?= =?UTF-8?q?ls=20that=20are=20not=20Abri=20condition=20deals=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/tests/test_hubspot_deal_differ.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index f5ddd7538..e5b2f87c9 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -901,6 +901,33 @@ def test_abri_deal_abandonment__third_attempt_with_negative_outcome__returns_tru assert result is True +def test_abri_deal_abandonment__non_abri_deal__returns_false() -> None: + deal_id = uuid.uuid4() + + # Arrange + old_deal = make_old_deal( + id=deal_id, + project_code="Southern Retrofit", + outcome=None, + ) + new_deal = make_new_deal( + deal_id, + project_code="Southern Retrofit", + number_of_attempts="3", + outcome="No Answer", + ) + + # Act + result = HubspotDealDiffer.check_for_abri_deal_abandonment( + new_deal=new_deal, + new_project=None, + old_deal=old_deal, + ) + + # Assert + assert result is False + + # ======================= # DB UPDATE TRIGGER TESTS # ======================= From 79423f594cfcc480417aa0b9a2efbcabf73687b7 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:06:53 +0000 Subject: [PATCH 04/14] =?UTF-8?q?Abri=20deal=20abandonment=20ignores=20dea?= =?UTF-8?q?ls=20that=20are=20not=20Abri=20condition=20deals=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/hubspot_deal_differ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 2ab4994d4..3997014dd 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -226,7 +226,7 @@ class HubspotDealDiffer: new_project: Optional[ProjectData], old_deal: HubspotDealData, ) -> bool: - return True + return HubspotDealDiffer._is_abri_condition_deal(new_deal, new_project) @staticmethod def check_for_magicplan_trigger( From af85e0ed2b1aaa1466f37117efe82162c53a232b Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:07:38 +0000 Subject: [PATCH 05/14] =?UTF-8?q?Abri=20deal=20abandonment=20requires=20at?= =?UTF-8?q?=20least=20three=20parseable=20attempts=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/tests/test_hubspot_deal_differ.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index e5b2f87c9..5699262fb 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -928,6 +928,44 @@ def test_abri_deal_abandonment__non_abri_deal__returns_false() -> None: assert result is False +@pytest.mark.parametrize( + "new_overrides", + [ + {"number_of_attempts": "2"}, + {"number_of_attempts": ""}, + {"number_of_attempts": "not-a-number"}, + {}, + ], +) +def test_abri_deal_abandonment__fewer_than_three_parseable_attempts__returns_false( + new_overrides: Dict[str, str], +) -> None: + deal_id = uuid.uuid4() + + # Arrange + old_deal = make_old_deal( + id=deal_id, + project_code="Abri Condition", + outcome=None, + ) + new_deal = make_new_deal( + deal_id, + project_code="Abri Condition", + outcome="No Answer", + **new_overrides, + ) + + # Act + result = HubspotDealDiffer.check_for_abri_deal_abandonment( + new_deal=new_deal, + new_project=None, + old_deal=old_deal, + ) + + # Assert + assert result is False + + # ======================= # DB UPDATE TRIGGER TESTS # ======================= From 88093eb7cd0f1cc3c4f57ece16015039214f998e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:10:03 +0000 Subject: [PATCH 06/14] =?UTF-8?q?Abri=20deal=20abandonment=20requires=20at?= =?UTF-8?q?=20least=20three=20parseable=20attempts=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/hubspot_deal_differ.py | 8 ++++++-- etl/hubspot/utils.py | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 3997014dd..1ce82ba3a 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -2,7 +2,7 @@ from typing import Dict, List, Optional from backend.app.db.models.hubspot_deal_data import HubspotDealData from etl.hubspot.project_data import ProjectData -from etl.hubspot.utils import parse_hs_bool, parse_hs_date +from etl.hubspot.utils import parse_hs_bool, parse_hs_date, parse_hs_int class HubspotDealDiffer: @@ -226,7 +226,11 @@ class HubspotDealDiffer: new_project: Optional[ProjectData], old_deal: HubspotDealData, ) -> bool: - return HubspotDealDiffer._is_abri_condition_deal(new_deal, new_project) + if not HubspotDealDiffer._is_abri_condition_deal(new_deal, new_project): + return False + + new_attempts = parse_hs_int(new_deal.get("number_of_attempts")) + return new_attempts is not None and new_attempts >= 3 @staticmethod def check_for_magicplan_trigger( diff --git a/etl/hubspot/utils.py b/etl/hubspot/utils.py index 6077727ee..dea173324 100644 --- a/etl/hubspot/utils.py +++ b/etl/hubspot/utils.py @@ -16,6 +16,15 @@ def parse_hs_date(value: Optional[str]) -> Optional[datetime]: return None +def parse_hs_int(value: Optional[str]) -> Optional[int]: + if not value: + return None + try: + return int(value) + except ValueError: + return None + + def parse_hs_bool(value: Optional[str]) -> Optional[bool]: if value is None or value == "": return None From 41d62ce2b2fd7dd63917b9551d42b72e14b84cd6 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:10:26 +0000 Subject: [PATCH 07/14] =?UTF-8?q?Abri=20deal=20abandonment=20requires=20a?= =?UTF-8?q?=20negative=20outcome=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/tests/test_hubspot_deal_differ.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index 5699262fb..754385c5b 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -966,6 +966,83 @@ def test_abri_deal_abandonment__fewer_than_three_parseable_attempts__returns_fal assert result is False +@pytest.mark.parametrize( + "new_overrides", + [ + {"outcome": "Surveyed"}, + {"outcome": ""}, + {}, + ], +) +def test_abri_deal_abandonment__outcome_not_negative__returns_false( + new_overrides: Dict[str, str], +) -> None: + deal_id = uuid.uuid4() + + # Arrange + old_deal = make_old_deal( + id=deal_id, + project_code="Abri Condition", + outcome=None, + ) + new_deal = make_new_deal( + deal_id, + project_code="Abri Condition", + number_of_attempts="3", + **new_overrides, + ) + + # Act + result = HubspotDealDiffer.check_for_abri_deal_abandonment( + new_deal=new_deal, + new_project=None, + old_deal=old_deal, + ) + + # Assert + assert result is False + + +@pytest.mark.parametrize( + "outcome", + [ + "No Answer", + "Cancelled", + "No Show", + "Tenant Refusal", + "Not Viable", + "NO ANSWER", + ], +) +def test_abri_deal_abandonment__each_negative_outcome__returns_true( + outcome: str, +) -> None: + deal_id = uuid.uuid4() + + # Arrange + old_deal = make_old_deal( + id=deal_id, + project_code="Abri Condition", + outcome=None, + ) + new_deal = make_new_deal( + deal_id, + project_code="Abri Condition", + number_of_attempts="3", + outcome=outcome, + ) + + # Act + result = HubspotDealDiffer.check_for_abri_deal_abandonment( + new_deal=new_deal, + new_project=None, + old_deal=old_deal, + ) + + # Assert + assert result is True + + # ======================= # DB UPDATE TRIGGER TESTS # ======================= From 6de0da869e664a9f49c0d81954d2355283e44be4 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:11:14 +0000 Subject: [PATCH 08/14] =?UTF-8?q?Abri=20deal=20abandonment=20requires=20a?= =?UTF-8?q?=20negative=20outcome=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/hubspot_deal_differ.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 1ce82ba3a..96a771a12 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -13,6 +13,13 @@ class HubspotDealDiffer: ] RETROFIT_DESIGN_COMPLETE = "uploaded" LODGEMENT_COMPLETE: List[str] = ["lodgement complete", "measures lodged"] + NEGATIVE_OUTCOMES: List[str] = [ + "no answer", + "cancelled", + "no show", + "tenant refusal", + "not viable", + ] ABRI_CONDITION_ASSOCIATED_PROJECT_ID = "123456" ABRI_CONDITION_PROJECT_CODE = "Abri Condition" @@ -230,7 +237,11 @@ class HubspotDealDiffer: return False new_attempts = parse_hs_int(new_deal.get("number_of_attempts")) - return new_attempts is not None and new_attempts >= 3 + if new_attempts is None or new_attempts < 3: + return False + + new_outcome = (new_deal.get("outcome") or "").lower() + return new_outcome in HubspotDealDiffer.NEGATIVE_OUTCOMES @staticmethod def check_for_magicplan_trigger( From 9559f237417ba818fa64f242ad58fce791b7f109 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:12:59 +0000 Subject: [PATCH 09/14] =?UTF-8?q?Abri=20deal=20abandonment=20fires=20only?= =?UTF-8?q?=20when=20the=20deal=20newly=20becomes=20abandoned=20?= =?UTF-8?q?=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- backend/app/db/models/hubspot_deal_data.py | 1 + etl/hubspot/tests/test_hubspot_deal_differ.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/backend/app/db/models/hubspot_deal_data.py b/backend/app/db/models/hubspot_deal_data.py index caf1df5d5..48a6f1ba0 100644 --- a/backend/app/db/models/hubspot_deal_data.py +++ b/backend/app/db/models/hubspot_deal_data.py @@ -23,6 +23,7 @@ class HubspotDealData(SQLModel, table=True): landlord_property_id: Optional[str] = Field(default=None) uprn: Optional[str] = Field(default=None) outcome: Optional[str] = Field(default=None) + number_of_attempts: Optional[str] = Field(default=None) outcome_notes: Optional[str] = Field(default=None) booking_status: Optional[str] = Field(default=None) diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index 754385c5b..970f5e0ca 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -1043,6 +1043,34 @@ def test_abri_deal_abandonment__each_negative_outcome__returns_true( assert result is True +def test_abri_deal_abandonment__deal_already_abandoned__returns_false() -> None: + deal_id = uuid.uuid4() + + # Arrange + old_deal = make_old_deal( + id=deal_id, + project_code="Abri Condition", + number_of_attempts="3", + outcome="No Answer", + ) + new_deal = make_new_deal( + deal_id, + project_code="Abri Condition", + number_of_attempts="4", + outcome="No Answer", + ) + + # Act + result = HubspotDealDiffer.check_for_abri_deal_abandonment( + new_deal=new_deal, + new_project=None, + old_deal=old_deal, + ) + + # Assert + assert result is False + + # ======================= # DB UPDATE TRIGGER TESTS # ======================= From 58ce855e0b7e1800e91c0c48b2a698ac69a101dd Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:13:58 +0000 Subject: [PATCH 10/14] =?UTF-8?q?Abri=20deal=20abandonment=20fires=20only?= =?UTF-8?q?=20when=20the=20deal=20newly=20becomes=20abandoned=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/hubspot_deal_differ.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 96a771a12..67f840a9e 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -236,12 +236,14 @@ class HubspotDealDiffer: if not HubspotDealDiffer._is_abri_condition_deal(new_deal, new_project): return False - new_attempts = parse_hs_int(new_deal.get("number_of_attempts")) - if new_attempts is None or new_attempts < 3: + if HubspotDealDiffer._is_abandoned( + old_deal.number_of_attempts, old_deal.outcome + ): return False - new_outcome = (new_deal.get("outcome") or "").lower() - return new_outcome in HubspotDealDiffer.NEGATIVE_OUTCOMES + return HubspotDealDiffer._is_abandoned( + new_deal.get("number_of_attempts"), new_deal.get("outcome") + ) @staticmethod def check_for_magicplan_trigger( @@ -251,6 +253,16 @@ class HubspotDealDiffer: old_outcome = (old_deal.outcome or "").lower() return new_outcome == "surveyed" and old_outcome != "surveyed" + @staticmethod + def _is_abandoned( + number_of_attempts: Optional[str], outcome: Optional[str] + ) -> bool: + attempts = parse_hs_int(number_of_attempts) + if attempts is None or attempts < 3: + return False + + return (outcome or "").lower() in HubspotDealDiffer.NEGATIVE_OUTCOMES + @staticmethod def _is_abri_condition_deal( new_deal: Dict[str, str], new_project: Optional[ProjectData] From c00ae894ee9de2c9ef71f0c3e04958d765bfe491 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:14:22 +0000 Subject: [PATCH 11/14] =?UTF-8?q?Deal=20differ=20detects=20number=20of=20a?= =?UTF-8?q?ttempts=20changes=20for=20db=20update=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/tests/test_hubspot_deal_differ.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index 970f5e0ca..d1feb9a90 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -1128,6 +1128,32 @@ def test_db_update_trigger__dealname_changed__returns_true() -> None: assert result is True +def test_db_update_trigger__number_of_attempts_changed__returns_true() -> None: + deal_id = uuid.uuid4() + + # Arrange + old_deal = make_old_deal( + id=deal_id, + number_of_attempts="2", + ) + new_deal = make_new_deal( + deal_id, + hs_object_id="1", + number_of_attempts="3", + ) + + # Act + result = HubspotDealDiffer.check_for_db_update_trigger( + new_deal=new_deal, + new_company=None, + new_listing=None, + old_deal=old_deal, + ) + + # Assert + assert result is True + + def test_db_update_trigger__company_changed__returns_true() -> None: deal_id = uuid.uuid4() From 18989252f495fd98dcd1eab6d2a3a58f0104e7f1 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:17:51 +0000 Subject: [PATCH 12/14] =?UTF-8?q?Deal=20differ=20detects=20number=20of=20a?= =?UTF-8?q?ttempts=20changes=20for=20db=20update=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/hubspotClient.py | 1 + etl/hubspot/hubspotDataTodB.py | 2 ++ etl/hubspot/hubspot_deal_differ.py | 1 + 3 files changed, 4 insertions(+) diff --git a/etl/hubspot/hubspotClient.py b/etl/hubspot/hubspotClient.py index 28abf3030..918701cac 100644 --- a/etl/hubspot/hubspotClient.py +++ b/etl/hubspot/hubspotClient.py @@ -294,6 +294,7 @@ class HubspotClient: "dealstage", "pipeline", "outcome", + "number_of_attempts", "outcome_notes", "booking_status", "project_code", diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index ae2daa491..f72564c69 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -211,6 +211,7 @@ class HubspotDataToDb: ), "uprn": listing.get("national_uprn", None) if listing else None, "outcome": deal_data.get("outcome"), + "number_of_attempts": deal_data.get("number_of_attempts"), "outcome_notes": deal_data.get("outcome_notes"), "booking_status": deal_data.get("booking_status"), "project_code": deal_data.get("project_code"), @@ -322,6 +323,7 @@ class HubspotDataToDb: ), uprn=listing.get("national_uprn") if listing else None, outcome=deal_data.get("outcome"), + number_of_attempts=deal_data.get("number_of_attempts"), outcome_notes=deal_data.get("outcome_notes"), booking_status=deal_data.get("booking_status"), project_code=deal_data.get("project_code"), diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 67f840a9e..abb0f3d54 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -59,6 +59,7 @@ class HubspotDealDiffer: # --- Field mappings --- FIELD_MAP = { "outcome": "outcome", + "number_of_attempts": "number_of_attempts", "dealstage": "dealstage", "dealname": "dealname", "project_code": "project_code", From f4f915cfb71d9923d23e3f73b2e408b4792b196f Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:19:06 +0000 Subject: [PATCH 13/14] =?UTF-8?q?Abri=20deal=20abandonment=20names=20its?= =?UTF-8?q?=20attempt=20threshold=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/hubspot_deal_differ.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index abb0f3d54..de9680e65 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -13,6 +13,7 @@ class HubspotDealDiffer: ] RETROFIT_DESIGN_COMPLETE = "uploaded" LODGEMENT_COMPLETE: List[str] = ["lodgement complete", "measures lodged"] + ABANDONMENT_ATTEMPTS_THRESHOLD = 3 NEGATIVE_OUTCOMES: List[str] = [ "no answer", "cancelled", @@ -259,7 +260,10 @@ class HubspotDealDiffer: number_of_attempts: Optional[str], outcome: Optional[str] ) -> bool: attempts = parse_hs_int(number_of_attempts) - if attempts is None or attempts < 3: + if ( + attempts is None + or attempts < HubspotDealDiffer.ABANDONMENT_ATTEMPTS_THRESHOLD + ): return False return (outcome or "").lower() in HubspotDealDiffer.NEGATIVE_OUTCOMES From 1a5368571f782915efc5e7e130d1b1ea9e76718e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 10:46:24 +0000 Subject: [PATCH 14/14] add typehint --- etl/hubspot/hubspot_deal_differ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index de9680e65..1a3dfc409 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -259,7 +259,7 @@ class HubspotDealDiffer: def _is_abandoned( number_of_attempts: Optional[str], outcome: Optional[str] ) -> bool: - attempts = parse_hs_int(number_of_attempts) + attempts: Optional[int] = parse_hs_int(number_of_attempts) if ( attempts is None or attempts < HubspotDealDiffer.ABANDONMENT_ATTEMPTS_THRESHOLD