From b8316418e14ab044ab3fafe50c78e2884e4237e1 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 8 Jul 2026 14:45:03 +0000 Subject: [PATCH 1/2] Add planning-authority HubSpot deal properties to ETL sync Sync Planning Authority, Designated Area, Article 4 PD Rights, and Listed Building from HubSpot into hubspot_deal_data so downstream consumers of the existing HubSpot ETL process can read them. Co-Authored-By: Claude Sonnet 5 --- backend/app/db/models/hubspot_deal_data.py | 5 ++ etl/hubspot/hubspotDataTodB.py | 8 ++++ etl/hubspot/hubspot_deal_differ.py | 4 ++ etl/hubspot/tests/test_hubspot_data_to_db.py | 48 ++++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/backend/app/db/models/hubspot_deal_data.py b/backend/app/db/models/hubspot_deal_data.py index 88bb5a69f..d5db4c20a 100644 --- a/backend/app/db/models/hubspot_deal_data.py +++ b/backend/app/db/models/hubspot_deal_data.py @@ -93,6 +93,11 @@ class HubspotDealData(SQLModel, table=True): last_outbound_email: Optional[datetime] = Field(default=None) last_submission_date: Optional[datetime] = Field(default=None) + planning_authority: Optional[str] = Field(default=None) + designated_area: Optional[str] = Field(default=None) + article_4_pd_rights: Optional[str] = Field(default=None) + listed_building: Optional[str] = Field(default=None) + created_at: Optional[datetime] = Field( sa_column=Column( DateTime(timezone=True), diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 54c174c3f..82e50747d 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -302,6 +302,10 @@ class HubspotDataToDb: "last_submission_date": parse_hs_date( deal_data.get("last_submission_date") ), + "planning_authority": deal_data.get("planning_authority"), + "designated_area": deal_data.get("designated_area"), + "article_4_pd_rights": deal_data.get("article_pd_rights"), + "listed_building": deal_data.get("listed_building"), }.items(): setattr(existing, attr, value) @@ -407,6 +411,10 @@ class HubspotDataToDb: last_outbound_call=parse_hs_date(deal_data.get("last_outbound_call")), last_outbound_email=parse_hs_date(deal_data.get("last_outbound_email")), last_submission_date=parse_hs_date(deal_data.get("last_submission_date")), + planning_authority=deal_data.get("planning_authority"), + designated_area=deal_data.get("designated_area"), + article_4_pd_rights=deal_data.get("article_pd_rights"), + listed_building=deal_data.get("listed_building"), ) def _handle_existing_photo_upload( diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 22d7fb83c..2aa2c3bf1 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -109,6 +109,10 @@ class HubspotDealDiffer: "measures_for_pibi_ordered": "measures_for_pibi_ordered", "property_halted_reason": "property_halted_reason", "technical_approved_measures_for_install": "technical_approved_measures_for_install", + "planning_authority": "planning_authority", + "designated_area": "designated_area", + "article_pd_rights": "article_4_pd_rights", + "listed_building": "listed_building", } for hs_field, db_field in FIELD_MAP.items(): diff --git a/etl/hubspot/tests/test_hubspot_data_to_db.py b/etl/hubspot/tests/test_hubspot_data_to_db.py index 8499ccd08..7d1693040 100644 --- a/etl/hubspot/tests/test_hubspot_data_to_db.py +++ b/etl/hubspot/tests/test_hubspot_data_to_db.py @@ -127,3 +127,51 @@ def test_build_new_deal__client_booking_reference_maps_to_job_no() -> None: ) assert new_deal.client_booking_reference == "AD0226519" + + +def test_build_new_deal__planning_fields_mapped() -> None: + new_deal = _make_instance()._build_new_deal( + deal_id="MOCK_DEAL_ID", + deal_data={ + "planning_authority": "Test Council", + "designated_area": "Conservation Area", + "article_pd_rights": "Removed", + "listed_building": "Grade II", + }, + listing=None, + company=None, + project=None, + ) + + assert new_deal.planning_authority == "Test Council" + assert new_deal.designated_area == "Conservation Area" + assert new_deal.article_4_pd_rights == "Removed" + assert new_deal.listed_building == "Grade II" + + +def test_update_existing_deal__planning_fields_overwritten() -> None: + existing = HubspotDealData( + deal_id="MOCK_DEAL_ID", + planning_authority="Old Council", + designated_area="Old Area", + article_4_pd_rights="Old Rights", + listed_building="Old Listing", + ) + deal_data = { + "planning_authority": "New Council", + "designated_area": "New Area", + "article_pd_rights": "New Rights", + "listed_building": "New Listing", + } + + _make_instance()._update_existing_deal( + existing=existing, + deal_data=deal_data, + listing=None, + company=None, + ) + + assert existing.planning_authority == "New Council" + assert existing.designated_area == "New Area" + assert existing.article_4_pd_rights == "New Rights" + assert existing.listed_building == "New Listing" From 04d1df1343f92e9ddf05b4c50269a842ba894f78 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 8 Jul 2026 15:12:24 +0000 Subject: [PATCH 2/2] Add remaining planning/address-profiling fields to HubSpot ETL sync Sync Design Constraints, Planning Comments, Planning Status, and Planning Suggested Approach, matching the columns added in Hestia-Homes/assessment-model#377. Co-Authored-By: Claude Sonnet 5 --- backend/app/db/models/hubspot_deal_data.py | 4 ++++ etl/hubspot/hubspotDataTodB.py | 12 ++++++++++++ etl/hubspot/hubspot_deal_differ.py | 4 ++++ etl/hubspot/tests/test_hubspot_data_to_db.py | 20 ++++++++++++++++++++ 4 files changed, 40 insertions(+) diff --git a/backend/app/db/models/hubspot_deal_data.py b/backend/app/db/models/hubspot_deal_data.py index d5db4c20a..2445045e4 100644 --- a/backend/app/db/models/hubspot_deal_data.py +++ b/backend/app/db/models/hubspot_deal_data.py @@ -97,6 +97,10 @@ class HubspotDealData(SQLModel, table=True): designated_area: Optional[str] = Field(default=None) article_4_pd_rights: Optional[str] = Field(default=None) listed_building: Optional[str] = Field(default=None) + design_constraints: Optional[str] = Field(default=None) + planning_comments: Optional[str] = Field(default=None) + planning_status: Optional[str] = Field(default=None) + planning_suggested_approach: Optional[str] = Field(default=None) created_at: Optional[datetime] = Field( sa_column=Column( diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index 82e50747d..68f84477e 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -306,6 +306,12 @@ class HubspotDataToDb: "designated_area": deal_data.get("designated_area"), "article_4_pd_rights": deal_data.get("article_pd_rights"), "listed_building": deal_data.get("listed_building"), + "design_constraints": deal_data.get("design_constraints"), + "planning_comments": deal_data.get("planning_comments"), + "planning_status": deal_data.get("planning_status"), + "planning_suggested_approach": deal_data.get( + "planning_suggested_approach" + ), }.items(): setattr(existing, attr, value) @@ -415,6 +421,12 @@ class HubspotDataToDb: designated_area=deal_data.get("designated_area"), article_4_pd_rights=deal_data.get("article_pd_rights"), listed_building=deal_data.get("listed_building"), + design_constraints=deal_data.get("design_constraints"), + planning_comments=deal_data.get("planning_comments"), + planning_status=deal_data.get("planning_status"), + planning_suggested_approach=deal_data.get( + "planning_suggested_approach" + ), ) def _handle_existing_photo_upload( diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 2aa2c3bf1..fc8fbfd4e 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -113,6 +113,10 @@ class HubspotDealDiffer: "designated_area": "designated_area", "article_pd_rights": "article_4_pd_rights", "listed_building": "listed_building", + "design_constraints": "design_constraints", + "planning_comments": "planning_comments", + "planning_status": "planning_status", + "planning_suggested_approach": "planning_suggested_approach", } for hs_field, db_field in FIELD_MAP.items(): diff --git a/etl/hubspot/tests/test_hubspot_data_to_db.py b/etl/hubspot/tests/test_hubspot_data_to_db.py index 7d1693040..9f1f94ccd 100644 --- a/etl/hubspot/tests/test_hubspot_data_to_db.py +++ b/etl/hubspot/tests/test_hubspot_data_to_db.py @@ -137,6 +137,10 @@ def test_build_new_deal__planning_fields_mapped() -> None: "designated_area": "Conservation Area", "article_pd_rights": "Removed", "listed_building": "Grade II", + "design_constraints": "Sloped site", + "planning_comments": "Awaiting decision", + "planning_status": "Submitted", + "planning_suggested_approach": "Full application", }, listing=None, company=None, @@ -147,6 +151,10 @@ def test_build_new_deal__planning_fields_mapped() -> None: assert new_deal.designated_area == "Conservation Area" assert new_deal.article_4_pd_rights == "Removed" assert new_deal.listed_building == "Grade II" + assert new_deal.design_constraints == "Sloped site" + assert new_deal.planning_comments == "Awaiting decision" + assert new_deal.planning_status == "Submitted" + assert new_deal.planning_suggested_approach == "Full application" def test_update_existing_deal__planning_fields_overwritten() -> None: @@ -156,12 +164,20 @@ def test_update_existing_deal__planning_fields_overwritten() -> None: designated_area="Old Area", article_4_pd_rights="Old Rights", listed_building="Old Listing", + design_constraints="Old Constraints", + planning_comments="Old Comments", + planning_status="Old Status", + planning_suggested_approach="Old Approach", ) deal_data = { "planning_authority": "New Council", "designated_area": "New Area", "article_pd_rights": "New Rights", "listed_building": "New Listing", + "design_constraints": "New Constraints", + "planning_comments": "New Comments", + "planning_status": "New Status", + "planning_suggested_approach": "New Approach", } _make_instance()._update_existing_deal( @@ -175,3 +191,7 @@ def test_update_existing_deal__planning_fields_overwritten() -> None: assert existing.designated_area == "New Area" assert existing.article_4_pd_rights == "New Rights" assert existing.listed_building == "New Listing" + assert existing.design_constraints == "New Constraints" + assert existing.planning_comments == "New Comments" + assert existing.planning_status == "New Status" + assert existing.planning_suggested_approach == "New Approach"