mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
268 lines
11 KiB
Python
268 lines
11 KiB
Python
"""Manual smoke test for the scraper->dispatch LogJob wiring (issue #1480).
|
|
|
|
The closest run to end-to-end available before Abri send API credentials:
|
|
every seam is real except the Abri HTTP edge, which is stubbed to return a
|
|
canned LogJob success carrying a fake job_no. Concretely, the script
|
|
|
|
1. fetches the deal, listing and project from the real HubSpot portal
|
|
(exactly what the scraper lambda sees),
|
|
2. reads the deal's row from the real (dev) deal database,
|
|
3. runs the real differ predicates via build_abri_trigger_message,
|
|
4. validates the real message contract (AbriTriggerRequest),
|
|
5. runs the real dispatch through the real AbriOrchestrator, so the fake
|
|
job_no is written to the deal's client_booking_reference in HubSpot
|
|
first, then to the job_no column in the database,
|
|
6. reads both back and prints them.
|
|
|
|
NOTE: the LogJob trigger fires when the CONFIRMED SURVEY DATE is first set
|
|
on an Abri deal. (Setting the *expected commencement date* fires the
|
|
tenant-data flow instead — that flow is skipped here so the stub never
|
|
mints junk contacts; smoke_test_tenant_contacts.py covers it.)
|
|
|
|
Usage:
|
|
1. Ensure HUBSPOT_API_KEY and the postgres_* connection values are in
|
|
backend/.env (they are read tolerantly; the strict Settings .env
|
|
parsing is bypassed).
|
|
2. Pick a test deal on the "Abri Condition" project that the scraper has
|
|
already ingested (its row must exist in hubspot_deal_data). Paste its
|
|
id into DEAL_ID below.
|
|
3. In the HubSpot UI, set the deal's confirmed survey date (and
|
|
optionally a time). If the dev scraper lambda syncs the change into
|
|
the database before you run this, the differ will see no change — set
|
|
RESET_DB_ROW_FIRST = True to null the row's survey date and job_no so
|
|
the trigger fires deterministically.
|
|
4. Run: python scripts/smoke_test_abri_logjob_flow.py
|
|
5. Check the printed read-backs, and the deal in the HubSpot UI: the
|
|
fake job_no (SMOKE...) should sit in client_booking_reference.
|
|
6. Run it again unchanged: the dispatch should print
|
|
"skipped: job SMOKE... already logged" — the idempotency guard.
|
|
7. Clean up: set CLEANUP = True and re-run to clear the fake job_no from
|
|
both HubSpot and the database, then unset the deal's survey date.
|
|
"""
|
|
|
|
import os
|
|
import xml.etree.ElementTree as ET
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, List, Optional, cast
|
|
|
|
from dotenv import dotenv_values
|
|
|
|
DEAL_ID = "EDIT-ME"
|
|
|
|
# Null the DB row's confirmed_survey_date + job_no before evaluating the
|
|
# differ, so the trigger fires even if the dev scraper already synced it.
|
|
RESET_DB_ROW_FIRST = False
|
|
|
|
# Cleanup mode: clear the fake job_no from HubSpot and the database.
|
|
CLEANUP = False
|
|
|
|
FAKE_JOB_NO = "SMOKE" + datetime.now().strftime("%d%H%M")
|
|
|
|
JOB_NO_DEAL_PROPERTY = "client_booking_reference"
|
|
|
|
|
|
def _bootstrap_env() -> None:
|
|
"""Export backend/.env tolerantly and bypass strict Settings parsing.
|
|
|
|
backend/.env carries keys the strict Settings model rejects; exporting
|
|
them ourselves and pointing ENVIRONMENT away from "local" lets the
|
|
legacy HubspotClient (and PostgresConfig.from_env) run unchanged.
|
|
"""
|
|
env_file = Path(__file__).parents[1] / "backend" / ".env"
|
|
for key, value in dotenv_values(env_file).items():
|
|
if value is not None:
|
|
os.environ.setdefault(key.upper(), value)
|
|
os.environ["ENVIRONMENT"] = "smoke-test"
|
|
|
|
|
|
class _CannedLogJobResponse:
|
|
content = (
|
|
f'<Root><Jobs><Job_logged job_no="{FAKE_JOB_NO}" '
|
|
f'logged_info="Job logged. Appointment booked." /></Jobs></Root>'
|
|
).encode()
|
|
|
|
def raise_for_status(self) -> None:
|
|
pass
|
|
|
|
|
|
class _StubAbriSession:
|
|
"""Stands in for requests.Session; records envelopes, returns the canned
|
|
LogJob success. Anything but a logjob request is a wiring bug."""
|
|
|
|
def __init__(self) -> None:
|
|
self.sent_envelopes: List[bytes] = []
|
|
|
|
def post(self, url: str, data: bytes) -> _CannedLogJobResponse:
|
|
request_type = (
|
|
ET.fromstring(data).find("Body/Request").get("request_type") # type: ignore[reportOptionalMemberAccess]
|
|
)
|
|
if request_type != "logjob":
|
|
raise AssertionError(
|
|
f"stub received a {request_type!r} request; only logjob is smoke-tested"
|
|
)
|
|
self.sent_envelopes.append(data)
|
|
return _CannedLogJobResponse()
|
|
|
|
|
|
def _print_sent_parameters(envelope: bytes) -> None:
|
|
parameters = {
|
|
parameter.get("attribute"): parameter.get("attribute_value")
|
|
for parameter in ET.fromstring(envelope).findall("Body/Request/Parameters")
|
|
}
|
|
print(" Envelope sent to (stubbed) Abri relay:")
|
|
for name, value in parameters.items():
|
|
print(f" {name} = {value}")
|
|
|
|
|
|
def main() -> None:
|
|
if DEAL_ID == "EDIT-ME":
|
|
raise SystemExit("Edit DEAL_ID before running")
|
|
|
|
_bootstrap_env()
|
|
|
|
# Imports that read settings/env must come after the bootstrap.
|
|
from hubspot.client import Client # type: ignore[reportMissingTypeStubs]
|
|
from sqlalchemy.pool import NullPool
|
|
from sqlmodel import Session, select
|
|
|
|
from applications.abri.abri_trigger_request import AbriTriggerRequest
|
|
from applications.abri.dispatch import dispatch_abri_flows
|
|
from backend.app.db.models.hubspot_deal_data import HubspotDealData
|
|
from etl.hubspot.abri_flow_triggers import build_abri_trigger_message
|
|
from etl.hubspot.hubspotClient import HubspotClient
|
|
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 infrastructure.postgres.config import PostgresConfig
|
|
from infrastructure.postgres.engine import make_engine
|
|
from orchestration.abri_orchestrator import AbriOrchestrator
|
|
from repositories.hubspot_deals.deal_database_postgres_gateway import (
|
|
DealDatabasePostgresGateway,
|
|
)
|
|
|
|
engine = make_engine(PostgresConfig.from_env(dict(os.environ)), poolclass=NullPool)
|
|
sdk_client: Client = Client.create( # type: ignore[reportUnknownMemberType]
|
|
access_token=os.environ["HUBSPOT_API_KEY"]
|
|
)
|
|
deal_properties = HubspotDealPropertiesClient(sdk_client=sdk_client)
|
|
|
|
with Session(engine) as session:
|
|
deal_row = session.exec(
|
|
select(HubspotDealData).where(HubspotDealData.deal_id == DEAL_ID)
|
|
).first()
|
|
if deal_row is None:
|
|
raise SystemExit(
|
|
f"Deal {DEAL_ID} has no hubspot_deal_data row — let the scraper "
|
|
"ingest it first (the gateway write-back needs the row)."
|
|
)
|
|
|
|
if CLEANUP:
|
|
deal_properties.write_deal_property(
|
|
deal_id=DEAL_ID, property_name=JOB_NO_DEAL_PROPERTY, value=""
|
|
)
|
|
deal_row.job_no = None
|
|
session.add(deal_row)
|
|
session.commit()
|
|
print(
|
|
f"Cleaned up: cleared {JOB_NO_DEAL_PROPERTY} in HubSpot and "
|
|
"job_no in the database. Unset the survey date in the UI too."
|
|
)
|
|
return
|
|
|
|
if RESET_DB_ROW_FIRST:
|
|
deal_row.confirmed_survey_date = None
|
|
deal_row.job_no = None
|
|
session.add(deal_row)
|
|
session.commit()
|
|
session.refresh(deal_row)
|
|
print("Reset DB row: confirmed_survey_date and job_no nulled.")
|
|
|
|
print(f"Fetching deal {DEAL_ID} from HubSpot...")
|
|
hubspot_deal, _company, listing, project = HubspotClient(
|
|
).get_deal_and_company_and_listing_and_project(DEAL_ID)
|
|
print(
|
|
f" dealname={hubspot_deal.get('dealname')!r} "
|
|
f"project_code={hubspot_deal.get('project_code')!r} "
|
|
f"confirmed_survey_date={hubspot_deal.get('confirmed_survey_date')!r} "
|
|
f"confirmed_survey_time={hubspot_deal.get('confirmed_survey_time')!r}"
|
|
)
|
|
print(
|
|
f" DB row: confirmed_survey_date={deal_row.confirmed_survey_date} "
|
|
f"job_no={deal_row.job_no}"
|
|
)
|
|
|
|
message = build_abri_trigger_message(
|
|
hubspot_deal_id=DEAL_ID,
|
|
new_deal=hubspot_deal,
|
|
new_project=project,
|
|
new_listing=listing,
|
|
old_deal=deal_row,
|
|
)
|
|
if message is None:
|
|
raise SystemExit(
|
|
"No Abri trigger fired. For LogJob, set the CONFIRMED SURVEY "
|
|
"DATE on the deal (expected commencement date fires the "
|
|
"tenant-data flow, not job logging). If the dev scraper "
|
|
"already synced your change, set RESET_DB_ROW_FIRST = True."
|
|
)
|
|
print(f"Trigger message built: flows={message['flows']}")
|
|
|
|
request = AbriTriggerRequest.model_validate(message)
|
|
if "log_job" not in request.flows:
|
|
raise SystemExit(
|
|
f"Fired flows {request.flows} do not include log_job — set the "
|
|
"confirmed survey date (not the expected commencement date)."
|
|
)
|
|
if request.flows != ["log_job"]:
|
|
skipped = [flow for flow in request.flows if flow != "log_job"]
|
|
print(f" Restricting to log_job for this smoke; skipping {skipped}.")
|
|
request = request.model_copy(update={"flows": ["log_job"]})
|
|
|
|
stub_session = _StubAbriSession()
|
|
abri_client = AbriClient(
|
|
config=AbriConfig(
|
|
endpoint_url="https://stubbed.invalid/relay",
|
|
username="smoke-test",
|
|
password="",
|
|
default_resource="SMOKE",
|
|
)
|
|
)
|
|
abri_client._session = cast( # pyright: ignore[reportPrivateUsage]
|
|
Any, stub_session
|
|
)
|
|
gateway = DealDatabasePostgresGateway(session=session)
|
|
orchestrator = AbriOrchestrator(
|
|
abri_client=abri_client,
|
|
deal_contacts=HubspotDealContactsClient(sdk_client=sdk_client),
|
|
deal_properties=deal_properties,
|
|
deal_database=gateway,
|
|
)
|
|
|
|
print("Dispatching...")
|
|
summary = dispatch_abri_flows(request, orchestrator, gateway)
|
|
for envelope in stub_session.sent_envelopes:
|
|
_print_sent_parameters(envelope)
|
|
print(f"Dispatch summary: {summary}")
|
|
|
|
# Read both write-backs back through the real interfaces.
|
|
deal = sdk_client.crm.deals.basic_api.get_by_id( # type: ignore[reportUnknownMemberType]
|
|
DEAL_ID, properties=[JOB_NO_DEAL_PROPERTY]
|
|
)
|
|
hubspot_value = cast(
|
|
Optional[str],
|
|
cast(Any, deal).properties.get(JOB_NO_DEAL_PROPERTY),
|
|
)
|
|
print(f"HubSpot {JOB_NO_DEAL_PROPERTY} is now: {hubspot_value!r}")
|
|
print(f"Database job_no is now: {gateway.job_no_for_deal(DEAL_ID)!r}")
|
|
print(
|
|
"Re-run to see the idempotency guard skip the log; "
|
|
"set CLEANUP = True to undo."
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|