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:57:16 +00:00
parent b847fa2f4c
commit 350a76f942

View file

@ -1,3 +1,5 @@
import json
import traceback
import xml.etree.ElementTree as ET
from datetime import date
from pathlib import Path
@ -6,6 +8,7 @@ from unittest.mock import MagicMock, patch
import pytest
import requests
from hubspot.crm.deals import ApiException as DealsApiException # type: ignore[reportMissingTypeStubs]
from hubspot.crm.deals import SimplePublicObjectInput # type: ignore[reportMissingTypeStubs]
from domain.abri.models import AbriRequestRejected, PlaceRef
@ -17,6 +20,7 @@ from orchestration.abri_log_job_orchestrator import (
AbriLogJobOrchestrator,
ConfirmedSurveyBooking,
LogJobSummary,
LogJobWriteBackError,
)
FIXTURE_DIR = Path(__file__).parents[1] / "abri"
@ -221,3 +225,70 @@ def test_run_raises_a_transport_error_when_the_relay_is_unreachable(
orchestrator.run(BOOKING)
assert sdk_client.mock_calls == []
assert deal_database.recorded_job_nos == []
# --- partial failure: job logged in OpenHousing, HubSpot write failed ---
def _body_echoing_api_error() -> DealsApiException:
"""A HubSpot validation error whose body echoes the submitted values."""
api_error = DealsApiException(status=400, reason="Bad Request")
api_error.body = json.dumps(
{
"status": "error",
"message": 'Invalid value "AD0226519" for client_booking_reference',
"correlationId": "corr-abc-123",
"category": "VALIDATION_ERROR",
}
)
api_error.headers = {}
return api_error
def test_run_raises_an_error_carrying_the_orphaned_job_no_when_the_hubspot_write_fails(
orchestrator: AbriLogJobOrchestrator,
mock_session: MagicMock,
sdk_client: MagicMock,
deal_database: FakeDealDatabase,
) -> None:
# Arrange
mock_session.post.return_value.content = _load_fixture(
"abri_relay_logjob_success_response.xml"
)
sdk_client.crm.deals.basic_api.update.side_effect = _body_echoing_api_error()
# Act
with pytest.raises(LogJobWriteBackError) as exc_info:
orchestrator.run(BOOKING)
# Assert: the orphaned job_no is reported for manual paste-in, the
# database write is skipped, and log_job is never retried.
error = exc_info.value
assert error.deal_id == DEAL_ID
assert error.job_no == "AD0226519"
assert "AD0226519" in str(error)
assert deal_database.recorded_job_nos == []
assert mock_session.post.call_count == 1
def test_run_raises_a_write_back_error_scrubbed_of_the_hubspot_response_body(
orchestrator: AbriLogJobOrchestrator,
mock_session: MagicMock,
sdk_client: MagicMock,
) -> None:
# Arrange
mock_session.post.return_value.content = _load_fixture(
"abri_relay_logjob_success_response.xml"
)
sdk_client.crm.deals.basic_api.update.side_effect = _body_echoing_api_error()
# Act
with pytest.raises(LogJobWriteBackError) as exc_info:
orchestrator.run(BOOKING)
# Assert: non-PII diagnostics survive; the echoed body does not
rendered_traceback = "".join(traceback.format_exception(exc_info.value))
assert "400" in str(exc_info.value)
assert "VALIDATION_ERROR" in str(exc_info.value)
assert "corr-abc-123" in str(exc_info.value)
assert "Invalid value" not in rendered_traceback