A failed HubSpot write raises a scrubbed error carrying the orphaned job_no 🟩

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-06 15:58:36 +00:00
parent 350a76f942
commit 049475ec96
2 changed files with 60 additions and 11 deletions

View file

@ -1,6 +1,40 @@
import json
from typing import Any, Dict, Optional, cast
from hubspot.client import Client # type: ignore[reportMissingTypeStubs]
from hubspot.crm.deals import SimplePublicObjectInput # type: ignore[reportMissingTypeStubs]
from infrastructure.hubspot.errors import HubspotRequestError
def _scrubbed(error: Exception) -> HubspotRequestError:
"""Reduce an SDK error to non-PII diagnostics, dropping the body.
Each HubSpot sub-module ships its own ApiException class with no shared
base beyond Exception, so the fields are read by duck-typing.
"""
status = getattr(error, "status", None)
status_code = status if isinstance(status, int) else None
category: Optional[str] = None
correlation_id: Optional[str] = None
try:
parsed: Any = json.loads(getattr(error, "body", None) or "")
if isinstance(parsed, dict):
details = cast(Dict[str, Any], parsed)
raw_category = details.get("category")
raw_correlation_id = details.get("correlationId")
category = raw_category if isinstance(raw_category, str) else None
correlation_id = (
raw_correlation_id if isinstance(raw_correlation_id, str) else None
)
except ValueError:
pass
return HubspotRequestError(
status_code=status_code, category=category, correlation_id=correlation_id
)
class HubspotDealPropertiesClient:
"""Focused HubSpot client for writing single deal properties.
@ -16,9 +50,13 @@ class HubspotDealPropertiesClient:
def write_deal_property(
self, deal_id: str, property_name: str, value: str
) -> None:
self._client.crm.deals.basic_api.update( # type: ignore[reportUnknownMemberType]
deal_id,
simple_public_object_input=SimplePublicObjectInput(
properties={property_name: value}
),
)
try:
self._client.crm.deals.basic_api.update( # type: ignore[reportUnknownMemberType]
deal_id,
simple_public_object_input=SimplePublicObjectInput(
properties={property_name: value}
),
)
except Exception as error:
# `from None` keeps the body-carrying SDK error out of tracebacks.
raise _scrubbed(error) from None

View file

@ -7,6 +7,7 @@ from domain.abri.models import AbriRequestRejected, LogJobRequest, PlaceRef
from domain.abri.slots import slot_for_confirmed_time
from infrastructure.abri.abri_client import AbriClient
from infrastructure.hubspot.deal_properties_client import HubspotDealPropertiesClient
from infrastructure.hubspot.errors import HubspotRequestError
# The HubSpot deal property that carries the OpenHousing job number; the
# property ID is fixed externally, the domain and database call it job_no.
@ -98,11 +99,21 @@ class AbriLogJobOrchestrator:
if isinstance(outcome, AbriRequestRejected):
return outcome
self._hubspot.write_deal_property(
deal_id=booking.deal_id,
property_name=JOB_NO_DEAL_PROPERTY,
value=outcome.job_no,
)
try:
self._hubspot.write_deal_property(
deal_id=booking.deal_id,
property_name=JOB_NO_DEAL_PROPERTY,
value=outcome.job_no,
)
except HubspotRequestError as error:
# The job now exists in OpenHousing; never re-run log_job (it
# would log a duplicate). `error` is already scrubbed, so
# chaining it keeps tracebacks free of the response body.
raise LogJobWriteBackError(
message=str(error),
deal_id=booking.deal_id,
job_no=outcome.job_no,
) from error
self._deal_database.record_job_no(
deal_id=booking.deal_id, job_no=outcome.job_no
)