From f742795434e3fa79bab1df0e658744819edf2296 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 27 Jul 2026 13:28:50 +0000 Subject: [PATCH] =?UTF-8?q?Flag=20a=20void=20property=20at=20the=20front?= =?UTF-8?q?=20of=20the=20deal's=20booking=20information=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- etl/hubspot/hubspotClient.py | 2 +- etl/hubspot/hubspot_deal_differ.py | 2 +- .../hubspot/deal_properties_client.py | 11 ++++++++ orchestration/abri_orchestrator.py | 12 ++++++--- ...test_abri_orchestrator_tenant_data_sync.py | 27 +++++++++++++++++++ 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/etl/hubspot/hubspotClient.py b/etl/hubspot/hubspotClient.py index 28d67cafb..76964bdd4 100644 --- a/etl/hubspot/hubspotClient.py +++ b/etl/hubspot/hubspotClient.py @@ -264,7 +264,7 @@ class HubspotClient: "number_of_attempts", "outcome_notes", "booking_status", - "extra_booking_information", + "notes_for_surveyor", "project_code", "major_condition_issue_description", "major_condition_issue_photos", diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index c2c5212ab..ec07af5e6 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -70,7 +70,7 @@ class HubspotDealDiffer: "project_code": "project_code", "outcome_notes": "outcome_notes", "booking_status": "booking_status", - "extra_booking_information": "extra_booking_information", + "notes_for_surveyor": "notes_for_surveyor", "major_condition_issue_description": "major_condition_issue_description", "major_condition_issue_photos": "major_condition_issue_photos", "coordination_status__stage_1_": "coordination_status", diff --git a/infrastructure/hubspot/deal_properties_client.py b/infrastructure/hubspot/deal_properties_client.py index dda4813a4..d6f72b9e6 100644 --- a/infrastructure/hubspot/deal_properties_client.py +++ b/infrastructure/hubspot/deal_properties_client.py @@ -1,3 +1,5 @@ +from typing import Any, Dict, Optional, cast + from hubspot.client import Client # type: ignore[reportMissingTypeStubs] from hubspot.crm.deals import SimplePublicObjectInput # type: ignore[reportMissingTypeStubs] @@ -15,6 +17,15 @@ class HubspotDealPropertiesClient: def __init__(self, sdk_client: Client) -> None: self._client: Client = sdk_client + def read_deal_property(self, deal_id: str, property_name: str) -> Optional[str]: + """The property's current value, or None when it is unset. + + Read live rather than from the scraped snapshot: callers that rewrite + a property whole (HubSpot has no append) would otherwise overwrite + edits made since the last scrape. + """ + raise NotImplementedError + def write_deal_property( self, deal_id: str, property_name: str, value: str ) -> None: diff --git a/orchestration/abri_orchestrator.py b/orchestration/abri_orchestrator.py index 74900336a..dfbf2dd80 100644 --- a/orchestration/abri_orchestrator.py +++ b/orchestration/abri_orchestrator.py @@ -25,10 +25,14 @@ from infrastructure.hubspot.errors import HubspotRequestError # property ID is fixed externally, the domain and database call it job_no. JOB_NO_DEAL_PROPERTY = "client_booking_reference" -# The note left on a deal whose place Abri reports as having no live tenancy. -# Worded as what Abri said rather than as a conclusion about the property: the -# same signal would arrive for a place_ref the deal should never have carried. -VOID_PROPERTY_NOTE = "Abri reports no live tenancy for this property." +# The HubSpot deal property behind "Extra booking information"; the property +# ID is fixed externally, and the surveyor-facing label is what users see. +NOTES_FOR_SURVEYOR_DEAL_PROPERTY = "notes_for_surveyor" + +# Prepended to that property when Abri reports the place as having no live +# tenancy, so a surveyor sees it first. Doubles as the idempotency marker: a +# note already carrying it is left alone, so redelivery never stacks markers. +VOID_MARKER = "Void ||" def _vulnerability_description(tenant: Tenant) -> Optional[str]: diff --git a/tests/orchestration/test_abri_orchestrator_tenant_data_sync.py b/tests/orchestration/test_abri_orchestrator_tenant_data_sync.py index a7368ecb2..a62580bd4 100644 --- a/tests/orchestration/test_abri_orchestrator_tenant_data_sync.py +++ b/tests/orchestration/test_abri_orchestrator_tenant_data_sync.py @@ -10,6 +10,7 @@ import requests from hubspot.crm.associations.v4 import AssociationSpec # type: ignore[reportMissingTypeStubs] from hubspot.crm.contacts import ApiException as ContactsApiException # type: ignore[reportMissingTypeStubs] from hubspot.crm.contacts import SimplePublicObjectInputForCreate # type: ignore[reportMissingTypeStubs] +from hubspot.crm.deals import SimplePublicObjectInput as DealPropertyInput # type: ignore[reportMissingTypeStubs] from domain.abri.models import AbriRequestRejected, PlaceRef from infrastructure.hubspot.deal_contacts_client import HubspotDealContactsClient @@ -561,3 +562,29 @@ def test_sync_tenant_data_reports_the_property_void_and_creates_no_contacts( # Assert assert result == PropertyReportedVoid(place_ref=PLACE_REF) assert sdk_client.crm.contacts.mock_calls == [] + + +def test_a_void_property_is_flagged_at_the_front_of_the_deals_booking_information( + orchestrator: AbriOrchestrator, + mock_session: MagicMock, + sdk_client: MagicMock, +) -> None: + # Arrange: a surveyor must see the property is empty before whatever the + # coordinator already wrote for them. + mock_session.post.return_value.content = _load_fixture( + "abri_relay_getscstenantdata_notenancyfound_response.xml" + ) + sdk_client.crm.deals.basic_api.get_by_id.return_value.properties = { + "notes_for_surveyor": "Key safe code 1234" + } + + # Act + orchestrator.sync_tenant_data(PLACE_REF, deal_id=DEAL_ID) + + # Assert + sdk_client.crm.deals.basic_api.update.assert_called_once_with( + DEAL_ID, + simple_public_object_input=DealPropertyInput( + properties={"notes_for_surveyor": "Void || Key safe code 1234"} + ), + )