diff --git a/domain/abri/models.py b/domain/abri/models.py index d96d27fda..159d184f2 100644 --- a/domain/abri/models.py +++ b/domain/abri/models.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from datetime import date +from enum import Enum from typing import Literal, NewType, Optional, Tuple, Union PlaceRef = NewType("PlaceRef", str) @@ -7,6 +8,16 @@ PlaceRef = NewType("PlaceRef", str) SlotCode = Literal["AM", "PM", "AD"] +class AbandonReason(str, Enum): + """OpenHousing's coded reason a job was abandoned. + + Only CARDED is used for now; the full HubSpot-outcome-to-code table is a + documented follow-up (needs Abri's code list). + """ + + CARDED = "CARDED" + + @dataclass(frozen=True) class LogJobRequest: client_ref: str @@ -78,3 +89,18 @@ class TenancyData: GetTenantDataResult = Union[TenancyData, AbriRequestRejected] + + +@dataclass(frozen=True) +class AbandonJobRequest: + job_no: str + reason: AbandonReason + date_abandoned: date + + +@dataclass(frozen=True) +class JobAbandoned: + job_no: str + + +AbandonJobResult = Union[JobAbandoned, AbriRequestRejected] diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py index 80f9cde77..3b85903d0 100644 --- a/infrastructure/abri/abri_client.py +++ b/infrastructure/abri/abri_client.py @@ -5,11 +5,14 @@ from typing import Dict, List, Tuple, Union import requests from domain.abri.models import ( + AbandonJobRequest, + AbandonJobResult, AbriRequestRejected, AmendJobRequest, AmendJobResult, AppointmentAmended, GetTenantDataResult, + JobAbandoned, JobLogged, LogJobRequest, LogJobResult, @@ -94,6 +97,9 @@ class AbriClient: return self._parse_appointment_amended(outcome) + def abandon_job(self, request: AbandonJobRequest) -> AbandonJobResult: + raise NotImplementedError + def get_tenant_data(self, place_ref: PlaceRef) -> GetTenantDataResult: outcome = self._exchange( request_type="getSCSTenantData", diff --git a/tests/abri/abri_relay_canceljob_request_example.xml b/tests/abri/abri_relay_canceljob_request_example.xml new file mode 100644 index 000000000..cab9323bf --- /dev/null +++ b/tests/abri/abri_relay_canceljob_request_example.xml @@ -0,0 +1,13 @@ + + +
+ +
+ + + + + + + +
diff --git a/tests/abri/abri_relay_canceljob_success_response.xml b/tests/abri/abri_relay_canceljob_success_response.xml new file mode 100644 index 000000000..5d841e494 --- /dev/null +++ b/tests/abri/abri_relay_canceljob_success_response.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/infrastructure/abri/test_abri_client.py b/tests/infrastructure/abri/test_abri_client.py index 6ab6c7698..5def9f129 100644 --- a/tests/infrastructure/abri/test_abri_client.py +++ b/tests/infrastructure/abri/test_abri_client.py @@ -7,9 +7,12 @@ import pytest import requests from domain.abri.models import ( + AbandonJobRequest, + AbandonReason, AbriRequestRejected, AmendJobRequest, AppointmentAmended, + JobAbandoned, JobLogged, LogJobRequest, PlaceRef, @@ -60,6 +63,14 @@ def _spec_example_amend_request() -> AmendJobRequest: ) +def _spec_example_abandon_request() -> AbandonJobRequest: + return AbandonJobRequest( + job_no="AC0439951", + reason=AbandonReason.CARDED, + date_abandoned=date(2026, 7, 7), + ) + + @pytest.fixture() def mock_session() -> MagicMock: return MagicMock() @@ -348,3 +359,42 @@ def test_amend_job_raises_transport_error_when_the_relay_is_unreachable( # Act / Assert with pytest.raises(AbriTransportError): client.amend_job(_spec_example_amend_request()) + + +# --- abandon_job: success --- + + +def test_abandon_job_returns_job_abandoned_with_the_cancelled_job_number( + client: AbriClient, mock_session: MagicMock +) -> None: + # Arrange + mock_session.post.return_value.content = _load_fixture( + "abri_relay_canceljob_success_response.xml" + ) + + # Act + result = client.abandon_job(_spec_example_abandon_request()) + + # Assert + assert result == JobAbandoned(job_no="AC0439951") + + +def test_abandon_job_sends_the_recorded_canceljob_envelope_to_the_relay_endpoint( + client: AbriClient, mock_session: MagicMock +) -> None: + # Arrange + mock_session.post.return_value.content = _load_fixture( + "abri_relay_canceljob_success_response.xml" + ) + + # Act + client.abandon_job(_spec_example_abandon_request()) + + # Assert + (url,) = mock_session.post.call_args.args + sent_body: bytes = mock_session.post.call_args.kwargs["data"] + expected_body = _load_fixture("abri_relay_canceljob_request_example.xml") + assert url == ENDPOINT_URL + assert ET.canonicalize(xml_data=sent_body, strip_text=True) == ET.canonicalize( + xml_data=expected_body, strip_text=True + )