From 2cfed0c33dca788fc6bb9fa55fa1aed86fe7d637 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:15:44 +0000 Subject: [PATCH 01/12] =?UTF-8?q?Abri=20amend=5Fjob=20amends=20an=20OpenHo?= =?UTF-8?q?using=20appointment=20and=20returns=20the=20echoed=20booking=20?= =?UTF-8?q?=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- domain/abri/models.py | 20 +++++++++++- infrastructure/abri/abri_client.py | 5 +++ ...i_relay_amendoptiappt_success_response.xml | 3 ++ tests/infrastructure/abri/test_abri_client.py | 32 +++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/abri/abri_relay_amendoptiappt_success_response.xml diff --git a/domain/abri/models.py b/domain/abri/models.py index e160c3e3b..0444ec1cc 100644 --- a/domain/abri/models.py +++ b/domain/abri/models.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from datetime import date -from typing import Literal, NewType, Union +from typing import Literal, NewType, Optional, Union PlaceRef = NewType("PlaceRef", str) @@ -30,3 +30,21 @@ class AbriRequestRejected: LogJobResult = Union[JobLogged, AbriRequestRejected] + + +@dataclass(frozen=True) +class AmendJobRequest: + job_no: str + appointment_date: date + appointment_time: SlotCode + resource: Optional[str] = None + + +@dataclass(frozen=True) +class AppointmentAmended: + job_no: str + appointment_date: str + appointment_time: str + + +AmendJobResult = Union[AppointmentAmended, AbriRequestRejected] diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py index 72a3c4767..4342ba946 100644 --- a/infrastructure/abri/abri_client.py +++ b/infrastructure/abri/abri_client.py @@ -5,6 +5,8 @@ import requests from domain.abri.models import ( AbriRequestRejected, + AmendJobRequest, + AmendJobResult, JobLogged, LogJobRequest, LogJobResult, @@ -45,6 +47,9 @@ class AbriClient: return self._parse_job_logged(outcome) + def amend_job(self, request: AmendJobRequest) -> AmendJobResult: + raise NotImplementedError + def _exchange( self, request_type: str, parameters: List[Tuple[str, str]] ) -> Union[ET.Element, AbriRequestRejected]: diff --git a/tests/abri/abri_relay_amendoptiappt_success_response.xml b/tests/abri/abri_relay_amendoptiappt_success_response.xml new file mode 100644 index 000000000..def7734c3 --- /dev/null +++ b/tests/abri/abri_relay_amendoptiappt_success_response.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/infrastructure/abri/test_abri_client.py b/tests/infrastructure/abri/test_abri_client.py index 133c74d8f..7c205ab7d 100644 --- a/tests/infrastructure/abri/test_abri_client.py +++ b/tests/infrastructure/abri/test_abri_client.py @@ -8,6 +8,8 @@ import requests from domain.abri.models import ( AbriRequestRejected, + AmendJobRequest, + AppointmentAmended, JobLogged, LogJobRequest, PlaceRef, @@ -50,6 +52,14 @@ def _spec_example_request() -> LogJobRequest: ) +def _spec_example_amend_request() -> AmendJobRequest: + return AmendJobRequest( + job_no="AC0439951", + appointment_date=date(2026, 6, 19), + appointment_time="PM", + ) + + @pytest.fixture() def mock_session() -> MagicMock: return MagicMock() @@ -192,3 +202,25 @@ def test_log_job_raises_retriable_parse_error_on_an_unexpectedly_shaped_response # Act / Assert with pytest.raises(AbriResponseParseError): client.log_job(_spec_example_request()) + + +# --- amend_job: success --- + + +def test_amend_job_returns_appointment_amended_echoing_the_response_attributes( + client: AbriClient, mock_session: MagicMock +) -> None: + # Arrange + mock_session.post.return_value.content = _load_fixture( + "abri_relay_amendoptiappt_success_response.xml" + ) + + # Act + result = client.amend_job(_spec_example_amend_request()) + + # Assert + assert result == AppointmentAmended( + job_no="AC0439951", + appointment_date="24/06/2026", + appointment_time="PM", + ) From c542a57ae5e8c5f14c5ac517b4fc189d4f61ac8b Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:17:41 +0000 Subject: [PATCH 02/12] =?UTF-8?q?Abri=20amend=5Fjob=20amends=20an=20OpenHo?= =?UTF-8?q?using=20appointment=20and=20returns=20the=20echoed=20booking=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- infrastructure/abri/abri_client.py | 41 +++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py index 4342ba946..f9b1ddc1f 100644 --- a/infrastructure/abri/abri_client.py +++ b/infrastructure/abri/abri_client.py @@ -7,6 +7,7 @@ from domain.abri.models import ( AbriRequestRejected, AmendJobRequest, AmendJobResult, + AppointmentAmended, JobLogged, LogJobRequest, LogJobResult, @@ -48,7 +49,20 @@ class AbriClient: return self._parse_job_logged(outcome) def amend_job(self, request: AmendJobRequest) -> AmendJobResult: - raise NotImplementedError + outcome = self._exchange( + request_type="amendoptiappt", + parameters=[ + ("job_no", request.job_no), + ("action", "amend"), + ("appointment_date", request.appointment_date.strftime("%d/%m/%Y")), + ("appointment_time", request.appointment_time), + ], + ) + + if isinstance(outcome, AbriRequestRejected): + return outcome + + return self._parse_appointment_amended(outcome) def _exchange( self, request_type: str, parameters: List[Tuple[str, str]] @@ -106,6 +120,31 @@ class AbriClient: return JobLogged(job_no=job_no, logged_info=logged_info) + @staticmethod + def _parse_appointment_amended(root: ET.Element) -> AppointmentAmended: + amended = root.find("AmendOptiAppt") + + if amended is None: + raise AbriResponseParseError( + "AmendOptiAppt element missing from relay response" + ) + + job_no = amended.get("job_no") + appointment_date = amended.get("appointment_date") + appointment_time = amended.get("appointment_time") + + if job_no is None or appointment_date is None or appointment_time is None: + raise AbriResponseParseError( + "AmendOptiAppt element missing job_no, appointment_date " + "or appointment_time" + ) + + return AppointmentAmended( + job_no=job_no, + appointment_date=appointment_date, + appointment_time=appointment_time, + ) + @staticmethod def _parse_rejection(root: ET.Element) -> AbriRequestRejected: success = root.findtext("success") From fbb4345116758af03358f9e5f90b1d295f8bc24e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:18:24 +0000 Subject: [PATCH 03/12] =?UTF-8?q?Abri=20amend=5Fjob=20serialises=20the=20s?= =?UTF-8?q?pec's=20amendoptiappt=20envelope=20with=20the=20surveyor=20reso?= =?UTF-8?q?urce=20last=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- ...ri_relay_amendoptiappt_request_example.xml | 15 ++++++++++ tests/infrastructure/abri/test_abri_client.py | 28 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/abri/abri_relay_amendoptiappt_request_example.xml diff --git a/tests/abri/abri_relay_amendoptiappt_request_example.xml b/tests/abri/abri_relay_amendoptiappt_request_example.xml new file mode 100644 index 000000000..10c9b8941 --- /dev/null +++ b/tests/abri/abri_relay_amendoptiappt_request_example.xml @@ -0,0 +1,15 @@ + + +
+ +
+ + + + + + + + + +
diff --git a/tests/infrastructure/abri/test_abri_client.py b/tests/infrastructure/abri/test_abri_client.py index 7c205ab7d..87e13b9d1 100644 --- a/tests/infrastructure/abri/test_abri_client.py +++ b/tests/infrastructure/abri/test_abri_client.py @@ -224,3 +224,31 @@ def test_amend_job_returns_appointment_amended_echoing_the_response_attributes( appointment_date="24/06/2026", appointment_time="PM", ) + + +def test_amend_job_sends_the_recorded_amendoptiappt_envelope_to_the_relay_endpoint( + client: AbriClient, mock_session: MagicMock +) -> None: + # Arrange + mock_session.post.return_value.content = _load_fixture( + "abri_relay_amendoptiappt_success_response.xml" + ) + + # Act + client.amend_job( + AmendJobRequest( + job_no="AC0439951", + appointment_date=date(2026, 6, 19), + appointment_time="PM", + resource="NAULKH", + ) + ) + + # Assert + (url,) = mock_session.post.call_args.args + sent_body: bytes = mock_session.post.call_args.kwargs["data"] + expected_body = _load_fixture("abri_relay_amendoptiappt_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 + ) From 7bdba469fbb1b8f6339ae9077b57fc684a95d86c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:19:16 +0000 Subject: [PATCH 04/12] =?UTF-8?q?Abri=20amend=5Fjob=20serialises=20the=20s?= =?UTF-8?q?pec's=20amendoptiappt=20envelope=20with=20the=20surveyor=20reso?= =?UTF-8?q?urce=20last=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- infrastructure/abri/abri_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py index f9b1ddc1f..3f9cc19b0 100644 --- a/infrastructure/abri/abri_client.py +++ b/infrastructure/abri/abri_client.py @@ -56,6 +56,7 @@ class AbriClient: ("action", "amend"), ("appointment_date", request.appointment_date.strftime("%d/%m/%Y")), ("appointment_time", request.appointment_time), + ("resource", request.resource or ""), ], ) From b70afc8d9dffcf000f869815b7bbc03d303721c3 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:20:41 +0000 Subject: [PATCH 05/12] =?UTF-8?q?Abri=20amend=5Fjob=20omits=20the=20survey?= =?UTF-8?q?or=20resource=20so=20amendments=20cannot=20clobber=20Abri-side?= =?UTF-8?q?=20reassignments=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- tests/infrastructure/abri/test_abri_client.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/infrastructure/abri/test_abri_client.py b/tests/infrastructure/abri/test_abri_client.py index 87e13b9d1..ed6b4cecf 100644 --- a/tests/infrastructure/abri/test_abri_client.py +++ b/tests/infrastructure/abri/test_abri_client.py @@ -252,3 +252,23 @@ def test_amend_job_sends_the_recorded_amendoptiappt_envelope_to_the_relay_endpoi assert ET.canonicalize(xml_data=sent_body, strip_text=True) == ET.canonicalize( xml_data=expected_body, strip_text=True ) + + +def test_amend_job_omits_the_resource_parameter_when_no_surveyor_is_provided( + client: AbriClient, mock_session: MagicMock +) -> None: + # Arrange + mock_session.post.return_value.content = _load_fixture( + "abri_relay_amendoptiappt_success_response.xml" + ) + + # Act + client.amend_job(_spec_example_amend_request()) + + # Assert + sent_body: bytes = mock_session.post.call_args.kwargs["data"] + sent_attributes = [ + parameter.get("attribute") + for parameter in ET.fromstring(sent_body).findall(".//Parameters") + ] + assert "resource" not in sent_attributes From e363a1105ac75d9bf72d02ef2ed79b4e6eb22e6d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:21:06 +0000 Subject: [PATCH 06/12] =?UTF-8?q?Abri=20amend=5Fjob=20omits=20the=20survey?= =?UTF-8?q?or=20resource=20so=20amendments=20cannot=20clobber=20Abri-side?= =?UTF-8?q?=20reassignments=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- infrastructure/abri/abri_client.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py index 3f9cc19b0..4cfe51d7e 100644 --- a/infrastructure/abri/abri_client.py +++ b/infrastructure/abri/abri_client.py @@ -49,15 +49,18 @@ class AbriClient: return self._parse_job_logged(outcome) def amend_job(self, request: AmendJobRequest) -> AmendJobResult: + parameters = [ + ("job_no", request.job_no), + ("action", "amend"), + ("appointment_date", request.appointment_date.strftime("%d/%m/%Y")), + ("appointment_time", request.appointment_time), + ] + if request.resource is not None: + parameters.append(("resource", request.resource)) + outcome = self._exchange( request_type="amendoptiappt", - parameters=[ - ("job_no", request.job_no), - ("action", "amend"), - ("appointment_date", request.appointment_date.strftime("%d/%m/%Y")), - ("appointment_time", request.appointment_time), - ("resource", request.resource or ""), - ], + parameters=parameters, ) if isinstance(outcome, AbriRequestRejected): From e9cb46a58b83ae21f44265db6e9abea2151ab662 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:22:11 +0000 Subject: [PATCH 07/12] =?UTF-8?q?Abri=20amend=5Fjob=20surfaces=20OpenHousi?= =?UTF-8?q?ng=20rejections=20with=20code=20and=20message=20verbatim=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- ...elay_amendoptiappt_relayerror_response.xml | 6 ++++++ tests/infrastructure/abri/test_abri_client.py | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/abri/abri_relay_amendoptiappt_relayerror_response.xml diff --git a/tests/abri/abri_relay_amendoptiappt_relayerror_response.xml b/tests/abri/abri_relay_amendoptiappt_relayerror_response.xml new file mode 100644 index 000000000..225563172 --- /dev/null +++ b/tests/abri/abri_relay_amendoptiappt_relayerror_response.xml @@ -0,0 +1,6 @@ + + + false + RelayError + Job_no 1019905 is invalid. Job not found. + diff --git a/tests/infrastructure/abri/test_abri_client.py b/tests/infrastructure/abri/test_abri_client.py index ed6b4cecf..ab8f3d87c 100644 --- a/tests/infrastructure/abri/test_abri_client.py +++ b/tests/infrastructure/abri/test_abri_client.py @@ -272,3 +272,24 @@ def test_amend_job_omits_the_resource_parameter_when_no_surveyor_is_provided( for parameter in ET.fromstring(sent_body).findall(".//Parameters") ] assert "resource" not in sent_attributes + + +# --- amend_job: rejection --- + + +def test_amend_job_returns_rejection_with_openhousing_error_code_and_message_verbatim( + client: AbriClient, mock_session: MagicMock +) -> None: + # Arrange + mock_session.post.return_value.content = _load_fixture( + "abri_relay_amendoptiappt_relayerror_response.xml" + ) + + # Act + result = client.amend_job(_spec_example_amend_request()) + + # Assert + assert result == AbriRequestRejected( + code="RelayError", + message="Job_no 1019905 is invalid. Job not found.", + ) From 65ec38012b86816049c55dd3b506a96be22fca76 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:22:49 +0000 Subject: [PATCH 08/12] =?UTF-8?q?Abri=20amend=5Fjob=20treats=20an=20unconf?= =?UTF-8?q?irmed=20amendment=20response=20as=20retriable=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- tests/infrastructure/abri/test_abri_client.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/infrastructure/abri/test_abri_client.py b/tests/infrastructure/abri/test_abri_client.py index ab8f3d87c..55aa5deb5 100644 --- a/tests/infrastructure/abri/test_abri_client.py +++ b/tests/infrastructure/abri/test_abri_client.py @@ -293,3 +293,30 @@ def test_amend_job_returns_rejection_with_openhousing_error_code_and_message_ver code="RelayError", message="Job_no 1019905 is invalid. Job not found.", ) + + +# --- amend_job: ambiguous responses are retriable, never success or rejection --- + + +@pytest.mark.parametrize( + "body", + [ + b'', + b'', + b'', + b"", + b"", + ], +) +def test_amend_job_raises_retriable_parse_error_when_the_amendment_is_unconfirmed( + client: AbriClient, mock_session: MagicMock, body: bytes +) -> None: + # Arrange + mock_session.post.return_value.content = body + + # Act / Assert + with pytest.raises(AbriResponseParseError): + client.amend_job(_spec_example_amend_request()) From 096e4ae1f741bf1b41e53a035b5dd8fee6ceba14 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:23:10 +0000 Subject: [PATCH 09/12] =?UTF-8?q?Abri=20amend=5Fjob=20treats=20an=20unconf?= =?UTF-8?q?irmed=20amendment=20response=20as=20retriable=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- infrastructure/abri/abri_client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py index 4cfe51d7e..8ebd3ac2e 100644 --- a/infrastructure/abri/abri_client.py +++ b/infrastructure/abri/abri_client.py @@ -133,6 +133,11 @@ class AbriClient: "AmendOptiAppt element missing from relay response" ) + if amended.get("success") != "Y": + raise AbriResponseParseError( + "AmendOptiAppt element did not confirm success" + ) + job_no = amended.get("job_no") appointment_date = amended.get("appointment_date") appointment_time = amended.get("appointment_time") From 05bd22269da29750ee59b57e3ff46ed1257d500e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:23:53 +0000 Subject: [PATCH 10/12] =?UTF-8?q?Abri=20amend=5Fjob=20raises=20retriable?= =?UTF-8?q?=20transport=20errors=20when=20the=20amendment=20outcome=20is?= =?UTF-8?q?=20unknown=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- tests/infrastructure/abri/test_abri_client.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/infrastructure/abri/test_abri_client.py b/tests/infrastructure/abri/test_abri_client.py index 55aa5deb5..6ab6c7698 100644 --- a/tests/infrastructure/abri/test_abri_client.py +++ b/tests/infrastructure/abri/test_abri_client.py @@ -320,3 +320,31 @@ def test_amend_job_raises_retriable_parse_error_when_the_amendment_is_unconfirme # Act / Assert with pytest.raises(AbriResponseParseError): client.amend_job(_spec_example_amend_request()) + + +# --- amend_job: transport failures --- + + +def test_amend_job_raises_transport_error_on_http_error_status( + client: AbriClient, mock_session: MagicMock +) -> None: + # Arrange + mock_session.post.return_value.content = b"Server Error" + mock_session.post.return_value.raise_for_status.side_effect = requests.HTTPError( + "500 Server Error" + ) + + # Act / Assert + with pytest.raises(AbriTransportError): + client.amend_job(_spec_example_amend_request()) + + +def test_amend_job_raises_transport_error_when_the_relay_is_unreachable( + client: AbriClient, mock_session: MagicMock +) -> None: + # Arrange + mock_session.post.side_effect = requests.ConnectionError("connection refused") + + # Act / Assert + with pytest.raises(AbriTransportError): + client.amend_job(_spec_example_amend_request()) From b5f7cc35489eb7f9d6f3e9ae953855211de84603 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:24:48 +0000 Subject: [PATCH 11/12] =?UTF-8?q?Abri=20job=20logging=20and=20appointment?= =?UTF-8?q?=20amendment=20serialise=20bookings=20through=20one=20date=20ru?= =?UTF-8?q?le=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- infrastructure/abri/abri_client.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py index 8ebd3ac2e..3c95c2766 100644 --- a/infrastructure/abri/abri_client.py +++ b/infrastructure/abri/abri_client.py @@ -1,4 +1,5 @@ import xml.etree.ElementTree as ET +from datetime import date from typing import List, Tuple, Union import requests @@ -21,6 +22,10 @@ CLIENT_CODE = "HSG" RESOURCE_GROUP = "Surveyors" +def _format_appointment_date(appointment_date: date) -> str: + return appointment_date.strftime("%d/%m/%Y") + + class AbriClient: def __init__(self, config: AbriConfig) -> None: self._config = config @@ -36,7 +41,7 @@ class AbriClient: ("short_description", request.short_description), ("long_description", request.long_description), ("client_ref", request.client_ref), - ("appointment_date", request.appointment_date.strftime("%d/%m/%Y")), + ("appointment_date", _format_appointment_date(request.appointment_date)), ("appointment_time", request.appointment_time), ("resource", self._config.default_resource), ("resource_group", RESOURCE_GROUP), @@ -52,7 +57,7 @@ class AbriClient: parameters = [ ("job_no", request.job_no), ("action", "amend"), - ("appointment_date", request.appointment_date.strftime("%d/%m/%Y")), + ("appointment_date", _format_appointment_date(request.appointment_date)), ("appointment_time", request.appointment_time), ] if request.resource is not None: From 021e7c15bb0157729dbacc576841e3be81d6164f Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 3 Jul 2026 16:26:15 +0000 Subject: [PATCH 12/12] =?UTF-8?q?Abri=20job=20amendment=20trigger=20is=20n?= =?UTF-8?q?amed=20after=20the=20OpenHousing=20job=20it=20amends=20?= =?UTF-8?q?=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- etl/hubspot/hubspot_deal_differ.py | 6 ++++- etl/hubspot/tests/test_hubspot_deal_differ.py | 26 +++++++++---------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index b693cf7a4..977dc3e4d 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -197,7 +197,7 @@ class HubspotDealDiffer: return parse_hs_date(new_deal.get("confirmed_survey_date")) is not None @staticmethod - def check_for_abri_survey_amendment( + def check_for_abri_job_amendment( new_deal: Dict[str, str], new_project: Optional[ProjectData], old_deal: HubspotDealData, @@ -209,6 +209,10 @@ class HubspotDealDiffer: if old_deal.confirmed_survey_date is None: return False + # TODO: a cleared survey date (was set, now empty) still fires this + # check, but the amendoptiappt route cannot express a cancellation. + # Cancellation semantics (the "delete" action / canceljob route) need + # a deliberate decision before this trigger is wired to a consumer. new_survey_date = parse_hs_date(new_deal.get("confirmed_survey_date")) if old_deal.confirmed_survey_date != new_survey_date: return True diff --git a/etl/hubspot/tests/test_hubspot_deal_differ.py b/etl/hubspot/tests/test_hubspot_deal_differ.py index 08e6c78a1..c3e24e08f 100644 --- a/etl/hubspot/tests/test_hubspot_deal_differ.py +++ b/etl/hubspot/tests/test_hubspot_deal_differ.py @@ -548,12 +548,12 @@ def test_abri_job_logging__non_abri_project_id_with_abri_code__returns_false() - assert result is False -# =================================== -# ABRI SURVEY AMENDMENT TRIGGER TESTS -# =================================== +# ================================ +# ABRI JOB AMENDMENT TRIGGER TESTS +# ================================ -def test_abri_survey_amendment__date_changed_on_abri_deal__returns_true() -> None: +def test_abri_job_amendment__date_changed_on_abri_deal__returns_true() -> None: deal_id = uuid.uuid4() # Arrange @@ -569,7 +569,7 @@ def test_abri_survey_amendment__date_changed_on_abri_deal__returns_true() -> Non ) # Act - result = HubspotDealDiffer.check_for_abri_survey_amendment( + result = HubspotDealDiffer.check_for_abri_job_amendment( new_deal=new_deal, new_project=None, old_deal=old_deal, @@ -579,7 +579,7 @@ def test_abri_survey_amendment__date_changed_on_abri_deal__returns_true() -> Non assert result is True -def test_abri_survey_amendment__non_abri_deal__returns_false() -> None: +def test_abri_job_amendment__non_abri_deal__returns_false() -> None: deal_id = uuid.uuid4() # Arrange @@ -595,7 +595,7 @@ def test_abri_survey_amendment__non_abri_deal__returns_false() -> None: ) # Act - result = HubspotDealDiffer.check_for_abri_survey_amendment( + result = HubspotDealDiffer.check_for_abri_job_amendment( new_deal=new_deal, new_project=None, old_deal=old_deal, @@ -605,7 +605,7 @@ def test_abri_survey_amendment__non_abri_deal__returns_false() -> None: assert result is False -def test_abri_survey_amendment__date_and_time_unchanged__returns_false() -> None: +def test_abri_job_amendment__date_and_time_unchanged__returns_false() -> None: deal_id = uuid.uuid4() # Arrange @@ -623,7 +623,7 @@ def test_abri_survey_amendment__date_and_time_unchanged__returns_false() -> None ) # Act - result = HubspotDealDiffer.check_for_abri_survey_amendment( + result = HubspotDealDiffer.check_for_abri_job_amendment( new_deal=new_deal, new_project=None, old_deal=old_deal, @@ -633,7 +633,7 @@ def test_abri_survey_amendment__date_and_time_unchanged__returns_false() -> None assert result is False -def test_abri_survey_amendment__time_changed_on_abri_deal__returns_true() -> None: +def test_abri_job_amendment__time_changed_on_abri_deal__returns_true() -> None: deal_id = uuid.uuid4() # Arrange @@ -651,7 +651,7 @@ def test_abri_survey_amendment__time_changed_on_abri_deal__returns_true() -> Non ) # Act - result = HubspotDealDiffer.check_for_abri_survey_amendment( + result = HubspotDealDiffer.check_for_abri_job_amendment( new_deal=new_deal, new_project=None, old_deal=old_deal, @@ -661,7 +661,7 @@ def test_abri_survey_amendment__time_changed_on_abri_deal__returns_true() -> Non assert result is True -def test_abri_survey_amendment__date_first_set_on_abri_deal__returns_false() -> None: +def test_abri_job_amendment__date_first_set_on_abri_deal__returns_false() -> None: deal_id = uuid.uuid4() # Arrange @@ -677,7 +677,7 @@ def test_abri_survey_amendment__date_first_set_on_abri_deal__returns_false() -> ) # Act - result = HubspotDealDiffer.check_for_abri_survey_amendment( + result = HubspotDealDiffer.check_for_abri_job_amendment( new_deal=new_deal, new_project=None, old_deal=old_deal,