From 45bb3a7e61466a1ef955cb191077c61c80ee69b1 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 7 Jul 2026 10:30:08 +0000 Subject: [PATCH] =?UTF-8?q?A=20manual=20smoke=20script=20proves=20log=20th?= =?UTF-8?q?en=20amend=20end-to-end=20against=20Abri=20UAT=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 --- scripts/smoke_test_abri_log_amend.py | 145 +++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 scripts/smoke_test_abri_log_amend.py diff --git a/scripts/smoke_test_abri_log_amend.py b/scripts/smoke_test_abri_log_amend.py new file mode 100644 index 000000000..cc786f449 --- /dev/null +++ b/scripts/smoke_test_abri_log_amend.py @@ -0,0 +1,145 @@ +"""Manual smoke test for the Abri log -> amend wiring against UAT (issue #1480). + +Drives the real AbriOrchestrator log-job and amend-job flows end-to-end +against Abri's UAT relay endpoint: a job is logged for a test place +reference, its job_no is written back to a throwaway HubSpot deal, and the +appointment is then amended to the following day's other slot — proving the +full log -> write-back -> amend sequence converges. + +The deal-database gateway is in-memory: the write-back to the real deal-data +table belongs to the deployed lambda, and this script must not touch a +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. + 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. + 3. Run: python scripts/smoke_test_abri_log_amend.py + 4. Check: the printed job_no matches the deal's client_booking_reference + in the HubSpot UI, and Abri can see the appointment on the amended + date/slot in their TM solution. + 5. Clean up: delete the test deal in the UI and ask Abri to cancel the + UAT job (there is no CancelJob route yet). +""" + +import os +from datetime import date, timedelta +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +from dotenv import dotenv_values +from hubspot.client import Client # type: ignore[reportMissingTypeStubs] + +from domain.abri.models import AbriRequestRejected, PlaceRef +from infrastructure.abri.abri_client import AbriClient +from infrastructure.abri.config import AbriConfig +from infrastructure.hubspot.deal_contacts_client import HubspotDealContactsClient +from infrastructure.hubspot.deal_properties_client import HubspotDealPropertiesClient +from orchestration.abri_orchestrator import ( + AbriOrchestrator, + AppointmentChange, + ConfirmedSurveyBooking, +) + +DEAL_ID = "EDIT-ME" +PLACE_REF = PlaceRef("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. +LOG_DATE = date.today() + timedelta(days=7) +LOG_TIME = "10:00" # AM slot +AMEND_DATE = LOG_DATE + timedelta(days=1) +AMEND_TIME = "14:30" # PM slot + + +def _env(name: str) -> str: + env_file = Path(__file__).parents[1] / "backend" / ".env" + value = os.getenv(name) or dotenv_values(env_file).get(name) + if not value: + raise SystemExit(f"Missing {name} (env var or backend/.env)") + return value + + +def _hubspot_sdk_client() -> Client: + return Client.create(access_token=_env("HUBSPOT_API_KEY")) # type: ignore[reportUnknownMemberType] + + +def _uat_abri_client() -> AbriClient: + return AbriClient( + config=AbriConfig( + endpoint_url=_env("ABRI_RELAY_URL"), + username=_env("ABRI_RELAY_USERNAME"), + password=_env("ABRI_RELAY_PASSWORD"), + default_resource=_env("ABRI_RELAY_DEFAULT_RESOURCE"), + ) + ) + + +class InMemoryDealDatabase: + """Stands in for the deal-data table; the real write-back is the lambda's.""" + + def __init__(self) -> None: + self.recorded: List[Tuple[str, str]] = [] + self._job_nos: Dict[str, str] = {} + + def record_job_no(self, deal_id: str, job_no: str) -> None: + self.recorded.append((deal_id, job_no)) + self._job_nos[deal_id] = job_no + + def job_no_for_deal(self, deal_id: str) -> Optional[str]: + return self._job_nos.get(deal_id) + + +def main() -> None: + if DEAL_ID == "EDIT-ME" or PLACE_REF == PlaceRef("EDIT-ME"): + raise SystemExit("Edit DEAL_ID and PLACE_REF before running") + + sdk_client = _hubspot_sdk_client() + deal_database = InMemoryDealDatabase() + orchestrator = AbriOrchestrator( + abri_client=_uat_abri_client(), + deal_contacts=HubspotDealContactsClient(sdk_client=sdk_client), + deal_properties=HubspotDealPropertiesClient(sdk_client=sdk_client), + deal_database=deal_database, + ) + + print(f"Logging job for place_ref {PLACE_REF} on {LOG_DATE} at {LOG_TIME}...") + log_result = orchestrator.log_job( + ConfirmedSurveyBooking( + deal_id=DEAL_ID, + place_ref=PLACE_REF, + deal_name=f"Domna UAT smoke test {date.today().isoformat()}", + confirmed_survey_date=LOG_DATE, + confirmed_survey_time=LOG_TIME, + ) + ) + if isinstance(log_result, AbriRequestRejected): + raise SystemExit( + f"LogJob rejected: {log_result.code}: {log_result.message}" + ) + print(f"Job logged: job_no={log_result.job_no} (written back to deal {DEAL_ID})") + + print(f"Amending appointment to {AMEND_DATE} at {AMEND_TIME}...") + amend_result = orchestrator.amend_job( + AppointmentChange( + deal_id=DEAL_ID, + confirmed_survey_date=AMEND_DATE, + confirmed_survey_time=AMEND_TIME, + ) + ) + if isinstance(amend_result, AbriRequestRejected): + raise SystemExit( + f"AmendJob rejected: {amend_result.code}: {amend_result.message}" + ) + print( + f"Appointment amended: job_no={amend_result.job_no} " + f"date={amend_result.appointment_date} slot={amend_result.appointment_time}" + ) + print("Smoke test complete. Remember to clean up (see module docstring).") + + +if __name__ == "__main__": + main()