Model/scripts/smoke_test_tenant_contacts.py
Daniel Roth 9bae8df6ea A changed booking amends the deal's OpenHousing appointment 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:01:01 +00:00

153 lines
5.8 KiB
Python

"""Manual smoke test for the Abri tenant-data -> HubSpot sync (issue #1467).
Drives the real AbriOrchestrator tenant-data flow end-to-end with only the
Abri HTTP edge stubbed (no Abri API key needed): the canned tenancy XML below flows
through the real parse, phone-priority selection and vulnerability mapping,
and the resulting contact writes hit the real HubSpot portal.
Usage:
1. Ensure HUBSPOT_API_KEY is set in backend/.env.
2. Create a throwaway test deal in the HubSpot UI and paste its id into
DEAL_ID below.
3. Run: python scripts/smoke_test_tenant_contacts.py
4. Check the deal in the UI: two contacts (Daniel Roth, Joe Bloggs), with
Daniel carrying mobilephone, phone, secondary_phone_number,
is_vulnerable=true and a two-line vulnerability_description, and Joe
carrying only a name and is_vulnerable=false.
5. Clean up: paste the printed contact ids into CONTACT_IDS_TO_ARCHIVE,
set ARCHIVE = True and re-run, then delete the test deal in the UI.
All tenant details below are fictional (Ofcom-reserved number ranges).
"""
import os
from pathlib import Path
from typing import Any, List, Optional, cast
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
DEAL_ID = "485125892321"
# Cleanup mode: set ARCHIVE = True and list the contact ids printed by a
# previous run to archive them instead of creating new ones.
ARCHIVE = False
CONTACT_IDS_TO_ARCHIVE: List[str] = []
PLACE_REF = PlaceRef("SMOKE1")
# Daniel Roth exercises best-of-kind phone selection, the runner-up pick into
# secondary_phone_number, and the vulnerability fields; Joe Bloggs exercises
# the always-written is_vulnerable=false with every other property omitted.
SMOKE_TENANCY_XML = b"""<Root>
<Tenancy end_date="" start_date="15/12/2008">SMOKETEST0001<Tenant forenames="Daniel" main-contact="yes" person_title="Mr" surname="Roth">9000001<Mobile priority="10">07700900123</Mobile>
<Mobile priority="20">07700900456</Mobile>
<Telephone priority="5">01632960123</Telephone>
<Telephone priority="30">01632960789</Telephone>
<Vulnerability>Blindness</Vulnerability>
<Vulnerability>Hard of hearing</Vulnerability>
</Tenant>
<Tenant forenames="Joe" main-contact="no" person_title="Mr" surname="Bloggs">9000002</Tenant>
</Tenancy>
</Root>"""
class _CannedResponse:
content = SMOKE_TENANCY_XML
def raise_for_status(self) -> None:
pass
class _StubAbriSession:
"""Stands in for requests.Session; returns the canned tenancy."""
def post(self, url: str, data: bytes) -> _CannedResponse:
return _CannedResponse()
def _hubspot_sdk_client() -> Client:
# Read the key directly rather than via backend.app.config.get_settings,
# whose strict Settings model rejects unrelated extra keys in backend/.env.
env_file = Path(__file__).parents[1] / "backend" / ".env"
access_token = os.getenv("HUBSPOT_API_KEY") or dotenv_values(env_file).get(
"HUBSPOT_API_KEY"
)
if not access_token:
raise SystemExit("Missing HUBSPOT_API_KEY (env var or backend/.env)")
return Client.create(access_token=access_token) # type: ignore[reportUnknownMemberType]
def _stubbed_abri_client() -> AbriClient:
client = AbriClient(
config=AbriConfig(
endpoint_url="https://stubbed.invalid/relay",
username="smoke-test",
password="",
default_resource="",
)
)
client._session = cast(
Any, _StubAbriSession()
) # pyright: ignore[reportPrivateUsage]
return client
class _UnusedDealDatabase:
"""Satisfies the gateway dependency; the tenant-data flow never touches it."""
def record_job_no(self, deal_id: str, job_no: str) -> None:
raise AssertionError("tenant-data sync must not touch the deal database")
def job_no_for_deal(self, deal_id: str) -> Optional[str]:
raise AssertionError("tenant-data sync must not touch the deal database")
def _archive_contacts(sdk_client: Client) -> None:
if not CONTACT_IDS_TO_ARCHIVE:
raise SystemExit("ARCHIVE is set but CONTACT_IDS_TO_ARCHIVE is empty")
for contact_id in CONTACT_IDS_TO_ARCHIVE:
sdk_client.crm.contacts.basic_api.archive(contact_id=contact_id) # type: ignore[reportUnknownMemberType]
print(f"Archived contact {contact_id}")
def main() -> None:
sdk_client = _hubspot_sdk_client()
if ARCHIVE:
_archive_contacts(sdk_client)
return
if DEAL_ID == "EDIT-ME":
raise SystemExit("Set DEAL_ID to a throwaway test deal id first")
orchestrator = AbriOrchestrator(
abri_client=_stubbed_abri_client(),
deal_contacts=HubspotDealContactsClient(sdk_client=sdk_client),
deal_properties=HubspotDealPropertiesClient(sdk_client=sdk_client),
deal_database=_UnusedDealDatabase(),
)
result = orchestrator.sync_tenant_data(PLACE_REF, deal_id=DEAL_ID)
if isinstance(result, AbriRequestRejected):
raise SystemExit(f"Unexpected Abri rejection from stub: {result}")
print(f"Tenancy reference: {result.tenancy_reference}")
print(f"Contacts created: {list(result.contact_ids)}")
print(f"Vulnerable contact count: {result.vulnerable_contact_count}")
print(
"\nTo clean up: paste the ids above into CONTACT_IDS_TO_ARCHIVE, "
"set ARCHIVE = True and re-run."
)
if __name__ == "__main__":
main()