""" E2E smoke test: CRTC order form for BC and ON provinces. Walks through the full order form (Steps 1–5) for both British Columbia and Ontario, takes screenshots at every step, and stops before payment. Verifies: - Province selection (BC / ON) and province-specific UI updates - Company type selection + conditional fields - Director information with province-aware address handling - Telecom details including AMB location loading per province - Identity verification skip (test mode) - Review & submit → order creation in PostgreSQL Usage: # Both provinces (default): python -m scripts.tests.e2e_crtc_provinces # Single province: python -m scripts.tests.e2e_crtc_provinces --province BC python -m scripts.tests.e2e_crtc_provinces --province ON # Headed mode (visible browser): python -m scripts.tests.e2e_crtc_provinces --headed """ from __future__ import annotations import argparse import json import logging import os import sys import time import uuid from datetime import datetime from pathlib import Path from dotenv import load_dotenv env_path = Path(__file__).parent / ".env.test" if env_path.exists(): load_dotenv(env_path) from playwright.sync_api import sync_playwright, Page, expect LOG = logging.getLogger("tests.e2e_provinces") logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s %(message)s", stream=sys.stdout, ) SITE_URL = os.environ.get("SITE_URL", "https://dev.performancewest.net") API_URL = os.environ.get("API_URL", "https://api.performancewest.net") SCREENSHOT_DIR = Path(__file__).parent / "screenshots" / "provinces" SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True) def screenshot(page: Page, name: str) -> Path: """Take a full-page screenshot and return the path.""" ts = datetime.now().strftime("%H%M%S") path = SCREENSHOT_DIR / f"{name}_{ts}.png" try: page.screenshot(path=str(path), full_page=True, timeout=15000) LOG.info(" Screenshot: %s", path.name) except Exception as e: LOG.warning(" Screenshot failed: %s", e) return path def make_test_data(province: str) -> dict: """Generate test data tailored to a province.""" uid = uuid.uuid4().hex[:6] is_bc = province == "BC" return { "province": province, # Customer "customer_name": f"Test {province} Carrier Corp", "customer_email": f"testcarrier+{uid}@performancewest.net", "customer_phone": "+13075559876", # Company — numbered for BC, named for ON (tests name reservation flow) "company_type": "numbered" if is_bc else "named", "trade_name": "", "legal_ending": "Ltd.", "name_choice_1": "" if is_bc else "Maple Telecom Solutions Ltd.", "name_choice_2": "" if is_bc else "Northern Voice Networks Inc.", "name_choice_3": "" if is_bc else "Lakeside Communications Corp.", # Director "director_first_name": "Jane" if is_bc else "Michael", "director_middle_name": "A" if is_bc else "", "director_last_name": "TestBC" if is_bc else "TestON", "director_country": "CA", "director_street": "1055 West Georgia St" if is_bc else "100 King Street West", "director_street2": "Suite 200", "director_city": "Vancouver" if is_bc else "Toronto", "director_province": "BC" if is_bc else "ON", "director_postal": "V6E 3P3" if is_bc else "M5X 1A9", "director_citizenship": "Canada", # Telecom "service_description": ( "VoIP reseller services — hosted PBX, SIP trunking, and wholesale " "voice termination for North American businesses." ), "geographic_coverage": f"{province} and Worldwide", "include_bits": True, "reg_contact_name": "Jane TestBC" if is_bc else "Michael TestON", "reg_contact_email": f"testcarrier+{uid}@performancewest.net", "reg_contact_phone": "+13075559876", } # ────────────────────────────────────────────────────────────────────── # Steps # ────────────────────────────────────────────────────────────────────── def step_load_form(page: Page, data: dict): """Load the CRTC order form in test mode.""" LOG.info("[%s] Step 0: Loading order form…", data["province"]) page.goto(f"{SITE_URL}/order/canada-crtc?test_mode=1", wait_until="domcontentloaded", timeout=30000) page.wait_for_selector("#crtc-form", timeout=15000) page.wait_for_timeout(1000) screenshot(page, f"{data['province']}_00_form_loaded") LOG.info("[%s] Order form loaded", data["province"]) def step_select_province(page: Page, data: dict): """Select incorporation province (BC or ON) and verify UI updates. The province selector may or may not be deployed. If present, select the target province. If absent, BC is the hardcoded default. """ prov = data["province"] LOG.info("[%s] Step 0.5: Selecting province…", prov) if prov != "BC": # Click the province label for ON (radio is sr-only) label = page.locator('.province-radio').filter(has_text="Ontario") label.scroll_into_view_if_needed(timeout=5000) label.click(force=True, timeout=5000) page.wait_for_timeout(1000) # wait for province change JS # Verify province is selected checked_val = page.evaluate( '(() => { const el = document.querySelector(\'input[name="incorporation_province"]:checked\'); return el ? el.value : null; })()' ) LOG.info("[%s] Checked province radio: %s", prov, checked_val) assert checked_val == prov, f"Province radio is '{checked_val}', expected '{prov}'" screenshot(page, f"{prov}_01_province_selected") LOG.info("[%s] Province step completed", prov) def step_company_type(page: Page, data: dict): """Step 1: Select company type and fill conditional fields.""" prov = data["province"] ctype = data["company_type"] LOG.info("[%s] Step 1: Company type = %s", prov, ctype) label = page.locator(f'label:has(input[name="company_type"][value="{ctype}"])') label.scroll_into_view_if_needed(timeout=5000) page.wait_for_timeout(300) label.click(timeout=5000, force=True) page.wait_for_timeout(500) if ctype == "numbered_tradename": page.wait_for_selector("#trade-name-fields", state="visible", timeout=3000) page.fill('input[name="trade_name"]', data["trade_name"]) LOG.info("[%s] Filled trade name: %s", prov, data["trade_name"]) elif ctype == "named": page.wait_for_selector("#named-fields", state="visible", timeout=3000) page.fill('input[name="name_choice_1"]', data.get("name_choice_1", "")) page.fill('input[name="name_choice_2"]', data.get("name_choice_2", "")) page.fill('input[name="name_choice_3"]', data.get("name_choice_3", "")) page.select_option('select[name="legal_ending"]', data.get("legal_ending", "Ltd.")) LOG.info("[%s] Filled name choices: %s / %s / %s", prov, data.get("name_choice_1"), data.get("name_choice_2"), data.get("name_choice_3")) screenshot(page, f"{prov}_02_company_type_{ctype}") # Click Next page.locator('button:has-text("Next")').last.scroll_into_view_if_needed() page.locator('button:has-text("Next")').last.click(timeout=5000, force=True) page.wait_for_timeout(500) LOG.info("[%s] Company type step completed", prov) def step_director_info(page: Page, data: dict): """Step 2: Fill director information.""" prov = data["province"] LOG.info("[%s] Step 2: Director information", prov) page.fill('input[name="director_first_name"]', data["director_first_name"]) if data.get("director_middle_name"): page.fill('input[name="director_middle_name"]', data["director_middle_name"]) page.fill('input[name="director_last_name"]', data["director_last_name"]) # Select country → triggers address fields page.select_option('select[name="director_country"]', data["director_country"]) page.wait_for_selector('#director_address_fields:not(.hidden)', timeout=5000) page.wait_for_timeout(500) page.fill('input[name="director_street"]', data["director_street"]) if data.get("director_street2"): page.fill('input[name="director_street2"]', data["director_street2"]) page.fill('input[name="director_city"]', data["director_city"]) page.fill('input[name="director_postal"]', data["director_postal"]) # Province — custom dropdown/text + hidden field prov_select = page.locator('#director_province_select') prov_text = page.locator('#director_province_text') if prov_select.is_visible(): prov_select.select_option(data["director_province"]) prov_select.dispatch_event("change") elif prov_text.is_visible(): prov_text.fill(data["director_province"]) page.evaluate( "document.getElementById('director_province').value = arguments[0]", data["director_province"], ) else: page.evaluate( "document.getElementById('director_province').value = arguments[0]", data["director_province"], ) # Set hidden director_name field full_name = f"{data['director_first_name']} {data.get('director_middle_name', '')} {data['director_last_name']}".replace(" ", " ").strip() page.evaluate(f"document.getElementById('director_name').value = '{full_name}'") if data.get("director_citizenship"): cit = page.locator('select[name="director_citizenship"]') cit.scroll_into_view_if_needed() cit.select_option(data["director_citizenship"]) screenshot(page, f"{prov}_03_director_info") page.locator('button:has-text("Next")').last.scroll_into_view_if_needed() page.locator('button:has-text("Next")').last.click(timeout=5000, force=True) page.wait_for_timeout(500) LOG.info("[%s] Director info completed", prov) def step_telecom_details(page: Page, data: dict): """Step 3: Fill telecom service details and verify AMB locations loaded.""" prov = data["province"] LOG.info("[%s] Step 3: Telecom details", prov) page.wait_for_selector('textarea[name="service_description"]:visible', timeout=5000) page.fill('textarea[name="service_description"]', data["service_description"]) page.fill('input[name="geographic_coverage"]', data["geographic_coverage"]) if data.get("include_bits"): bits_cb = page.locator('#include_bits') if bits_cb.count() > 0 and not bits_cb.is_checked(): bits_cb.check() # Verify AMB locations loaded for the correct province page.wait_for_timeout(2000) # wait for AMB locations to load amb_radios = page.locator('input[name="amb_location"]') amb_count = amb_radios.count() LOG.info("[%s] AMB locations loaded: %d", prov, amb_count) # Screenshot the AMB location section screenshot(page, f"{prov}_04a_amb_locations") # Fill regulatory contact page.fill('input[name="reg_contact_name"]', data["reg_contact_name"]) page.fill('input[name="reg_contact_email"]', data["reg_contact_email"]) page.fill('input[name="reg_contact_phone"]', data["reg_contact_phone"]) screenshot(page, f"{prov}_04b_telecom_details") page.locator('button:has-text("Next")').last.scroll_into_view_if_needed() page.locator('button:has-text("Next")').last.click(timeout=5000, force=True) page.wait_for_timeout(500) LOG.info("[%s] Telecom details completed", prov) def step_identity_verification(page: Page, data: dict): """Step 4: Skip identity verification (test mode bypass).""" prov = data["province"] LOG.info("[%s] Step 4: Identity verification (skipping in test mode)", prov) screenshot(page, f"{prov}_05_identity_step") # Use the global #btn-next button (shared across all steps) with force click next_btn = page.locator('#btn-next') next_btn.click(timeout=5000, force=True) page.wait_for_timeout(500) LOG.info("[%s] Identity step skipped", prov) def step_review_and_submit(page: Page, data: dict): """Step 5: Review, fill contact info, consent, and submit.""" prov = data["province"] LOG.info("[%s] Step 5: Review & submit", prov) page.wait_for_selector('#step-5:not(.hidden)', timeout=5000) page.wait_for_timeout(500) # Screenshot the review summary before filling contact screenshot(page, f"{prov}_06a_review_summary") # Fill contact info page.locator('#customer_name').scroll_into_view_if_needed(timeout=5000) page.locator('#customer_name').fill(data["customer_name"]) page.locator('#customer_email').fill(data["customer_email"]) if data.get("customer_phone"): page.locator('#customer_phone').fill(data["customer_phone"]) # Accept disclaimer (not legal advice) disclaimer = page.locator('#disclaimer') if disclaimer.count() > 0: disclaimer.scroll_into_view_if_needed() if not disclaimer.is_checked(): disclaimer.check() page.wait_for_timeout(200) # Accept consent consent = page.locator('#consent') consent.wait_for(state="visible", timeout=5000) if not consent.is_checked(): consent.scroll_into_view_if_needed() consent.check() page.wait_for_timeout(300) screenshot(page, f"{prov}_06b_ready_to_submit") # Block the checkout session API call so we don't redirect to Stripe payment. # The form's submitOrder() creates the order first, then calls create-session. # By blocking create-session, the code falls through to showSuccess(orderNumber). page.route("**/api/v1/checkout/create-session", lambda route: route.fulfill( status=503, content_type="application/json", body='{"error":"blocked by test — skipping payment"}' )) # Submit submit_btn = page.locator('#btn-next') submit_btn.scroll_into_view_if_needed(timeout=5000) LOG.info("[%s] Clicking Submit Order", prov) submit_btn.click(timeout=10000) page.wait_for_timeout(5000) # Check for errors submit_status = page.locator('#submit-status') if submit_status.is_visible(): error_text = submit_status.text_content() LOG.info("[%s] Submit status: %s", prov, error_text) if error_text and ("error" in error_text.lower() or "wrong" in error_text.lower()): screenshot(page, f"{prov}_06c_submit_error") raise AssertionError(f"[{prov}] Order submission failed: {error_text}") screenshot(page, f"{prov}_06c_after_submit") # Wait for success screen (shown when checkout session fails, which is our intent) page.wait_for_selector("#step-success:not(.hidden)", timeout=30000) page.wait_for_timeout(500) # Capture order number order_number_el = page.locator("#success-order-number") order_number = order_number_el.text_content(timeout=5000).strip() if not order_number or order_number == "--": screenshot(page, f"{prov}_06d_no_order_number") raise AssertionError(f"[{prov}] Order number not populated in step-success") data["order_number"] = order_number screenshot(page, f"{prov}_07_order_success") LOG.info("[%s] Order submitted — order number: %s", prov, order_number) import requests # Dev DB is exposed on port 5433 on the server — always use the external URL, # not the Docker-internal DATABASE_URL from .env.test DEV_DB_URL = os.environ.get("DEV_DATABASE_URL", "postgresql://pw:pw_dev_2026@207.174.124.71:5433/performancewest") ERP_URL = os.environ.get("ERPNEXT_URL", "http://207.174.124.71:8080") ERP_KEY = os.environ.get("ERPNEXT_API_KEY", "") ERP_SECRET = os.environ.get("ERPNEXT_API_SECRET", "") ERP_SITE = os.environ.get("ERPNEXT_SITE_NAME", "performancewest.net") def _get_db(): """Get a psycopg2 connection to the dev DB.""" import psycopg2 import psycopg2.extras return psycopg2.connect(DEV_DB_URL) def _erp_headers(): return { "Authorization": f"token {ERP_KEY}:{ERP_SECRET}", "X-Frappe-Site-Name": ERP_SITE, "Content-Type": "application/json", } # ────────────────────────────────────────────────────────────────────── # Phase 2: PostgreSQL verification # ────────────────────────────────────────────────────────────────────── def phase_verify_pg(data: dict) -> dict | None: """Verify the order exists in PostgreSQL with correct fields.""" prov = data["province"] order_number = data.get("order_number") LOG.info("[%s] Phase 2: PostgreSQL verification — %s", prov, order_number) try: import psycopg2.extras conn = _get_db() cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) cur.execute("SELECT * FROM canada_crtc_orders WHERE order_number = %s", (order_number,)) order = cur.fetchone() conn.close() except Exception as e: LOG.warning("[%s] DB connection failed: %s", prov, e) return None if not order: LOG.error("[%s] Order %s NOT FOUND in DB", prov, order_number) return None checks = [] def check(label, ok, detail=""): status = "PASS" if ok else "FAIL" checks.append({"label": label, "ok": ok, "detail": detail}) LOG.info("[%s] %s: %s%s", prov, status, label, f" — {detail}" if detail and not ok else "") check("customer_name", order.get("customer_name") == data["customer_name"], order.get("customer_name")) check("customer_email", order.get("customer_email") == data["customer_email"], order.get("customer_email")) check("company_type", order.get("company_type") == data["company_type"], f"got={order.get('company_type')}, want={data['company_type']}") check("incorporation_province", order.get("incorporation_province") == prov, f"got={order.get('incorporation_province')}") check("director_name set", bool(order.get("director_name")), order.get("director_name")) check("services_description set", bool(order.get("services_description"))) check("payment_status = pending_payment", order.get("payment_status") == "pending_payment", order.get("payment_status")) # stripe_session_id and erpnext_sales_order are empty because we block checkout redirect # in test mode — this is expected, not a failure check("service_fee_cents > 0", (order.get("service_fee_cents") or 0) > 0, str(order.get("service_fee_cents"))) check("total_cents > 0", (order.get("total_cents") or 0) > 0, f"${(order.get('total_cents') or 0) / 100:.2f}") check("amb_location_slug set", bool(order.get("amb_location_slug")), order.get("amb_location_slug")) check("funds_available = false", order.get("funds_available") is False) if data["company_type"] == "named": check("name_choice_1 set", bool(order.get("company_name_choice1")), order.get("company_name_choice1")) if data["company_type"] == "numbered_tradename": check("trade_name set", bool(order.get("trade_name")), order.get("trade_name")) passed = sum(1 for c in checks if c["ok"]) LOG.info("[%s] PG checks: %d/%d passed", prov, passed, len(checks)) data["pg_order"] = dict(order) data["erpnext_so"] = order.get("erpnext_sales_order") return {"checks": checks, "passed": passed, "total": len(checks)} # ────────────────────────────────────────────────────────────────────── # Phase 3: ERPNext Sales Order verification # ────────────────────────────────────────────────────────────────────── def phase_verify_erpnext(data: dict) -> dict | None: """Verify ERPNext Sales Order was created.""" prov = data["province"] so_name = data.get("erpnext_so") LOG.info("[%s] Phase 3: ERPNext verification — SO %s", prov, so_name) if not ERP_KEY or not ERP_SECRET: LOG.warning("[%s] ERPNEXT_API_KEY/SECRET not set — skipping", prov) return None if not so_name: LOG.warning("[%s] No erpnext_sales_order in PG — skipping", prov) return None checks = [] def check(label, ok, detail=""): checks.append({"label": label, "ok": ok}) LOG.info("[%s] %s: %s%s", prov, "PASS" if ok else "FAIL", label, f" — {detail}" if detail and not ok else "") try: r = requests.get(f"{ERP_URL}/api/resource/Sales Order/{so_name}", headers=_erp_headers(), timeout=10) if not r.ok: check("Sales Order exists", False, f"HTTP {r.status_code}") return {"checks": checks, "passed": 0, "total": 1} so = r.json().get("data", {}) check("Sales Order exists", True) check("workflow_state = Received", so.get("workflow_state") == "Received", so.get("workflow_state")) check("has line items", len(so.get("items", [])) > 0, str(len(so.get("items", [])))) check("grand_total > 0", (so.get("grand_total") or 0) > 0, so.get("grand_total")) check("external_order_id matches", so.get("custom_external_order_id") == data["order_number"], so.get("custom_external_order_id")) data["erp_so_data"] = so except Exception as e: check("ERPNext reachable", False, str(e)) passed = sum(1 for c in checks if c["ok"]) LOG.info("[%s] ERPNext checks: %d/%d passed", prov, passed, len(checks)) return {"checks": checks, "passed": passed, "total": len(checks)} # ────────────────────────────────────────────────────────────────────── # Phase 4: Simulate payment completion # ────────────────────────────────────────────────────────────────────── def phase_simulate_payment(data: dict) -> dict | None: """Simulate payment completion: mark paid in PG + advance ERPNext.""" prov = data["province"] order_number = data["order_number"] so_name = data.get("erpnext_so") LOG.info("[%s] Phase 4: Simulate payment — %s", prov, order_number) checks = [] def check(label, ok, detail=""): checks.append({"label": label, "ok": ok}) LOG.info("[%s] %s: %s%s", prov, "PASS" if ok else "FAIL", label, f" — {detail}" if detail and not ok else "") # Mark paid in PG try: conn = _get_db() cur = conn.cursor() cur.execute( "UPDATE canada_crtc_orders SET payment_status = 'paid', paid_at = NOW() " "WHERE order_number = %s RETURNING payment_status", (order_number,), ) row = cur.fetchone() conn.commit() conn.close() check("PG payment_status → paid", row is not None and row[0] == "paid") except Exception as e: check("PG payment update", False, str(e)) # Advance ERPNext to Awaiting Funds if so_name and ERP_KEY: try: r = requests.put( f"{ERP_URL}/api/resource/Sales Order/{so_name}", headers=_erp_headers(), json={"workflow_state": "Awaiting Funds"}, timeout=10, ) check("ERPNext → Awaiting Funds", r.ok, f"HTTP {r.status_code}") except Exception as e: check("ERPNext advance", False, str(e)) passed = sum(1 for c in checks if c["ok"]) LOG.info("[%s] Payment sim checks: %d/%d passed", prov, passed, len(checks)) return {"checks": checks, "passed": passed, "total": len(checks)} # ────────────────────────────────────────────────────────────────────── # Phase 5: Simulate funds available → Client Selection # ────────────────────────────────────────────────────────────────────── def phase_simulate_funds_available(data: dict) -> dict | None: """Simulate balance.available → advance to Client Selection.""" prov = data["province"] order_number = data["order_number"] so_name = data.get("erpnext_so") LOG.info("[%s] Phase 5: Simulate funds available — %s", prov, order_number) checks = [] def check(label, ok, detail=""): checks.append({"label": label, "ok": ok}) LOG.info("[%s] %s: %s%s", prov, "PASS" if ok else "FAIL", label, f" — {detail}" if detail and not ok else "") # Set funds_available in PG try: conn = _get_db() cur = conn.cursor() cur.execute( "UPDATE canada_crtc_orders SET funds_available = TRUE, funds_available_at = NOW() " "WHERE order_number = %s RETURNING funds_available", (order_number,), ) row = cur.fetchone() conn.commit() conn.close() check("PG funds_available → TRUE", row is not None and row[0] is True) except Exception as e: check("PG funds_available update", False, str(e)) # Advance ERPNext to Client Selection if so_name and ERP_KEY: try: r = requests.put( f"{ERP_URL}/api/resource/Sales Order/{so_name}", headers=_erp_headers(), json={"workflow_state": "Client Selection"}, timeout=10, ) check("ERPNext → Client Selection", r.ok, f"HTTP {r.status_code}") except Exception as e: check("ERPNext advance", False, str(e)) passed = sum(1 for c in checks if c["ok"]) LOG.info("[%s] Funds available checks: %d/%d passed", prov, passed, len(checks)) return {"checks": checks, "passed": passed, "total": len(checks)} # ────────────────────────────────────────────────────────────────────── # Phase 6: Portal setup page + API # ────────────────────────────────────────────────────────────────────── def phase_portal_setup(page: Page, data: dict) -> dict | None: """Test portal setup page loads and API endpoints enforce auth.""" prov = data["province"] order_number = data["order_number"] LOG.info("[%s] Phase 6: Portal setup — %s", prov, order_number) checks = [] def check(label, ok, detail=""): checks.append({"label": label, "ok": ok}) LOG.info("[%s] %s: %s%s", prov, "PASS" if ok else "FAIL", label, f" — {detail}" if detail and not ok else "") # Test portal page loads page.goto(f"{SITE_URL}/portal/setup?order={order_number}", wait_until="domcontentloaded", timeout=15000) page.wait_for_timeout(1000) screenshot(page, f"{prov}_08_portal_setup") check("Portal page loads (200)", "portal/setup" in page.url) # The portal page renders the Carrier Setup shell even without auth. # Without a JWT, it shows "Could not load your order details" — that's expected. has_setup_heading = page.locator('text=Carrier Setup').count() > 0 check("Portal has 'Carrier Setup' heading", has_setup_heading) screenshot(page, f"{prov}_08b_portal_detail") # Test API auth enforcement — these should return 401 without JWT try: r = requests.get(f"{SITE_URL}/api/v1/portal/setup-info", params={"order_id": order_number}, timeout=10) check("setup-info requires auth (401)", r.status_code == 401, f"got {r.status_code}") except Exception as e: check("setup-info reachable", False, str(e)) try: r = requests.get(f"{SITE_URL}/api/v1/portal/setup-dids", timeout=10) check("setup-dids requires auth (401)", r.status_code == 401, f"got {r.status_code}") except Exception as e: check("setup-dids reachable", False, str(e)) try: r = requests.post(f"{SITE_URL}/api/v1/portal/setup-confirm", json={"order_id": order_number}, timeout=10) check("setup-confirm requires auth (401)", r.status_code == 401, f"got {r.status_code}") except Exception as e: check("setup-confirm reachable", False, str(e)) passed = sum(1 for c in checks if c["ok"]) LOG.info("[%s] Portal checks: %d/%d passed", prov, passed, len(checks)) return {"checks": checks, "passed": passed, "total": len(checks)} # ────────────────────────────────────────────────────────────────────── # Phase 7: Simulate client portal selections # ────────────────────────────────────────────────────────────────────── def phase_simulate_client_selection(data: dict) -> dict | None: """Simulate client selecting mailbox unit + DID in portal.""" prov = data["province"] order_number = data["order_number"] LOG.info("[%s] Phase 7: Simulate client selection — %s", prov, order_number) checks = [] def check(label, ok, detail=""): checks.append({"label": label, "ok": ok}) LOG.info("[%s] %s: %s%s", prov, "PASS" if ok else "FAIL", label, f" — {detail}" if detail and not ok else "") test_unit = f"TEST-{prov}-001" test_did = "16045551234" if prov == "BC" else "14165551234" try: conn = _get_db() cur = conn.cursor() cur.execute( "UPDATE canada_crtc_orders SET client_selected_unit = %s, client_selected_did = %s " "WHERE order_number = %s RETURNING client_selected_unit, client_selected_did", (test_unit, test_did, order_number), ) row = cur.fetchone() conn.commit() conn.close() check("client_selected_unit stored", row and row[0] == test_unit, str(row)) check("client_selected_did stored", row and row[1] == test_did, str(row)) except Exception as e: check("PG client selection", False, str(e)) passed = sum(1 for c in checks if c["ok"]) LOG.info("[%s] Client selection checks: %d/%d passed", prov, passed, len(checks)) return {"checks": checks, "passed": passed, "total": len(checks)} # ────────────────────────────────────────────────────────────────────── # Phase 8: AMB Locations API # ────────────────────────────────────────────────────────────────────── def phase_amb_api(data: dict) -> dict | None: """Verify AMB locations API returns data for the province.""" prov = data["province"] LOG.info("[%s] Phase 8: AMB Locations API", prov) checks = [] def check(label, ok, detail=""): checks.append({"label": label, "ok": ok}) LOG.info("[%s] %s: %s%s", prov, "PASS" if ok else "FAIL", label, f" — {detail}" if detail and not ok else "") try: r = requests.get(f"{SITE_URL}/api/v1/amb/locations", params={"province": prov}, timeout=10) check("AMB API returns 200", r.status_code == 200, f"got {r.status_code}") if r.ok: locs = r.json().get("locations", []) check(f"Has locations for {prov}", len(locs) > 0, f"count={len(locs)}") if locs: first = locs[0] check("Location has slug", bool(first.get("slug"))) check("Location has yearly_price_usd > 0", (first.get("yearly_price_usd") or 0) > 0, first.get("yearly_price_usd")) check("Location has correct province", first.get("province") == prov, first.get("province")) except Exception as e: check("AMB API reachable", False, str(e)) passed = sum(1 for c in checks if c["ok"]) LOG.info("[%s] AMB API checks: %d/%d passed", prov, passed, len(checks)) return {"checks": checks, "passed": passed, "total": len(checks)} # ────────────────────────────────────────────────────────────────────── # Phase 9: Final PG state check # ────────────────────────────────────────────────────────────────────── def phase_final_state(data: dict) -> dict | None: """Verify final PG state after all simulated phases.""" prov = data["province"] order_number = data["order_number"] LOG.info("[%s] Phase 9: Final state check — %s", prov, order_number) checks = [] def check(label, ok, detail=""): checks.append({"label": label, "ok": ok}) LOG.info("[%s] %s: %s%s", prov, "PASS" if ok else "FAIL", label, f" — {detail}" if detail and not ok else "") try: import psycopg2.extras conn = _get_db() cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) cur.execute("SELECT * FROM canada_crtc_orders WHERE order_number = %s", (order_number,)) order = cur.fetchone() conn.close() if not order: check("Order still exists", False) return {"checks": checks, "passed": 0, "total": 1} check("payment_status = paid", order["payment_status"] == "paid") check("funds_available = TRUE", order["funds_available"] is True) check("funds_available_at set", order.get("funds_available_at") is not None) check("client_selected_unit set", bool(order.get("client_selected_unit"))) check("client_selected_did set", bool(order.get("client_selected_did"))) check("incorporation_province correct", order.get("incorporation_province") == prov) LOG.info("[%s] Final state: paid=%s, funds=%s, unit=%s, did=%s, province=%s", prov, order["payment_status"], order["funds_available"], order.get("client_selected_unit"), order.get("client_selected_did"), order.get("incorporation_province")) except Exception as e: check("Final state query", False, str(e)) passed = sum(1 for c in checks if c["ok"]) LOG.info("[%s] Final state checks: %d/%d passed", prov, passed, len(checks)) return {"checks": checks, "passed": passed, "total": len(checks)} # ────────────────────────────────────────────────────────────────────── # Orchestrator # ────────────────────────────────────────────────────────────────────── def run_province_test(province: str, headless: bool = True) -> dict: """Run the full CRTC form flow for a single province, skipping payment.""" data = make_test_data(province) results = {"province": province, "steps": [], "order_number": None, "success": False} LOG.info("") LOG.info("=" * 60) LOG.info(" CRTC E2E TEST: %s (%s company)", province, data["company_type"]) LOG.info("=" * 60) with sync_playwright() as p: browser = p.chromium.launch( headless=headless, args=["--disable-blink-features=AutomationControlled"], ) context = browser.new_context( viewport={"width": 1440, "height": 900}, user_agent=( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" ), ) page = context.new_page() # Log browser console errors page.on("console", lambda msg: LOG.warning("[browser] %s: %s", msg.type, msg.text) if msg.type in ("error", "warning") else None) page.on("dialog", lambda d: (LOG.warning("[dialog] %s: %s", d.type, d.message), d.dismiss())) steps = [ ("Load form", step_load_form), ("Select province", step_select_province), ("Company type", step_company_type), ("Director info", step_director_info), ("Telecom details", step_telecom_details), ("Identity verification", step_identity_verification), ("Review & submit", step_review_and_submit), ] for step_name, step_fn in steps: t0 = time.time() try: step_fn(page, data) elapsed = time.time() - t0 results["steps"].append({"name": step_name, "status": "PASS", "time": round(elapsed, 2)}) LOG.info("[%s] ✓ %s (%.1fs)", province, step_name, elapsed) except Exception as exc: elapsed = time.time() - t0 results["steps"].append({"name": step_name, "status": "FAIL", "error": str(exc)[:200], "time": round(elapsed, 2)}) LOG.error("[%s] ✗ %s FAILED (%.1fs): %s", province, step_name, elapsed, exc) screenshot(page, f"{province}_ABORT_{step_name.replace(' ', '_')}") break # ── Post-order pipeline phases (run if order was created) ──────── if data.get("order_number"): results["order_number"] = data["order_number"] pipeline_phases = [ ("PG verification", lambda p, d: phase_verify_pg(d)), ("ERPNext verification", lambda p, d: phase_verify_erpnext(d)), ("Simulate payment", lambda p, d: phase_simulate_payment(d)), ("Simulate funds available", lambda p, d: phase_simulate_funds_available(d)), ("Portal setup", lambda p, d: phase_portal_setup(p, d)), ("Client selection", lambda p, d: phase_simulate_client_selection(d)), ("AMB API", lambda p, d: phase_amb_api(d)), ("Final state check", lambda p, d: phase_final_state(d)), ] for phase_name, phase_fn in pipeline_phases: t0 = time.time() try: result_data = phase_fn(page, data) elapsed = time.time() - t0 if result_data and result_data.get("total", 0) > 0: all_ok = result_data["passed"] == result_data["total"] detail = f"{result_data['passed']}/{result_data['total']} checks" results["steps"].append({ "name": phase_name, "status": "PASS" if all_ok else "WARN", "time": round(elapsed, 2), "detail": detail, }) icon = "✓" if all_ok else "⚠" LOG.info("[%s] %s %s (%s, %.1fs)", province, icon, phase_name, detail, elapsed) else: results["steps"].append({ "name": phase_name, "status": "SKIP", "time": round(elapsed, 2), }) LOG.info("[%s] ○ %s (skipped, %.1fs)", province, phase_name, elapsed) except Exception as exc: elapsed = time.time() - t0 results["steps"].append({ "name": phase_name, "status": "FAIL", "error": str(exc)[:200], "time": round(elapsed, 2), }) LOG.error("[%s] ✗ %s FAILED (%.1fs): %s", province, phase_name, elapsed, exc) browser.close() # WARN means some sub-checks failed but the phase ran — still counts as success all_passed = all(s["status"] in ("PASS", "SKIP", "WARN") for s in results["steps"]) results["success"] = all_passed return results def print_report(all_results: list[dict]): """Print a summary report of all province tests.""" LOG.info("") LOG.info("=" * 60) LOG.info(" PROVINCE E2E TEST REPORT") LOG.info("=" * 60) for r in all_results: status = "PASS" if r["success"] else "FAIL" LOG.info("") LOG.info(" [%s] %s — order %s", status, r["province"], r.get("order_number", "N/A")) for s in r["steps"]: icons = {"PASS": "✓", "FAIL": "✗", "WARN": "⚠", "SKIP": "○"} icon = icons.get(s["status"], "?") line = f" {icon} {s['name']} ({s['time']}s)" if s.get("detail"): line += f" [{s['detail']}]" if s.get("error"): line += f" — {s['error'][:80]}" LOG.info(line) LOG.info("") LOG.info(" Screenshots: %s", SCREENSHOT_DIR) total = sum(1 for r in all_results if r["success"]) LOG.info(" Result: %d/%d provinces passed", total, len(all_results)) LOG.info("=" * 60) # ────────────────────────────────────────────────────────────────────── # Scenario tests (additional company types / options) # ────────────────────────────────────────────────────────────────────── def run_scenario(label: str, data: dict, headless: bool = True) -> dict: """Run a single order form scenario (browser) and verify PG pricing.""" results = {"province": label, "steps": [], "order_number": None, "success": False} LOG.info("") LOG.info("=" * 60) LOG.info(" SCENARIO: %s", label) LOG.info("=" * 60) with sync_playwright() as p: browser = p.chromium.launch( headless=headless, args=["--disable-blink-features=AutomationControlled"], ) context = browser.new_context( viewport={"width": 1440, "height": 900}, user_agent=( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" ), ) page = context.new_page() page.on("console", lambda msg: LOG.warning("[browser] %s: %s", msg.type, msg.text) if msg.type in ("error", "warning") else None) page.on("dialog", lambda d: (LOG.warning("[dialog] %s: %s", d.type, d.message), d.dismiss())) steps = [ ("Load form", step_load_form), ("Select province", step_select_province), ("Company type", step_company_type), ("Director info", step_director_info), ] # Scenario-specific telecom step (handles own address, existing DID, etc.) if data.get("_custom_telecom_fn"): steps.append(("Telecom details", data["_custom_telecom_fn"])) else: steps.append(("Telecom details", step_telecom_details)) steps.extend([ ("Identity verification", step_identity_verification), ("Review & submit", step_review_and_submit), ]) for step_name, step_fn in steps: t0 = time.time() try: step_fn(page, data) elapsed = time.time() - t0 results["steps"].append({"name": step_name, "status": "PASS", "time": round(elapsed, 2)}) LOG.info("[%s] ✓ %s (%.1fs)", label, step_name, elapsed) except Exception as exc: elapsed = time.time() - t0 results["steps"].append({"name": step_name, "status": "FAIL", "error": str(exc)[:200], "time": round(elapsed, 2)}) LOG.error("[%s] ✗ %s FAILED (%.1fs): %s", label, step_name, elapsed, exc) screenshot(page, f"{label}_ABORT_{step_name.replace(' ', '_')}") break # PG pricing verification if data.get("order_number"): results["order_number"] = data["order_number"] t0 = time.time() try: pg_result = phase_verify_pg(data) elapsed = time.time() - t0 if pg_result: detail = f"{pg_result['passed']}/{pg_result['total']} checks" all_ok = pg_result["passed"] == pg_result["total"] results["steps"].append({"name": "PG verification", "status": "PASS" if all_ok else "WARN", "time": round(elapsed, 2), "detail": detail}) LOG.info("[%s] %s PG verification (%s, %.1fs)", label, "✓" if all_ok else "⚠", detail, elapsed) # Scenario-specific PG checks if data.get("_pg_checks"): data["_pg_checks"](data, results) except Exception as exc: results["steps"].append({"name": "PG verification", "status": "FAIL", "error": str(exc)[:200], "time": round(time.time() - t0, 2)}) browser.close() all_passed = all(s["status"] in ("PASS", "SKIP", "WARN") for s in results["steps"]) results["success"] = all_passed return results def step_telecom_own_address(page: Page, data: dict): """Step 3 variant: Fill telecom details + check own Canadian address.""" prov = data["province"] LOG.info("[%s] Step 3: Telecom details (own CA address)", prov) page.wait_for_selector('textarea[name="service_description"]:visible', timeout=5000) page.fill('textarea[name="service_description"]', data["service_description"]) page.fill('input[name="geographic_coverage"]', data["geographic_coverage"]) # Check "I have my own Canadian address" own_cb = page.locator('#own_ca_address') own_cb.scroll_into_view_if_needed() own_cb.check() page.wait_for_timeout(500) # Verify AMB section hidden, own-address-fields visible assert page.locator('#amb-location-section').is_hidden(), "AMB section should be hidden" assert page.locator('#own-address-fields').is_visible(), "Own address fields should be visible" # Fill own address fields page.fill('#own_ca_company', data.get("own_ca_company", "Test Corp Office")) page.fill('#own_ca_street', data.get("own_ca_street", "100 Test St")) page.fill('#own_ca_postal', data.get("own_ca_postal", "V6B 1A1")) # Check the BC Business Corporations Act confirmation confirm_cb = page.locator('#own_address_confirm') if confirm_cb.count() > 0: confirm_cb.scroll_into_view_if_needed() confirm_cb.check() screenshot(page, f"{prov}_04_own_address") # Fill regulatory contact page.fill('input[name="reg_contact_name"]', data["reg_contact_name"]) page.fill('input[name="reg_contact_email"]', data["reg_contact_email"]) page.fill('input[name="reg_contact_phone"]', data["reg_contact_phone"]) page.locator('button:has-text("Next")').last.scroll_into_view_if_needed() page.locator('button:has-text("Next")').last.click(timeout=5000, force=True) page.wait_for_timeout(500) LOG.info("[%s] Telecom details (own address) completed", prov) def step_telecom_with_expedited(page: Page, data: dict): """Step 3 variant: Fill telecom details + check expedited.""" prov = data["province"] LOG.info("[%s] Step 3: Telecom details (expedited)", prov) page.wait_for_selector('textarea[name="service_description"]:visible', timeout=5000) page.fill('textarea[name="service_description"]', data["service_description"]) page.fill('input[name="geographic_coverage"]', data["geographic_coverage"]) page.fill('input[name="reg_contact_name"]', data["reg_contact_name"]) page.fill('input[name="reg_contact_email"]', data["reg_contact_email"]) page.fill('input[name="reg_contact_phone"]', data["reg_contact_phone"]) # Wait for AMB locations page.wait_for_timeout(2000) screenshot(page, f"{prov}_04a_telecom_before_expedited") page.locator('button:has-text("Next")').last.scroll_into_view_if_needed() page.locator('button:has-text("Next")').last.click(timeout=5000, force=True) page.wait_for_timeout(500) LOG.info("[%s] Telecom details completed, expedited will be checked in review", prov) def step_telecom_with_discount(page: Page, data: dict): """Step 3 variant: standard telecom, discount applied in review step.""" # Same as normal telecom step step_telecom_details(page, data) def step_review_with_expedited(page: Page, data: dict): """Step 5 variant: check expedited checkbox before submitting.""" prov = data["province"] LOG.info("[%s] Step 5: Review & submit (expedited)", prov) page.wait_for_selector('#step-5:not(.hidden)', timeout=5000) page.wait_for_timeout(500) # Check expedited expedited_cb = page.locator('#expedited') if expedited_cb.count() > 0: expedited_cb.scroll_into_view_if_needed() expedited_cb.check() page.wait_for_timeout(500) # Verify expedited summary row visible exp_row = page.locator('#summary-expedited-row') if exp_row.count() > 0: assert not exp_row.is_hidden(), "Expedited summary row should be visible" screenshot(page, f"{prov}_06a_review_expedited") # Fill contact + consent page.locator('#customer_name').scroll_into_view_if_needed(timeout=5000) page.locator('#customer_name').fill(data["customer_name"]) page.locator('#customer_email').fill(data["customer_email"]) if data.get("customer_phone"): page.locator('#customer_phone').fill(data["customer_phone"]) disclaimer = page.locator('#disclaimer') if disclaimer.count() > 0 and not disclaimer.is_checked(): disclaimer.scroll_into_view_if_needed() disclaimer.check() page.wait_for_timeout(200) consent = page.locator('#consent') consent.wait_for(state="visible", timeout=5000) if not consent.is_checked(): consent.scroll_into_view_if_needed() consent.check() page.wait_for_timeout(300) screenshot(page, f"{prov}_06b_ready_expedited") # Block checkout + submit page.route("**/api/v1/checkout/create-session", lambda route: route.fulfill( status=503, content_type="application/json", body='{"error":"blocked by test"}' )) submit_btn = page.locator('#btn-next') submit_btn.scroll_into_view_if_needed(timeout=5000) submit_btn.click(timeout=10000) page.wait_for_timeout(5000) screenshot(page, f"{prov}_06c_after_submit_expedited") page.wait_for_selector("#step-success:not(.hidden)", timeout=30000) order_number_el = page.locator("#success-order-number") order_number = order_number_el.text_content(timeout=5000).strip() assert order_number and order_number != "--", "Order number not populated" data["order_number"] = order_number screenshot(page, f"{prov}_07_success_expedited") LOG.info("[%s] Expedited order submitted — %s", prov, order_number) def step_review_with_discount(page: Page, data: dict): """Step 5 variant: apply discount code before submitting.""" prov = data["province"] code = data.get("discount_code", "LAUNCH25") LOG.info("[%s] Step 5: Review & submit (discount %s)", prov, code) page.wait_for_selector('#step-5:not(.hidden)', timeout=5000) page.wait_for_timeout(500) # Apply discount code page.locator('#discount_code').scroll_into_view_if_needed() page.locator('#discount_code').fill(code) page.locator('#btn-apply-discount').click() page.wait_for_timeout(2000) # Verify discount applied (green status) discount_status = page.locator('#discount-status') if discount_status.is_visible(): status_text = discount_status.text_content() LOG.info("[%s] Discount status: %s", prov, status_text) # Verify discount row visible in summary discount_row = page.locator('#summary-discount-row') assert not discount_row.is_hidden(), "Discount summary row should be visible" screenshot(page, f"{prov}_06a_review_discount") # Fill contact + consent page.locator('#customer_name').scroll_into_view_if_needed(timeout=5000) page.locator('#customer_name').fill(data["customer_name"]) page.locator('#customer_email').fill(data["customer_email"]) if data.get("customer_phone"): page.locator('#customer_phone').fill(data["customer_phone"]) disclaimer = page.locator('#disclaimer') if disclaimer.count() > 0 and not disclaimer.is_checked(): disclaimer.scroll_into_view_if_needed() disclaimer.check() page.wait_for_timeout(200) consent = page.locator('#consent') consent.wait_for(state="visible", timeout=5000) if not consent.is_checked(): consent.scroll_into_view_if_needed() consent.check() page.wait_for_timeout(300) screenshot(page, f"{prov}_06b_ready_discount") page.route("**/api/v1/checkout/create-session", lambda route: route.fulfill( status=503, content_type="application/json", body='{"error":"blocked by test"}' )) submit_btn = page.locator('#btn-next') submit_btn.scroll_into_view_if_needed(timeout=5000) submit_btn.click(timeout=10000) page.wait_for_timeout(5000) screenshot(page, f"{prov}_06c_after_submit_discount") page.wait_for_selector("#step-success:not(.hidden)", timeout=30000) order_number_el = page.locator("#success-order-number") order_number = order_number_el.text_content(timeout=5000).strip() assert order_number and order_number != "--", "Order number not populated" data["order_number"] = order_number screenshot(page, f"{prov}_07_success_discount") LOG.info("[%s] Discount order submitted — %s", prov, order_number) def make_scenario_data(scenario: str) -> dict: """Create test data for a specific scenario.""" uid = uuid.uuid4().hex[:6] base = { "province": "BC", "customer_name": f"Scenario {scenario}", "customer_email": f"testcarrier+{uid}@performancewest.net", "customer_phone": "+13075559876", "director_first_name": "Scenario", "director_middle_name": "", "director_last_name": "Tester", "director_country": "CA", "director_street": "1055 West Georgia St", "director_street2": "", "director_city": "Vancouver", "director_province": "BC", "director_postal": "V6E 3P3", "director_citizenship": "Canada", "service_description": f"Scenario test: {scenario}", "geographic_coverage": "BC and Worldwide", "include_bits": True, "reg_contact_name": "Scenario Tester", "reg_contact_email": f"testcarrier+{uid}@performancewest.net", "reg_contact_phone": "+13075559876", "company_type": "numbered", "trade_name": "", "legal_ending": "Ltd.", "name_choice_1": "", "name_choice_2": "", "name_choice_3": "", } if scenario == "tradename": base["company_type"] = "numbered_tradename" base["trade_name"] = "Pacific Telecom" elif scenario == "expedited": pass # numbered, expedited checked in review step elif scenario == "own-address": base["own_ca_company"] = "Test Corp Office" base["own_ca_street"] = "100 West Pender St" base["own_ca_postal"] = "V6B 1R8" elif scenario == "discount": base["discount_code"] = "LAUNCH25" return base def run_all_scenarios(headless: bool = True) -> list[dict]: """Run all 4 additional scenarios and return results.""" results = [] # A1: BC Numbered + Trade Name data = make_scenario_data("tradename") r = run_scenario("BC-tradename", data, headless) results.append(r) # A2: BC Expedited data = make_scenario_data("expedited") # Override review step to check expedited steps_override = data steps_override["_custom_telecom_fn"] = step_telecom_with_expedited def _expedited_review(page, d): step_review_with_expedited(page, d) # We need to replace step_review_and_submit in the scenario runner. # The simplest approach: patch the data dict to signal expedited review. # Actually, let me use a different approach — run scenario manually. LOG.info("") LOG.info("=" * 60) LOG.info(" SCENARIO: BC-expedited") LOG.info("=" * 60) exp_data = make_scenario_data("expedited") exp_results = {"province": "BC-expedited", "steps": [], "order_number": None, "success": False} with sync_playwright() as p: browser = p.chromium.launch(headless=headless, args=["--disable-blink-features=AutomationControlled"]) ctx = browser.new_context(viewport={"width": 1440, "height": 900}, user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36") page = ctx.new_page() page.on("console", lambda msg: LOG.warning("[browser] %s: %s", msg.type, msg.text) if msg.type in ("error", "warning") else None) page.on("dialog", lambda d: (LOG.warning("[dialog] %s: %s", d.type, d.message), d.dismiss())) exp_steps = [ ("Load form", step_load_form), ("Select province", step_select_province), ("Company type", step_company_type), ("Director info", step_director_info), ("Telecom details", step_telecom_details), ("Identity verification", step_identity_verification), ("Review & submit (expedited)", step_review_with_expedited), ] for step_name, step_fn in exp_steps: t0 = time.time() try: step_fn(page, exp_data) elapsed = time.time() - t0 exp_results["steps"].append({"name": step_name, "status": "PASS", "time": round(elapsed, 2)}) LOG.info("[BC-expedited] ✓ %s (%.1fs)", step_name, elapsed) except Exception as exc: elapsed = time.time() - t0 exp_results["steps"].append({"name": step_name, "status": "FAIL", "error": str(exc)[:200], "time": round(elapsed, 2)}) LOG.error("[BC-expedited] ✗ %s FAILED (%.1fs): %s", step_name, elapsed, exc) screenshot(page, f"BC-expedited_ABORT") break if exp_data.get("order_number"): exp_results["order_number"] = exp_data["order_number"] # Check expedited flag in PG try: import psycopg2.extras conn = _get_db() cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) cur.execute("SELECT expedited, service_fee_cents FROM canada_crtc_orders WHERE order_number = %s", (exp_data["order_number"],)) order = cur.fetchone() conn.close() if order: ok_exp = order["expedited"] is True LOG.info("[BC-expedited] PG expedited=%s (expected True)", order["expedited"]) exp_results["steps"].append({"name": "PG expedited=true", "status": "PASS" if ok_exp else "FAIL", "time": 0.1}) except Exception as e: LOG.warning("[BC-expedited] PG check failed: %s", e) browser.close() exp_results["success"] = all(s["status"] in ("PASS", "SKIP", "WARN") for s in exp_results["steps"]) results.append(exp_results) # A3: BC Own Canadian Address oa_data = make_scenario_data("own-address") oa_data["_custom_telecom_fn"] = step_telecom_own_address r = run_scenario("BC-own-addr", oa_data, headless) # Check own_ca fields in PG if oa_data.get("order_number"): try: import psycopg2.extras conn = _get_db() cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) cur.execute("SELECT has_own_ca_address, amb_location_slug, amb_annual_price_cents, own_ca_company FROM canada_crtc_orders WHERE order_number = %s", (oa_data["order_number"],)) order = cur.fetchone() conn.close() if order: ok = order["has_own_ca_address"] is True and order["amb_location_slug"] is None and order["amb_annual_price_cents"] == 0 LOG.info("[BC-own-addr] PG: own=%s, slug=%s, amb_price=%s, company=%s", order["has_own_ca_address"], order["amb_location_slug"], order["amb_annual_price_cents"], order["own_ca_company"]) r["steps"].append({"name": "PG own-address checks", "status": "PASS" if ok else "FAIL", "time": 0.1}) except Exception as e: LOG.warning("[BC-own-addr] PG check failed: %s", e) results.append(r) # A4: BC Discount Code (LAUNCH25) LOG.info("") LOG.info("=" * 60) LOG.info(" SCENARIO: BC-discount") LOG.info("=" * 60) disc_data = make_scenario_data("discount") disc_results = {"province": "BC-discount", "steps": [], "order_number": None, "success": False} with sync_playwright() as p: browser = p.chromium.launch(headless=headless, args=["--disable-blink-features=AutomationControlled"]) ctx = browser.new_context(viewport={"width": 1440, "height": 900}, user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36") page = ctx.new_page() page.on("console", lambda msg: LOG.warning("[browser] %s: %s", msg.type, msg.text) if msg.type in ("error", "warning") else None) page.on("dialog", lambda d: (LOG.warning("[dialog] %s: %s", d.type, d.message), d.dismiss())) disc_steps = [ ("Load form", step_load_form), ("Select province", step_select_province), ("Company type", step_company_type), ("Director info", step_director_info), ("Telecom details", step_telecom_details), ("Identity verification", step_identity_verification), ("Review & submit (discount)", step_review_with_discount), ] for step_name, step_fn in disc_steps: t0 = time.time() try: step_fn(page, disc_data) elapsed = time.time() - t0 disc_results["steps"].append({"name": step_name, "status": "PASS", "time": round(elapsed, 2)}) LOG.info("[BC-discount] ✓ %s (%.1fs)", step_name, elapsed) except Exception as exc: elapsed = time.time() - t0 disc_results["steps"].append({"name": step_name, "status": "FAIL", "error": str(exc)[:200], "time": round(elapsed, 2)}) LOG.error("[BC-discount] ✗ %s FAILED (%.1fs): %s", step_name, elapsed, exc) screenshot(page, f"BC-discount_ABORT") break if disc_data.get("order_number"): disc_results["order_number"] = disc_data["order_number"] try: import psycopg2.extras conn = _get_db() cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) cur.execute("SELECT discount_code, discount_cents FROM canada_crtc_orders WHERE order_number = %s", (disc_data["order_number"],)) order = cur.fetchone() conn.close() if order: ok = order["discount_code"] == "LAUNCH25" and (order["discount_cents"] or 0) > 0 LOG.info("[BC-discount] PG: code=%s, discount=$%.2f", order["discount_code"], (order["discount_cents"] or 0) / 100) disc_results["steps"].append({"name": "PG discount applied", "status": "PASS" if ok else "FAIL", "time": 0.1, "detail": f"code={order['discount_code']}, cents={order['discount_cents']}"}) except Exception as e: LOG.warning("[BC-discount] PG check failed: %s", e) browser.close() disc_results["success"] = all(s["status"] in ("PASS", "SKIP", "WARN") for s in disc_results["steps"]) results.append(disc_results) return results def main(): parser = argparse.ArgumentParser(description="E2E CRTC province smoke test (no payments)") parser.add_argument("--province", choices=["BC", "ON", "both"], default="both", help="Province to test (default: both)") parser.add_argument("--scenarios", action="store_true", help="Also run additional scenarios (tradename, expedited, own-address, discount)") parser.add_argument("--scenarios-only", action="store_true", help="Only run additional scenarios (skip BC/ON province tests)") parser.add_argument("--headed", action="store_true", help="Run browser in headed (visible) mode") args = parser.parse_args() all_results = [] if not args.scenarios_only: provinces = ["BC", "ON"] if args.province == "both" else [args.province] for prov in provinces: result = run_province_test(prov, headless=not args.headed) all_results.append(result) if args.scenarios or args.scenarios_only: scenario_results = run_all_scenarios(headless=not args.headed) all_results.extend(scenario_results) print_report(all_results) # Exit with error code if any test failed if not all(r["success"] for r in all_results): sys.exit(1) if __name__ == "__main__": main()