mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Send the deal's third-party surveyor as the OpenHousing resource
Carry third_party_surveyor_identifier on the Abri trigger message and through dispatch into the log and amend flows, booking the job against that resource. LogJobRequest gains the optional resource field AmendJobRequest already had; the client falls back to the configured default surveyor when the deal names no third-party resource, so existing deals behave unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
80113205c8
commit
c8fe4585bf
12 changed files with 185 additions and 1 deletions
|
|
@ -37,6 +37,9 @@ class AbriTriggerRequest(BaseModel):
|
|||
confirmed_survey_time: Optional[str] = None
|
||||
last_submission_date: Optional[date] = None
|
||||
outcome: Optional[str] = None
|
||||
# OpenHousing's bookable resource for the survey; optional because the
|
||||
# Abri client falls back to the configured default surveyor when absent.
|
||||
third_party_surveyor_identifier: Optional[str] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _each_fired_flow_has_its_fields(self) -> "AbriTriggerRequest":
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ def _run_amend(request: AbriTriggerRequest, flows: AbriFlows) -> str:
|
|||
deal_id=request.hubspot_deal_id,
|
||||
confirmed_survey_date=request.confirmed_survey_date,
|
||||
confirmed_survey_time=request.confirmed_survey_time,
|
||||
third_party_surveyor_identifier=request.third_party_surveyor_identifier,
|
||||
)
|
||||
)
|
||||
if isinstance(outcome, AbriRequestRejected):
|
||||
|
|
@ -121,6 +122,9 @@ def _run_log(
|
|||
deal_name=request.deal_name,
|
||||
confirmed_survey_date=request.confirmed_survey_date,
|
||||
confirmed_survey_time=request.confirmed_survey_time,
|
||||
third_party_surveyor_identifier=(
|
||||
request.third_party_surveyor_identifier
|
||||
),
|
||||
)
|
||||
)
|
||||
except LogJobWriteBackError as error:
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class LogJobRequest:
|
|||
appointment_time: SlotCode
|
||||
short_description: str
|
||||
long_description: str
|
||||
resource: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
|||
|
|
@ -267,6 +267,12 @@ class HubspotDealDiffer:
|
|||
else None
|
||||
),
|
||||
"confirmed_survey_time": new_deal.get("confirmed_survey_time"),
|
||||
# Carried for the log/amend flows: sent to OpenHousing as the
|
||||
# bookable resource, falling back to the configured default
|
||||
# surveyor when absent.
|
||||
"third_party_surveyor_identifier": new_deal.get(
|
||||
"third_party_surveyor_identifier"
|
||||
),
|
||||
# Carried for the abandon flow: it dates the cancel to the confirmed
|
||||
# survey date, falling back to the last submission date.
|
||||
"last_submission_date": (
|
||||
|
|
|
|||
|
|
@ -66,9 +66,33 @@ def test_a_first_confirmed_survey_date_builds_a_log_job_message() -> None:
|
|||
"confirmed_survey_time": "14:30",
|
||||
"last_submission_date": None,
|
||||
"outcome": None,
|
||||
"third_party_surveyor_identifier": None,
|
||||
}
|
||||
|
||||
|
||||
def test_the_message_carries_the_deals_third_party_surveyor_identifier() -> None:
|
||||
# Arrange
|
||||
old_deal = make_old_deal(project_code=ABRI_PROJECT_CODE, confirmed_survey_date=None)
|
||||
new_deal = make_new_deal(
|
||||
confirmed_survey_date="2026-06-24",
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="THIRDPARTY",
|
||||
)
|
||||
|
||||
# Act
|
||||
message = HubspotDealDiffer.check_abri_triggers_and_construct_message(
|
||||
hubspot_deal_id=DEAL_ID,
|
||||
new_deal=new_deal,
|
||||
new_project=None,
|
||||
new_listing=LISTING,
|
||||
old_deal=old_deal,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert message is not None
|
||||
assert message["third_party_surveyor_identifier"] == "THIRDPARTY"
|
||||
|
||||
|
||||
def test_a_changed_confirmed_survey_date_builds_an_amend_job_message() -> None:
|
||||
# Arrange
|
||||
old_deal = make_old_deal(
|
||||
|
|
|
|||
|
|
@ -56,6 +56,14 @@ class AbriClient:
|
|||
self._session = requests.Session()
|
||||
|
||||
def log_job(self, request: LogJobRequest) -> LogJobResult:
|
||||
# OpenHousing needs a bookable resource, so fall back to the
|
||||
# configured surveyor when the request carries no explicit override
|
||||
# (mirrors amend_job).
|
||||
resource = (
|
||||
request.resource
|
||||
if request.resource is not None
|
||||
else self._config.default_resource
|
||||
)
|
||||
outcome = self._exchange(
|
||||
request_type="logjob",
|
||||
parameters=[
|
||||
|
|
@ -67,7 +75,7 @@ class AbriClient:
|
|||
("client_ref", request.client_ref),
|
||||
("appointment_date", _format_appointment_date(request.appointment_date)),
|
||||
("appointment_time", request.appointment_time),
|
||||
("resource", self._config.default_resource),
|
||||
("resource", resource),
|
||||
("resource_group", RESOURCE_GROUP),
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -112,6 +112,9 @@ class ConfirmedSurveyBooking:
|
|||
deal_name: str
|
||||
confirmed_survey_date: date
|
||||
confirmed_survey_time: Optional[str]
|
||||
# OpenHousing calls it the resource; the HubSpot deal property (and the
|
||||
# database column) call it the third-party surveyor identifier.
|
||||
third_party_surveyor_identifier: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -138,6 +141,9 @@ class AppointmentChange:
|
|||
deal_id: str
|
||||
confirmed_survey_date: date
|
||||
confirmed_survey_time: Optional[str]
|
||||
# OpenHousing calls it the resource; the HubSpot deal property (and the
|
||||
# database column) call it the third-party surveyor identifier.
|
||||
third_party_surveyor_identifier: Optional[str] = None
|
||||
|
||||
|
||||
AmendJobOrchestrationResult = Union[AppointmentAmended, AbriRequestRejected]
|
||||
|
|
@ -283,6 +289,7 @@ class AbriOrchestrator:
|
|||
appointment_time=slot_for_confirmed_time(
|
||||
change.confirmed_survey_time
|
||||
),
|
||||
resource=change.third_party_surveyor_identifier,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -319,6 +326,7 @@ class AbriOrchestrator:
|
|||
),
|
||||
short_description=descriptions.short_description,
|
||||
long_description=descriptions.long_description,
|
||||
resource=booking.third_party_surveyor_identifier,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,27 @@ def test_a_message_naming_abandon_job_validates_with_only_the_deal_id() -> None:
|
|||
assert request.flows == ["abandon_job"]
|
||||
|
||||
|
||||
def test_a_message_carries_the_third_party_surveyor_identifier() -> None:
|
||||
# Arrange
|
||||
message = _full_message()
|
||||
message["third_party_surveyor_identifier"] = "THIRDPARTY"
|
||||
|
||||
# Act
|
||||
request = AbriTriggerRequest.model_validate(message)
|
||||
|
||||
# Assert
|
||||
assert request.third_party_surveyor_identifier == "THIRDPARTY"
|
||||
|
||||
|
||||
def test_a_message_without_a_surveyor_identifier_parses_with_none() -> None:
|
||||
# Arrange: optional because the Abri client falls back to the configured
|
||||
# default surveyor when the deal names no third-party resource.
|
||||
request = AbriTriggerRequest.model_validate(_full_message())
|
||||
|
||||
# Assert
|
||||
assert request.third_party_surveyor_identifier is None
|
||||
|
||||
|
||||
def test_a_message_carries_the_outcome_for_the_abandonment_reason_mapping() -> None:
|
||||
# Arrange
|
||||
message = _full_message()
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ def _request(flows: List[str]) -> AbriTriggerRequest:
|
|||
"confirmed_survey_time": "14:30",
|
||||
"last_submission_date": "2026-07-01",
|
||||
"outcome": "no answer",
|
||||
"third_party_surveyor_identifier": "THIRDPARTY",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -171,6 +172,7 @@ def test_flows_receive_the_deal_fields_carried_by_the_message(
|
|||
deal_id=DEAL_ID,
|
||||
confirmed_survey_date=date(2026, 6, 24),
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="THIRDPARTY",
|
||||
),
|
||||
)
|
||||
assert log_call == (
|
||||
|
|
@ -181,6 +183,7 @@ def test_flows_receive_the_deal_fields_carried_by_the_message(
|
|||
deal_name="49 Admers Crescent",
|
||||
confirmed_survey_date=date(2026, 6, 24),
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="THIRDPARTY",
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,61 @@ def test_log_job_sends_the_recorded_logjob_envelope_to_the_relay_endpoint(
|
|||
)
|
||||
|
||||
|
||||
def test_log_job_sends_the_requested_resource_instead_of_the_configured_default(
|
||||
client: AbriClient, mock_session: MagicMock
|
||||
) -> None:
|
||||
# Arrange: a deal carrying a third-party surveyor identifier books the
|
||||
# job against that resource, not the configured default surveyor.
|
||||
mock_session.post.return_value.content = _load_fixture(
|
||||
"abri_relay_logjob_success_response.xml"
|
||||
)
|
||||
|
||||
# Act
|
||||
client.log_job(
|
||||
LogJobRequest(
|
||||
client_ref="DOM51111",
|
||||
place_ref=PlaceRef("1007165"),
|
||||
appointment_date=date(2026, 6, 18),
|
||||
appointment_time="PM",
|
||||
short_description="Christian's SCS External Test.",
|
||||
long_description="Christian's SCS External Test.",
|
||||
resource="THIRDPARTY",
|
||||
)
|
||||
)
|
||||
|
||||
# Assert
|
||||
sent_body: bytes = mock_session.post.call_args.kwargs["data"]
|
||||
resources = [
|
||||
parameter.get("attribute_value")
|
||||
for parameter in ET.fromstring(sent_body).findall(".//Parameters")
|
||||
if parameter.get("attribute") == "resource"
|
||||
]
|
||||
assert resources == ["THIRDPARTY"]
|
||||
|
||||
|
||||
def test_log_job_defaults_the_resource_to_the_configured_surveyor(
|
||||
client: AbriClient, mock_session: MagicMock
|
||||
) -> None:
|
||||
# Arrange: OpenHousing needs a bookable resource, so a log without an
|
||||
# explicit surveyor falls back to the configured default rather than
|
||||
# omitting the parameter.
|
||||
mock_session.post.return_value.content = _load_fixture(
|
||||
"abri_relay_logjob_success_response.xml"
|
||||
)
|
||||
|
||||
# Act
|
||||
client.log_job(_spec_example_request())
|
||||
|
||||
# Assert
|
||||
sent_body: bytes = mock_session.post.call_args.kwargs["data"]
|
||||
resources = [
|
||||
parameter.get("attribute_value")
|
||||
for parameter in ET.fromstring(sent_body).findall(".//Parameters")
|
||||
if parameter.get("attribute") == "resource"
|
||||
]
|
||||
assert resources == [CONFIG.default_resource]
|
||||
|
||||
|
||||
def test_log_job_declares_the_xml_body_content_type_the_relay_requires(
|
||||
client: AbriClient, mock_session: MagicMock
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import replace
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
|
@ -120,6 +121,32 @@ def test_amend_job_sends_an_amendoptiappt_envelope_for_the_deals_recorded_job(
|
|||
}
|
||||
|
||||
|
||||
def test_amend_job_books_the_deals_third_party_surveyor_as_the_resource(
|
||||
orchestrator: AbriOrchestrator,
|
||||
mock_session: MagicMock,
|
||||
deal_database: FakeDealDatabase,
|
||||
) -> None:
|
||||
# Arrange: a deal carrying a third-party surveyor identifier amends the
|
||||
# appointment against that resource instead of the configured default.
|
||||
deal_database.job_nos[DEAL_ID] = JOB_NO
|
||||
mock_session.post.return_value.content = _load_fixture(
|
||||
"abri_relay_amendoptiappt_success_response.xml"
|
||||
)
|
||||
|
||||
# Act
|
||||
orchestrator.amend_job(
|
||||
replace(CHANGE, third_party_surveyor_identifier="THIRDPARTY")
|
||||
)
|
||||
|
||||
# Assert
|
||||
sent_body: bytes = mock_session.post.call_args.kwargs["data"]
|
||||
sent_parameters = {
|
||||
parameter.get("attribute"): parameter.get("attribute_value")
|
||||
for parameter in ET.fromstring(sent_body).findall("Body/Request/Parameters")
|
||||
}
|
||||
assert sent_parameters["resource"] == "THIRDPARTY"
|
||||
|
||||
|
||||
# --- happy path: the amended appointment is confirmed back ---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import traceback
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import replace
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
|
@ -141,6 +142,29 @@ def test_log_job_sends_a_logjob_envelope_built_from_the_deals_fields(
|
|||
}
|
||||
|
||||
|
||||
def test_log_job_books_the_deals_third_party_surveyor_as_the_resource(
|
||||
orchestrator: AbriOrchestrator, mock_session: MagicMock
|
||||
) -> None:
|
||||
# Arrange: a deal carrying a third-party surveyor identifier books the
|
||||
# job against that resource instead of the configured default surveyor.
|
||||
mock_session.post.return_value.content = _load_fixture(
|
||||
"abri_relay_logjob_success_response.xml"
|
||||
)
|
||||
|
||||
# Act
|
||||
orchestrator.log_job(
|
||||
replace(BOOKING, third_party_surveyor_identifier="THIRDPARTY")
|
||||
)
|
||||
|
||||
# Assert
|
||||
sent_body: bytes = mock_session.post.call_args.kwargs["data"]
|
||||
sent_parameters = {
|
||||
parameter.get("attribute"): parameter.get("attribute_value")
|
||||
for parameter in ET.fromstring(sent_body).findall("Body/Request/Parameters")
|
||||
}
|
||||
assert sent_parameters["resource"] == "THIRDPARTY"
|
||||
|
||||
|
||||
# --- happy path: the returned job_no lands in HubSpot, then the database ---
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue