Leave an already-flagged deal alone and fail loudly on a failed flag 🟩

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-27 13:34:50 +00:00
parent a397eba48d
commit b77769fdca
2 changed files with 82 additions and 5 deletions

View file

@ -1,7 +1,8 @@
from typing import Any, Dict, Optional, cast
from typing import Dict, Optional, cast
from hubspot.client import Client # type: ignore[reportMissingTypeStubs]
from hubspot.crm.deals import SimplePublicObjectInput # type: ignore[reportMissingTypeStubs]
from hubspot.crm.objects.models import SimplePublicObject as HubspotObject # type: ignore[reportMissingTypeStubs]
from infrastructure.hubspot.scrubbed_calls import scrubbed_call_with_retry
@ -24,12 +25,17 @@ class HubspotDealPropertiesClient:
a property whole (HubSpot has no append) would otherwise overwrite
edits made since the last scrape.
"""
deal: Any = scrubbed_call_with_retry(
lambda: self._client.crm.deals.basic_api.get_by_id( # type: ignore[reportUnknownMemberType]
deal_id, properties=[property_name]
deal: HubspotObject = scrubbed_call_with_retry(
lambda: cast(
HubspotObject,
self._client.crm.deals.basic_api.get_by_id( # type: ignore[reportUnknownMemberType]
deal_id, properties=[property_name]
),
)
)
properties = cast(Dict[str, Optional[str]], deal.properties)
properties: Dict[str, Optional[str]] = cast(
Dict[str, Optional[str]], deal.properties # type: ignore[reportUnknownMemberType]
)
return properties.get(property_name)
def write_deal_property(

View file

@ -10,10 +10,12 @@ 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 ApiException as DealsApiException # 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
from infrastructure.hubspot.errors import HubspotRequestError
from infrastructure.hubspot.deal_properties_client import HubspotDealPropertiesClient
from infrastructure.abri.abri_client import AbriClient
from infrastructure.abri.config import AbriConfig
@ -588,3 +590,72 @@ def test_a_void_property_is_flagged_at_the_front_of_the_deals_booking_informatio
properties={"notes_for_surveyor": "Void || Key safe code 1234"}
),
)
def test_a_void_property_with_no_existing_booking_information_is_flagged_alone(
orchestrator: AbriOrchestrator,
mock_session: MagicMock,
sdk_client: MagicMock,
) -> None:
# Arrange: HubSpot sends "" for an unset property.
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": ""
}
# Act
orchestrator.sync_tenant_data(PLACE_REF, deal_id=DEAL_ID)
# Assert: no dangling separator on an otherwise empty note.
sdk_client.crm.deals.basic_api.update.assert_called_once_with(
DEAL_ID,
simple_public_object_input=DealPropertyInput(
properties={"notes_for_surveyor": "Void ||"}
),
)
def test_a_deal_already_flagged_void_is_left_alone_so_redelivery_never_stacks_flags(
orchestrator: AbriOrchestrator,
mock_session: MagicMock,
sdk_client: MagicMock,
) -> None:
# Arrange: the state a redelivered message finds.
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": "Void || Key safe code 1234"
}
# Act
result = orchestrator.sync_tenant_data(PLACE_REF, deal_id=DEAL_ID)
# Assert
assert result == PropertyReportedVoid(place_ref=PLACE_REF)
assert sdk_client.crm.deals.basic_api.update.mock_calls == []
def test_a_failed_void_flag_write_fails_the_sync_rather_than_reporting_success(
orchestrator: AbriOrchestrator,
mock_session: MagicMock,
sdk_client: MagicMock,
) -> None:
# Arrange: nothing was created for a void property, so the whole flow is
# safe to re-run — the failure must reach the caller and be redelivered.
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": ""
}
api_error = DealsApiException(status=400, reason="Bad Request")
api_error.body = json.dumps({"category": "VALIDATION_ERROR"})
api_error.headers = {}
sdk_client.crm.deals.basic_api.update.side_effect = api_error
# Act / Assert
with pytest.raises(HubspotRequestError):
orchestrator.sync_tenant_data(PLACE_REF, deal_id=DEAL_ID)