mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Merge pull request #1648 from Hestia-Homes/feature/abri-api-resource-field
Sync third_party_surveyor_identifier deal field from HubSpot
This commit is contained in:
commit
dc96cf5aba
27 changed files with 431 additions and 102 deletions
4
.github/workflows/_deploy_lambda.yml
vendored
4
.github/workflows/_deploy_lambda.yml
vendored
|
|
@ -107,8 +107,6 @@ on:
|
|||
required: false
|
||||
TF_VAR_abri_relay_password:
|
||||
required: false
|
||||
TF_VAR_abri_relay_default_resource:
|
||||
required: false
|
||||
TF_VAR_test_abri_project_code_override:
|
||||
required: false
|
||||
jobs:
|
||||
|
|
@ -189,7 +187,6 @@ jobs:
|
|||
TF_VAR_abri_relay_url: ${{ secrets.TF_VAR_abri_relay_url }}
|
||||
TF_VAR_abri_relay_username: ${{ secrets.TF_VAR_abri_relay_username }}
|
||||
TF_VAR_abri_relay_password: ${{ secrets.TF_VAR_abri_relay_password }}
|
||||
TF_VAR_abri_relay_default_resource: ${{ secrets.TF_VAR_abri_relay_default_resource }}
|
||||
TF_VAR_test_abri_project_code_override: ${{ secrets.TF_VAR_test_abri_project_code_override }}
|
||||
run: |
|
||||
ECR_REPO_URL_VAR=""
|
||||
|
|
@ -247,7 +244,6 @@ jobs:
|
|||
TF_VAR_abri_relay_url: ${{ secrets.TF_VAR_abri_relay_url }}
|
||||
TF_VAR_abri_relay_username: ${{ secrets.TF_VAR_abri_relay_username }}
|
||||
TF_VAR_abri_relay_password: ${{ secrets.TF_VAR_abri_relay_password }}
|
||||
TF_VAR_abri_relay_default_resource: ${{ secrets.TF_VAR_abri_relay_default_resource }}
|
||||
run: |
|
||||
EXTRA_VARS=""
|
||||
if [[ -n "${{ inputs.ecr_repo }}" ]]; then
|
||||
|
|
|
|||
1
.github/workflows/deploy_terraform.yml
vendored
1
.github/workflows/deploy_terraform.yml
vendored
|
|
@ -740,7 +740,6 @@ jobs:
|
|||
TF_VAR_abri_relay_url: ${{ secrets.ABRI_RELAY_URL }}
|
||||
TF_VAR_abri_relay_username: ${{ secrets.ABRI_RELAY_USERNAME }}
|
||||
TF_VAR_abri_relay_password: ${{ secrets.ABRI_RELAY_PASSWORD }}
|
||||
TF_VAR_abri_relay_default_resource: ${{ secrets.ABRI_RELAY_DEFAULT_RESOURCE }}
|
||||
|
||||
# ============================================================
|
||||
# Build Audit Generator image
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ visibility surface.
|
|||
from datetime import date
|
||||
from typing import Dict, List, Literal, Optional, Tuple
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
AbriFlow = Literal["amend_job", "log_job", "sync_tenant_data", "abandon_job"]
|
||||
|
||||
|
|
@ -19,8 +19,13 @@ AbriFlow = Literal["amend_job", "log_job", "sync_tenant_data", "abandon_job"]
|
|||
# alone would reject valid messages; the orchestrator fails the flow if a deal
|
||||
# carries neither. Its outcome is optional too, only feeding the reason seam.
|
||||
_FIELDS_BY_FLOW: Dict[AbriFlow, Tuple[str, ...]] = {
|
||||
"amend_job": ("confirmed_survey_date",),
|
||||
"log_job": ("place_ref", "deal_name", "confirmed_survey_date"),
|
||||
"amend_job": ("confirmed_survey_date", "third_party_surveyor_identifier"),
|
||||
"log_job": (
|
||||
"place_ref",
|
||||
"deal_name",
|
||||
"confirmed_survey_date",
|
||||
"third_party_surveyor_identifier",
|
||||
),
|
||||
"sync_tenant_data": ("place_ref",),
|
||||
"abandon_job": (),
|
||||
}
|
||||
|
|
@ -37,6 +42,18 @@ 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; required by the log and
|
||||
# amend flows. Empty string is also rejected by Abri, so not allowed here
|
||||
third_party_surveyor_identifier: Optional[str] = None
|
||||
|
||||
@field_validator("third_party_surveyor_identifier")
|
||||
@classmethod
|
||||
def _blank_surveyor_identifier_is_missing(
|
||||
cls, value: Optional[str]
|
||||
) -> Optional[str]:
|
||||
if value is not None and not value.strip():
|
||||
return None
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _each_fired_flow_has_its_fields(self) -> "AbriTriggerRequest":
|
||||
|
|
|
|||
|
|
@ -81,13 +81,19 @@ def dispatch_abri_flows(
|
|||
|
||||
|
||||
def _run_amend(request: AbriTriggerRequest, flows: AbriFlows) -> str:
|
||||
if request.confirmed_survey_date is None:
|
||||
raise ValueError("amend_job fired without a confirmed_survey_date")
|
||||
if (
|
||||
request.confirmed_survey_date is None
|
||||
or request.third_party_surveyor_identifier is None
|
||||
):
|
||||
raise ValueError(
|
||||
"amend_job fired without a confirmed_survey_date/surveyor identifier"
|
||||
)
|
||||
outcome = flows.amend_job(
|
||||
AppointmentChange(
|
||||
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):
|
||||
|
|
@ -111,8 +117,12 @@ def _run_log(
|
|||
request.place_ref is None
|
||||
or request.deal_name is None
|
||||
or request.confirmed_survey_date is None
|
||||
or request.third_party_surveyor_identifier is None
|
||||
):
|
||||
raise ValueError("log_job fired without place_ref/deal_name/survey date")
|
||||
raise ValueError(
|
||||
"log_job fired without place_ref/deal_name/survey date/"
|
||||
"surveyor identifier"
|
||||
)
|
||||
try:
|
||||
outcome = flows.log_job(
|
||||
ConfirmedSurveyBooking(
|
||||
|
|
@ -121,6 +131,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:
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ class HubspotDealData(SQLModel, table=True):
|
|||
lodgement_date: Optional[datetime] = Field(default=None)
|
||||
expected_commencement_date: Optional[datetime] = Field(default=None)
|
||||
surveyor: Optional[str] = Field(default=None)
|
||||
third_party_surveyor_identifier: Optional[str] = Field(default=None)
|
||||
confirmed_survey_date: Optional[datetime] = Field(default=None)
|
||||
confirmed_survey_time: Optional[str] = Field(default=None)
|
||||
surveyed_date: Optional[datetime] = Field(default=None)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,9 @@ module "lambda" {
|
|||
|
||||
# Abri relay credentials are per-stage: the dev/staging stage points at
|
||||
# Abri's UAT relay endpoint; production values are pending from Abri.
|
||||
ABRI_RELAY_URL = var.abri_relay_url
|
||||
ABRI_RELAY_USERNAME = var.abri_relay_username
|
||||
ABRI_RELAY_PASSWORD = var.abri_relay_password
|
||||
ABRI_RELAY_DEFAULT_RESOURCE = var.abri_relay_default_resource
|
||||
ABRI_RELAY_URL = var.abri_relay_url
|
||||
ABRI_RELAY_USERNAME = var.abri_relay_username
|
||||
ABRI_RELAY_PASSWORD = var.abri_relay_password
|
||||
|
||||
HUBSPOT_API_KEY = var.hubspot_api_key
|
||||
|
||||
|
|
|
|||
|
|
@ -58,11 +58,6 @@ variable "abri_relay_password" {
|
|||
sensitive = true
|
||||
}
|
||||
|
||||
variable "abri_relay_default_resource" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "hubspot_api_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
|
|
|
|||
91
docs/abri-hubspot-ops-guide.md
Normal file
91
docs/abri-hubspot-ops-guide.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Abri bookings: what your HubSpot changes do
|
||||
|
||||
**Who this is for:** operations staff who work Abri deals in HubSpot — booking surveys,
|
||||
recording visit outcomes, updating deal fields.
|
||||
|
||||
**The one-line version:** for deals on the Abri Stock Condition project, certain HubSpot
|
||||
field changes are picked up automatically and sent to Abri's scheduling system
|
||||
(OpenHousing). You never talk to OpenHousing directly — the deal fields are the controls.
|
||||
|
||||
---
|
||||
|
||||
## Which deals does this apply to?
|
||||
|
||||
Only deals associated with the **Abri Stock Condition — Privately Funded** project.
|
||||
Changes to any other deal are ignored by this integration.
|
||||
|
||||
## How fast does it happen?
|
||||
|
||||
Changes are picked up automatically shortly after you save them in HubSpot — usually
|
||||
within a few minutes. Nothing fires while you are mid-edit; it's the saved change that
|
||||
counts.
|
||||
|
||||
---
|
||||
|
||||
## What your changes do in Abri's system
|
||||
|
||||
| When you… | Abri's system… | How you know it worked |
|
||||
|---|---|---|
|
||||
| Set **Expected commencement date** for the first time | Sends us the tenancy details for the property | Tenant contact records appear in HubSpot, linked to the deal |
|
||||
| Set **Confirmed survey date** for the first time | Creates the survey booking (a "job") against the property, assigned to the surveyor on the deal | Abri's job number appears in **Client booking reference** on the deal |
|
||||
| Change **Confirmed survey date**, **Confirmed survey time** or **Third-party surveyor identifier** on a deal that already has a booking | Updates the existing appointment — new date/time and/or reassigns it to the new surveyor | The deal keeps the same Client booking reference |
|
||||
| Record a **3rd attempt** (Number of attempts reaches 3) **and** set **Outcome** to an unsuccessful value (see below) | Cancels the booking as abandoned | — (this only fires once per deal) |
|
||||
|
||||
**Unsuccessful outcomes** that count towards abandonment — the wording must match exactly:
|
||||
|
||||
- `no answer`
|
||||
- `cancelled / no show`
|
||||
- `tenant refusal`
|
||||
- `not viable`
|
||||
|
||||
Any other outcome (or fewer than 3 attempts) does **not** cancel anything in Abri's
|
||||
system.
|
||||
|
||||
## The fields, and why they matter
|
||||
|
||||
| HubSpot deal field | What it feeds |
|
||||
|---|---|
|
||||
| **Expected commencement date** | Setting it the first time triggers the tenant-details fetch |
|
||||
| **Confirmed survey date** | The appointment date sent to Abri |
|
||||
| **Confirmed survey time** | The appointment slot: `morning` → AM, `afternoon` → PM, a clock time like `09:30` → AM/PM by whether it's before midday, left blank → all day |
|
||||
| **Third-party surveyor identifier** | Which surveyor the booking is assigned to in Abri's system. **Required** — a booking or appointment change cannot be sent without it, and it must be one of Abri's valid surveyor codes. Changing it on a deal that already has a booking reassigns that booking to the new surveyor |
|
||||
| **Number of attempts** + **Outcome** | Together they trigger abandonment (3+ attempts and an unsuccessful outcome) |
|
||||
| **Client booking reference** | Abri's job number, written back **by the system** after the booking is created |
|
||||
| **Deal name** | Used as the job description Abri's staff see |
|
||||
|
||||
---
|
||||
|
||||
## Things that will catch you out
|
||||
|
||||
### Clearing the survey date does NOT cancel the booking
|
||||
|
||||
If you delete or blank the confirmed survey date, the booking **still exists in Abri's
|
||||
system** and the surveyor is still expected. There is currently no way to cancel a
|
||||
booking from HubSpot other than the 3-attempts abandonment route. If a booking needs
|
||||
cancelling for any other reason, contact the tech team.
|
||||
|
||||
### Don't edit Client booking reference
|
||||
|
||||
It is written automatically with Abri's job number and is how the system finds the
|
||||
booking when you later change or abandon it. If you overwrite or clear it, appointment
|
||||
changes and abandonments will stop working for that deal.
|
||||
|
||||
### The surveyor identifier (Third-Party Surveyor Identifier) must be a valid Abri code
|
||||
|
||||
Free-typed or misspelt values will be rejected by Abri's system and the booking won't be
|
||||
made. The tech team holds the current list of valid surveyor codes from Abri — use a
|
||||
value from that list. Soon this field will be set automatically from the deal
|
||||
owner.
|
||||
|
||||
### Outcome wording is exact
|
||||
|
||||
Abandonment only recognises the four unsuccessful outcomes listed above, spelled exactly
|
||||
that way. A variation like "No Answer - left card" won't trigger it.
|
||||
|
||||
### If the job number never appears
|
||||
|
||||
The booking didn't go through — most often because the surveyor identifier was missing
|
||||
or invalid, or the deal isn't on the Abri project. Failures land with the tech team, not
|
||||
in HubSpot, so you won't see an error message. If **Client booking reference** is still
|
||||
empty well after you set the survey date, flag it to the tech team rather than re-editing
|
||||
fields.
|
||||
|
|
@ -44,6 +44,7 @@ class LogJobRequest:
|
|||
appointment_time: SlotCode
|
||||
short_description: str
|
||||
long_description: str
|
||||
resource: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -67,7 +68,7 @@ class AmendJobRequest:
|
|||
job_no: str
|
||||
appointment_date: date
|
||||
appointment_time: SlotCode
|
||||
resource: Optional[str] = None
|
||||
resource: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ class HubspotClient:
|
|||
"lodgement_date",
|
||||
"expected_commencement_date",
|
||||
"surveyor",
|
||||
"third_party_surveyor_identifier",
|
||||
"confirmed_survey_date",
|
||||
"confirmed_survey_time",
|
||||
"client_booking_reference",
|
||||
|
|
|
|||
|
|
@ -270,6 +270,9 @@ class HubspotDataToDb:
|
|||
deal_data.get("expected_commencement_date")
|
||||
),
|
||||
"surveyor": deal_data.get("surveyor"),
|
||||
"third_party_surveyor_identifier": deal_data.get(
|
||||
"third_party_surveyor_identifier"
|
||||
),
|
||||
"confirmed_survey_date": parse_hs_date(
|
||||
deal_data.get("confirmed_survey_date")
|
||||
),
|
||||
|
|
@ -391,6 +394,9 @@ class HubspotDataToDb:
|
|||
deal_data.get("expected_commencement_date")
|
||||
),
|
||||
surveyor=deal_data.get("surveyor"),
|
||||
third_party_surveyor_identifier=deal_data.get(
|
||||
"third_party_surveyor_identifier"
|
||||
),
|
||||
confirmed_survey_date=parse_hs_date(deal_data.get("confirmed_survey_date")),
|
||||
confirmed_survey_time=deal_data.get("confirmed_survey_time"),
|
||||
client_booking_reference=deal_data.get("client_booking_reference"),
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ class HubspotDealDiffer:
|
|||
"lodgement_status": "lodgement_status",
|
||||
"design_type": "design_type",
|
||||
"surveyor": "surveyor",
|
||||
"third_party_surveyor_identifier": "third_party_surveyor_identifier",
|
||||
"confirmed_survey_time": "confirmed_survey_time",
|
||||
"client_booking_reference": "client_booking_reference",
|
||||
"survey_type": "survey_type",
|
||||
|
|
@ -243,9 +244,7 @@ class HubspotDealDiffer:
|
|||
flows.append("abandon_job")
|
||||
|
||||
if not flows:
|
||||
logger.info(
|
||||
"No Abri flows triggered for HubSpot deal %s", hubspot_deal_id
|
||||
)
|
||||
logger.info("No Abri flows triggered for HubSpot deal %s", hubspot_deal_id)
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
|
|
@ -266,6 +265,9 @@ class HubspotDealDiffer:
|
|||
else None
|
||||
),
|
||||
"confirmed_survey_time": new_deal.get("confirmed_survey_time"),
|
||||
"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": (
|
||||
|
|
@ -318,14 +320,24 @@ class HubspotDealDiffer:
|
|||
|
||||
new_survey_date = parse_hs_date(new_deal.get("confirmed_survey_date"))
|
||||
new_survey_time = new_deal.get("confirmed_survey_time")
|
||||
new_surveyor = HubspotDealDiffer._normalised_surveyor(
|
||||
new_deal.get("third_party_surveyor_identifier")
|
||||
)
|
||||
old_surveyor = HubspotDealDiffer._normalised_surveyor(
|
||||
old_deal.third_party_surveyor_identifier
|
||||
)
|
||||
logger.info(
|
||||
"Abri job-amendment check: "
|
||||
"old_confirmed_survey_date=%s new_confirmed_survey_date=%s "
|
||||
"old_confirmed_survey_time=%s new_confirmed_survey_time=%s",
|
||||
"old_confirmed_survey_time=%s new_confirmed_survey_time=%s "
|
||||
"old_third_party_surveyor_identifier=%s "
|
||||
"new_third_party_surveyor_identifier=%s",
|
||||
old_deal.confirmed_survey_date,
|
||||
new_survey_date,
|
||||
old_deal.confirmed_survey_time,
|
||||
new_survey_time,
|
||||
old_surveyor,
|
||||
new_surveyor,
|
||||
)
|
||||
|
||||
# A first-time survey date logs a new job, not an amendment.
|
||||
|
|
@ -339,7 +351,10 @@ class HubspotDealDiffer:
|
|||
if old_deal.confirmed_survey_date != new_survey_date:
|
||||
return True
|
||||
|
||||
return old_deal.confirmed_survey_time != new_survey_time
|
||||
if old_deal.confirmed_survey_time != new_survey_time:
|
||||
return True
|
||||
|
||||
return old_surveyor != new_surveyor
|
||||
|
||||
@staticmethod
|
||||
def check_for_abri_tenant_data_fetch(
|
||||
|
|
@ -405,6 +420,14 @@ class HubspotDealDiffer:
|
|||
old_outcome = (old_deal.outcome or "").lower()
|
||||
return new_outcome == "surveyed" and old_outcome != "surveyed"
|
||||
|
||||
@staticmethod
|
||||
def _normalised_surveyor(value: Optional[str]) -> Optional[str]:
|
||||
# HubSpot sends "" for an unset property and the DB stores whatever
|
||||
# the scrape wrote, so blank and missing must compare as equal.
|
||||
if value is None or not value.strip():
|
||||
return None
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _is_abandoned(
|
||||
number_of_attempts: Optional[str], outcome: Optional[str]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -94,6 +118,115 @@ def test_a_changed_confirmed_survey_date_builds_an_amend_job_message() -> None:
|
|||
assert message["confirmed_survey_date"] == "2026-06-24"
|
||||
|
||||
|
||||
def test_a_changed_surveyor_on_a_booked_deal_builds_an_amend_job_message() -> None:
|
||||
# Arrange: booking exists; only the surveyor changes.
|
||||
old_deal = make_old_deal(
|
||||
project_code=ABRI_PROJECT_CODE,
|
||||
confirmed_survey_date=datetime(2026, 6, 24, tzinfo=timezone.utc),
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="OLDSURVEYOR",
|
||||
)
|
||||
new_deal = make_new_deal(
|
||||
confirmed_survey_date="2026-06-24",
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="NEWSURVEYOR",
|
||||
)
|
||||
|
||||
# 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["flows"] == ["amend_job"]
|
||||
assert message["third_party_surveyor_identifier"] == "NEWSURVEYOR"
|
||||
|
||||
|
||||
def test_a_blank_surveyor_on_a_booked_deal_with_no_stored_surveyor_is_not_a_change() -> None:
|
||||
# Arrange: booking exists; surveyor unset in the DB, blank in HubSpot.
|
||||
old_deal = make_old_deal(
|
||||
project_code=ABRI_PROJECT_CODE,
|
||||
confirmed_survey_date=datetime(2026, 6, 24, tzinfo=timezone.utc),
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier=None,
|
||||
)
|
||||
new_deal = make_new_deal(
|
||||
confirmed_survey_date="2026-06-24",
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="",
|
||||
)
|
||||
|
||||
# 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 None
|
||||
|
||||
|
||||
def test_a_blank_stored_surveyor_and_a_missing_incoming_one_are_not_a_change() -> None:
|
||||
# Arrange: booking exists; DB stored "" for the surveyor, HubSpot omits it.
|
||||
old_deal = make_old_deal(
|
||||
project_code=ABRI_PROJECT_CODE,
|
||||
confirmed_survey_date=datetime(2026, 6, 24, tzinfo=timezone.utc),
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="",
|
||||
)
|
||||
new_deal = make_new_deal(
|
||||
confirmed_survey_date="2026-06-24",
|
||||
confirmed_survey_time="14:30",
|
||||
)
|
||||
|
||||
# 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 None
|
||||
|
||||
|
||||
def test_an_unchanged_surveyor_on_a_booked_deal_fires_nothing() -> None:
|
||||
# Arrange: booking exists; surveyor and appointment are unchanged.
|
||||
old_deal = make_old_deal(
|
||||
project_code=ABRI_PROJECT_CODE,
|
||||
confirmed_survey_date=datetime(2026, 6, 24, tzinfo=timezone.utc),
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="SURVEYOR",
|
||||
)
|
||||
new_deal = make_new_deal(
|
||||
confirmed_survey_date="2026-06-24",
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="SURVEYOR",
|
||||
)
|
||||
|
||||
# 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 None
|
||||
|
||||
|
||||
def test_a_first_expected_commencement_date_builds_a_tenant_sync_message() -> None:
|
||||
# Arrange
|
||||
old_deal = make_old_deal(
|
||||
|
|
|
|||
|
|
@ -67,7 +67,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", request.resource),
|
||||
("resource_group", RESOURCE_GROUP),
|
||||
],
|
||||
)
|
||||
|
|
@ -78,20 +78,12 @@ class AbriClient:
|
|||
return self._parse_job_logged(outcome)
|
||||
|
||||
def amend_job(self, request: AmendJobRequest) -> AmendJobResult:
|
||||
# OpenHousing silently drops the appointment unless a bookable resource
|
||||
# is sent, so fall back to the configured surveyor when the request
|
||||
# carries no explicit override (mirrors log_job).
|
||||
resource = (
|
||||
request.resource
|
||||
if request.resource is not None
|
||||
else self._config.default_resource
|
||||
)
|
||||
parameters = [
|
||||
("job_no", request.job_no),
|
||||
("action", "amend"),
|
||||
("appointment_date", _format_appointment_date(request.appointment_date)),
|
||||
("appointment_time", request.appointment_time),
|
||||
("resource", resource),
|
||||
("resource", request.resource),
|
||||
]
|
||||
|
||||
outcome = self._exchange(
|
||||
|
|
|
|||
|
|
@ -7,20 +7,11 @@ class AbriConfig:
|
|||
endpoint_url: str
|
||||
username: str
|
||||
password: str
|
||||
default_resource: str
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, env: Mapping[str, str]) -> "AbriConfig":
|
||||
# A blank default_resource does not error at the relay — OpenHousing
|
||||
# logs the job but silently drops the appointment — so reject it here
|
||||
# rather than let the misconfiguration ship an empty resource.
|
||||
default_resource = env["ABRI_RELAY_DEFAULT_RESOURCE"]
|
||||
if not default_resource.strip():
|
||||
raise ValueError("ABRI_RELAY_DEFAULT_RESOURCE must not be blank")
|
||||
|
||||
return cls(
|
||||
endpoint_url=env["ABRI_RELAY_URL"],
|
||||
username=env["ABRI_RELAY_USERNAME"],
|
||||
password=env["ABRI_RELAY_PASSWORD"],
|
||||
default_resource=default_resource,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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: str
|
||||
|
||||
|
||||
@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: str
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ production database.
|
|||
Usage:
|
||||
1. Ensure these are set in the environment or backend/.env:
|
||||
HUBSPOT_API_KEY, ABRI_RELAY_URL (the UAT endpoint),
|
||||
ABRI_RELAY_USERNAME, ABRI_RELAY_PASSWORD, ABRI_RELAY_DEFAULT_RESOURCE.
|
||||
ABRI_RELAY_USERNAME, ABRI_RELAY_PASSWORD.
|
||||
2. Create a throwaway test deal in the HubSpot UI and paste its id into
|
||||
DEAL_ID below; paste Abri's agreed UAT test place reference into
|
||||
PLACE_REF.
|
||||
|
|
@ -46,6 +46,9 @@ from orchestration.abri_orchestrator import (
|
|||
|
||||
DEAL_ID = "EDIT-ME"
|
||||
PLACE_REF = PlaceRef("EDIT-ME")
|
||||
# The bookable OpenHousing resource to book the UAT job against (mandatory:
|
||||
# in production it comes from the deal's third_party_surveyor_identifier).
|
||||
SURVEYOR = "EDIT-ME"
|
||||
|
||||
# Log for the first weekday at least a week out (a slot Abri UAT will take),
|
||||
# then amend to the following day's opposite slot.
|
||||
|
|
@ -73,7 +76,6 @@ def _uat_abri_client() -> AbriClient:
|
|||
endpoint_url=_env("ABRI_RELAY_URL"),
|
||||
username=_env("ABRI_RELAY_USERNAME"),
|
||||
password=_env("ABRI_RELAY_PASSWORD"),
|
||||
default_resource=_env("ABRI_RELAY_DEFAULT_RESOURCE"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -94,8 +96,8 @@ class InMemoryDealDatabase:
|
|||
|
||||
|
||||
def main() -> None:
|
||||
if DEAL_ID == "EDIT-ME" or PLACE_REF == PlaceRef("EDIT-ME"):
|
||||
raise SystemExit("Edit DEAL_ID and PLACE_REF before running")
|
||||
if "EDIT-ME" in (DEAL_ID, str(PLACE_REF), SURVEYOR):
|
||||
raise SystemExit("Edit DEAL_ID, PLACE_REF and SURVEYOR before running")
|
||||
|
||||
sdk_client = _hubspot_sdk_client()
|
||||
deal_database = InMemoryDealDatabase()
|
||||
|
|
@ -114,6 +116,7 @@ def main() -> None:
|
|||
deal_name=f"Domna UAT smoke test {date.today().isoformat()}",
|
||||
confirmed_survey_date=LOG_DATE,
|
||||
confirmed_survey_time=LOG_TIME,
|
||||
third_party_surveyor_identifier=SURVEYOR,
|
||||
)
|
||||
)
|
||||
if isinstance(log_result, AbriRequestRejected):
|
||||
|
|
@ -128,6 +131,7 @@ def main() -> None:
|
|||
deal_id=DEAL_ID,
|
||||
confirmed_survey_date=AMEND_DATE,
|
||||
confirmed_survey_time=AMEND_TIME,
|
||||
third_party_surveyor_identifier=SURVEYOR,
|
||||
)
|
||||
)
|
||||
if isinstance(amend_result, AbriRequestRejected):
|
||||
|
|
|
|||
|
|
@ -229,7 +229,6 @@ def main() -> None:
|
|||
endpoint_url="https://stubbed.invalid/relay",
|
||||
username="smoke-test",
|
||||
password="",
|
||||
default_resource="SMOKE",
|
||||
)
|
||||
)
|
||||
abri_client._session = cast( # pyright: ignore[reportPrivateUsage]
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ def _stubbed_abri_client() -> AbriClient:
|
|||
endpoint_url="https://stubbed.invalid/relay",
|
||||
username="smoke-test",
|
||||
password="",
|
||||
default_resource="",
|
||||
)
|
||||
)
|
||||
client._session = cast(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ def _full_message() -> Dict[str, Any]:
|
|||
"deal_name": "49 Admers Crescent",
|
||||
"confirmed_survey_date": "2026-06-24",
|
||||
"confirmed_survey_time": "14:30",
|
||||
"third_party_surveyor_identifier": "THIRDPARTY",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ def test_a_full_message_parses_into_a_typed_request() -> None:
|
|||
assert request.deal_name == "49 Admers Crescent"
|
||||
assert request.confirmed_survey_date == date(2026, 6, 24)
|
||||
assert request.confirmed_survey_time == "14:30"
|
||||
assert request.third_party_surveyor_identifier == "THIRDPARTY"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -37,7 +39,9 @@ def test_a_full_message_parses_into_a_typed_request() -> None:
|
|||
("log_job", "place_ref"),
|
||||
("log_job", "deal_name"),
|
||||
("log_job", "confirmed_survey_date"),
|
||||
("log_job", "third_party_surveyor_identifier"),
|
||||
("amend_job", "confirmed_survey_date"),
|
||||
("amend_job", "third_party_surveyor_identifier"),
|
||||
("sync_tenant_data", "place_ref"),
|
||||
],
|
||||
)
|
||||
|
|
@ -97,6 +101,20 @@ def test_a_message_naming_abandon_job_validates_with_only_the_deal_id() -> None:
|
|||
assert request.flows == ["abandon_job"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("blank", ["", " "])
|
||||
def test_a_blank_surveyor_identifier_is_treated_as_missing(blank: str) -> None:
|
||||
# A blank resource does not error at the relay: OpenHousing logs the job
|
||||
# but silently drops the appointment. Treat blank as missing so the
|
||||
# per-flow validation fails loudly instead.
|
||||
message = _full_message()
|
||||
message["flows"] = ["log_job"]
|
||||
message["third_party_surveyor_identifier"] = blank
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(ValidationError, match="third_party_surveyor_identifier"):
|
||||
AbriTriggerRequest.model_validate(message)
|
||||
|
||||
|
||||
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",
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ CONFIG = AbriConfig(
|
|||
endpoint_url=ENDPOINT_URL,
|
||||
username="DomnaWeb",
|
||||
password="",
|
||||
default_resource="NAULKH",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -52,6 +51,7 @@ def _spec_example_request() -> LogJobRequest:
|
|||
appointment_time="PM",
|
||||
short_description="Christian's SCS External Test.",
|
||||
long_description="Christian's SCS External Test.",
|
||||
resource="NAULKH",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -60,6 +60,7 @@ def _spec_example_amend_request() -> AmendJobRequest:
|
|||
job_no="AC0439951",
|
||||
appointment_date=date(2026, 6, 19),
|
||||
appointment_time="PM",
|
||||
resource="NAULKH",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -145,6 +146,38 @@ def test_log_job_sends_the_recorded_logjob_envelope_to_the_relay_endpoint(
|
|||
)
|
||||
|
||||
|
||||
def test_log_job_books_the_job_against_the_requests_resource(
|
||||
client: AbriClient, mock_session: MagicMock
|
||||
) -> None:
|
||||
# Arrange: the resource is mandatory on the request — the deal's
|
||||
# third-party surveyor identifier — and is sent verbatim.
|
||||
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_declares_the_xml_body_content_type_the_relay_requires(
|
||||
client: AbriClient, mock_session: MagicMock
|
||||
) -> None:
|
||||
|
|
@ -410,29 +443,6 @@ def test_amend_job_sends_the_recorded_amendoptiappt_envelope_to_the_relay_endpoi
|
|||
)
|
||||
|
||||
|
||||
def test_amend_job_defaults_the_resource_to_the_configured_surveyor(
|
||||
client: AbriClient, mock_session: MagicMock
|
||||
) -> None:
|
||||
# Arrange: OpenHousing silently drops the appointment unless a bookable
|
||||
# resource is sent, so an amend 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_amendoptiappt_success_response.xml"
|
||||
)
|
||||
|
||||
# Act
|
||||
client.amend_job(_spec_example_amend_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]
|
||||
|
||||
|
||||
# --- amend_job: rejection ---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import pytest
|
||||
|
||||
from infrastructure.abri.config import AbriConfig
|
||||
|
||||
|
||||
def test_config_hydrates_relay_credentials_and_defaults_from_environment() -> None:
|
||||
def test_config_hydrates_relay_credentials_from_environment() -> None:
|
||||
# Arrange
|
||||
env = {
|
||||
"ABRI_RELAY_URL": "https://relay.example.test/api/DomnaRelay?code=key",
|
||||
"ABRI_RELAY_USERNAME": "DomnaWeb",
|
||||
"ABRI_RELAY_PASSWORD": "secret",
|
||||
"ABRI_RELAY_DEFAULT_RESOURCE": "CBRYAN",
|
||||
}
|
||||
|
||||
# Act
|
||||
|
|
@ -20,20 +17,4 @@ def test_config_hydrates_relay_credentials_and_defaults_from_environment() -> No
|
|||
endpoint_url="https://relay.example.test/api/DomnaRelay?code=key",
|
||||
username="DomnaWeb",
|
||||
password="secret",
|
||||
default_resource="CBRYAN",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("blank", ["", " "])
|
||||
def test_config_rejects_a_blank_default_resource(blank: str) -> None:
|
||||
# A blank resource does not error at the relay: OpenHousing logs the job
|
||||
# but silently drops the appointment. Fail loudly at config load instead.
|
||||
env = {
|
||||
"ABRI_RELAY_URL": "https://relay.example.test/api/DomnaRelay?code=key",
|
||||
"ABRI_RELAY_USERNAME": "DomnaWeb",
|
||||
"ABRI_RELAY_PASSWORD": "secret",
|
||||
"ABRI_RELAY_DEFAULT_RESOURCE": blank,
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="ABRI_RELAY_DEFAULT_RESOURCE"):
|
||||
AbriConfig.from_env(env)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ CONFIG = AbriConfig(
|
|||
endpoint_url=ENDPOINT_URL,
|
||||
username="DomnaWeb",
|
||||
password="",
|
||||
default_resource="NAULKH",
|
||||
)
|
||||
|
||||
ABANDONMENT = DealAbandonment(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -26,13 +27,13 @@ CONFIG = AbriConfig(
|
|||
endpoint_url=ENDPOINT_URL,
|
||||
username="DomnaWeb",
|
||||
password="",
|
||||
default_resource="NAULKH",
|
||||
)
|
||||
|
||||
CHANGE = AppointmentChange(
|
||||
deal_id=DEAL_ID,
|
||||
confirmed_survey_date=date(2026, 6, 24),
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="NAULKH",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -114,12 +115,38 @@ def test_amend_job_sends_an_amendoptiappt_envelope_for_the_deals_recorded_job(
|
|||
"action": "amend",
|
||||
"appointment_date": "24/06/2026",
|
||||
"appointment_time": "PM",
|
||||
# OpenHousing drops the appointment without a bookable resource, so the
|
||||
# amend defaults to the configured surveyor when none is overridden.
|
||||
"resource": CONFIG.default_resource,
|
||||
# OpenHousing drops the appointment without a bookable resource, so
|
||||
# the deal's third-party surveyor identifier is always sent.
|
||||
"resource": "NAULKH",
|
||||
}
|
||||
|
||||
|
||||
def test_amend_job_books_the_deals_third_party_surveyor_as_the_resource(
|
||||
orchestrator: AbriOrchestrator,
|
||||
mock_session: MagicMock,
|
||||
deal_database: FakeDealDatabase,
|
||||
) -> None:
|
||||
# Arrange: the deal's third-party surveyor identifier is booked as the
|
||||
# amended appointment's resource.
|
||||
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
|
||||
|
|
@ -32,7 +33,6 @@ CONFIG = AbriConfig(
|
|||
endpoint_url=ENDPOINT_URL,
|
||||
username="DomnaWeb",
|
||||
password="",
|
||||
default_resource="NAULKH",
|
||||
)
|
||||
|
||||
BOOKING = ConfirmedSurveyBooking(
|
||||
|
|
@ -41,6 +41,7 @@ BOOKING = ConfirmedSurveyBooking(
|
|||
deal_name="49 Admers Crescent",
|
||||
confirmed_survey_date=date(2026, 6, 18),
|
||||
confirmed_survey_time="14:30",
|
||||
third_party_surveyor_identifier="NAULKH",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -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: the deal's third-party surveyor identifier is booked as the
|
||||
# job's resource.
|
||||
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 ---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ CONFIG = AbriConfig(
|
|||
endpoint_url=ENDPOINT_URL,
|
||||
username="DomnaWeb",
|
||||
password="",
|
||||
default_resource="NAULKH",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue