new-site/scripts/tests/e2e_crtc_pricing.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

475 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
API-only CRTC pricing matrix test.
Verifies pricing math for all province × company-type × option combinations
by calling POST /api/v1/canada-crtc/orders directly (no browser needed).
Tests:
B1. 6 base pricing combos (2 provinces × 3 company types)
B2. 2 expedited pricing combos (BC + ON)
B3. 3 discount code tests (percent, flat, invalid)
B4. 1 own-Canadian-address test
B5. 4 validation edge cases (missing required fields → 400)
Usage:
python3 -m scripts.tests.e2e_crtc_pricing
python3 -m scripts.tests.e2e_crtc_pricing --keep # don't clean up test orders
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import sys
import uuid
from pathlib import Path
from dotenv import load_dotenv
env_path = Path(__file__).parent / ".env.test"
if env_path.exists():
load_dotenv(env_path)
import time
import requests
LOG = logging.getLogger("tests.pricing")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s %(message)s",
stream=sys.stdout,
)
# Use the dev API internal port directly (faster, avoids nginx TLS timeout)
API_URL = os.environ.get("DEV_API_URL", "http://207.174.124.71:3002")
DEV_DB_URL = os.environ.get(
"DEV_DATABASE_URL",
"postgresql://pw:pw_dev_2026@207.174.124.71:5433/performancewest",
)
# ── Expected constants (must match api/src/routes/canada-crtc.ts) ─────────
SERVICE_FEE = 389900
TRADE_NAME_ADDON = 7500
NAMED_ADDON = 8500
EXPEDITED_FEE = 50000
# Gov fees in CAD cents (from GOV_FEES_CAD in canada-crtc.ts)
GOV_CAD = {
"BC": {"numbered": 35000, "numbered_tradename": 39000, "named": 38000, "expedite": 10000},
"ON": {"numbered": 36000, "numbered_tradename": 40000, "named": 38500, "expedite": 0},
}
TYPE_ADDONS = {
"numbered": 0,
"numbered_tradename": TRADE_NAME_ADDON,
"named": NAMED_ADDON,
}
# FX range: CAD→USD rate is typically 0.68-0.80, plus 10% buffer + ceil to $1.
# We use a wide range to avoid flakiness.
FX_LOW = 0.65
FX_HIGH = 0.85
def cad_to_usd_range(cad_cents: int) -> tuple[int, int]:
"""Return (min, max) USD cents for a given CAD cents amount, accounting for FX variation."""
if cad_cents == 0:
return (0, 0)
lo = int(cad_cents * FX_LOW)
hi = int(cad_cents * FX_HIGH * 1.10) + 100 # +10% buffer + $1 rounding headroom
return (lo, hi)
# ── Test data factory ─────────────────────────────────────────────────────
def make_payload(
province: str = "BC",
company_type: str = "numbered",
trade_name: str = "",
name_choices: tuple[str, str, str] = ("", "", ""),
legal_ending: str = "Ltd.",
expedited: bool = False,
discount_code: str = "",
has_own_ca_address: bool = False,
own_ca_fields: dict | None = None,
amb_location_slug: str | None = None,
) -> dict:
uid = uuid.uuid4().hex[:6]
geo = f"{province} and Worldwide"
payload = {
"customer_name": f"PricingTest {province} {company_type}",
"customer_email": f"pricing-test+{uid}@performancewest.net",
"customer_phone": "+10005551234",
"company_type": company_type,
"incorporation_province": province,
"director_first_name": "Pricing",
"director_last_name": "Tester",
"director_country": "US",
"director_street": "100 Test Blvd",
"director_street2": "",
"director_city": "Houston",
"director_province": "TX",
"director_postal": "77001",
"director_citizenship": "United States",
"services_description": f"Pricing matrix test — {province} {company_type}",
"geographic_coverage": geo,
"include_bits": True,
"reg_contact_name": "Pricing Tester",
"reg_contact_email": f"pricing-test+{uid}@performancewest.net",
"reg_contact_phone": "+10005551234",
"expedited": expedited,
"disclaimer_agreed": True,
"test_mode": True,
}
if company_type == "numbered_tradename":
payload["trade_name"] = trade_name or "Test Trade Name"
payload["add_trade_name"] = True
elif company_type == "named":
payload["company_name_choice1"] = name_choices[0] or "Test Telecom Solutions Ltd."
payload["company_name_choice2"] = name_choices[1] or "Northern Voice Networks Inc."
payload["company_name_choice3"] = name_choices[2] or "Lakeside Communications Corp."
payload["legal_ending"] = legal_ending
if discount_code:
payload["discount_code"] = discount_code
if has_own_ca_address:
payload["has_own_ca_address"] = True
defaults = {
"own_ca_company": "Test Corp Office",
"own_ca_street": "100 Test St",
"own_ca_city": "Vancouver",
"own_ca_province": province,
"own_ca_postal": "V6B 1A1",
}
payload.update(own_ca_fields or defaults)
payload["amb_location_slug"] = None
elif amb_location_slug:
payload["amb_location_slug"] = amb_location_slug
return payload
_last_request_at = 0.0
def post_order(payload: dict) -> tuple[int, dict]:
"""POST to the order creation endpoint. Returns (status_code, response_json)."""
global _last_request_at
# Rate limit: wait at least 1.5s between requests to avoid 429
elapsed = time.time() - _last_request_at
if elapsed < 1.5:
time.sleep(1.5 - elapsed)
_last_request_at = time.time()
url = f"{API_URL}/api/v1/canada-crtc/orders"
r = requests.post(url, json=payload, timeout=15)
try:
data = r.json()
except Exception:
data = {"raw": r.text[:500]}
return r.status_code, data
# ── Result tracking ───────────────────────────────────────────────────────
PASS = 0
FAIL = 0
RESULTS: list[dict] = []
def check(label: str, ok: bool, detail: str = ""):
global PASS, FAIL
if ok:
PASS += 1
LOG.info(" PASS: %s", label)
else:
FAIL += 1
LOG.info(" FAIL: %s%s", label, detail)
RESULTS.append({"label": label, "ok": ok, "detail": detail})
def in_range(value: int, lo: int, hi: int) -> bool:
return lo <= value <= hi
# ── B1: Base pricing per province × company type ─────────────────────────
def test_base_pricing():
LOG.info("\n=== B1: Base Pricing Matrix (6 combos) ===")
for prov in ("BC", "ON"):
for ctype in ("numbered", "numbered_tradename", "named"):
label = f"{prov}/{ctype}"
LOG.info("\n --- %s ---", label)
payload = make_payload(province=prov, company_type=ctype)
status, data = post_order(payload)
check(f"{label}: HTTP 201", status == 201, f"got {status}: {data.get('error', '')}")
if status != 201:
continue
p = data.get("pricing", {})
expected_addon = TYPE_ADDONS[ctype]
expected_service = SERVICE_FEE + expected_addon
check(f"{label}: service_fee_cents = {expected_service}",
p.get("service_fee_cents") == expected_service,
f"got {p.get('service_fee_cents')}")
check(f"{label}: type_addon_cents = {expected_addon}",
p.get("type_addon_cents") == expected_addon,
f"got {p.get('type_addon_cents')}")
# Gov fees — range check (CAD→USD varies by FX rate)
gov_cad = GOV_CAD[prov][ctype]
gov_lo, gov_hi = cad_to_usd_range(gov_cad)
gov_actual = p.get("government_fee_cents", 0)
check(f"{label}: gov_fee in range [{gov_lo}, {gov_hi}]",
in_range(gov_actual, gov_lo, gov_hi),
f"got {gov_actual} (C${gov_cad / 100:.0f})")
# Total = service + gov + mailbox - discount
total = p.get("subtotal_cents", 0)
mailbox = p.get("mailbox_annual_cents", 0)
discount = p.get("discount_cents", 0)
expected_total = expected_service + gov_actual + mailbox - discount
check(f"{label}: total_cents math correct",
total == expected_total,
f"total={total}, expected={expected_total} (svc={expected_service}+gov={gov_actual}+mb={mailbox}-disc={discount})")
# ── B2: Expedited pricing ─────────────────────────────────────────────────
def test_expedited_pricing():
LOG.info("\n=== B2: Expedited Pricing (BC + ON) ===")
for prov in ("BC", "ON"):
label = f"{prov}/expedited"
LOG.info("\n --- %s ---", label)
payload = make_payload(province=prov, expedited=True)
status, data = post_order(payload)
check(f"{label}: HTTP 201", status == 201, f"got {status}: {data.get('error', '')}")
if status != 201:
continue
p = data.get("pricing", {})
expedite_actual = p.get("expedite_fee_cents", 0)
# BC: $500 + cadToUsd(C$100) ≈ $500 + ~$72-$94
# ON: $500 + cadToUsd(C$0) = $500 exactly
prov_expedite_cad = GOV_CAD[prov]["expedite"]
if prov_expedite_cad > 0:
exp_lo = EXPEDITED_FEE + cad_to_usd_range(prov_expedite_cad)[0]
exp_hi = EXPEDITED_FEE + cad_to_usd_range(prov_expedite_cad)[1]
else:
exp_lo = EXPEDITED_FEE
exp_hi = EXPEDITED_FEE
check(f"{label}: expedite_fee in [{exp_lo}, {exp_hi}]",
in_range(expedite_actual, exp_lo, exp_hi),
f"got {expedite_actual}")
# Verify total includes expedite fee
total = p.get("subtotal_cents", 0)
check(f"{label}: total includes expedite fee",
total >= SERVICE_FEE + expedite_actual,
f"total={total}")
# ── B3: Discount codes ────────────────────────────────────────────────────
def test_discount_codes():
LOG.info("\n=== B3: Discount Codes ===")
# Test 1: LAUNCH25 — 25% off service fee
LOG.info("\n --- LAUNCH25 (25%% percent) ---")
payload = make_payload(discount_code="LAUNCH25")
status, data = post_order(payload)
check("LAUNCH25: HTTP 201", status == 201, f"got {status}: {data.get('error', '')}")
if status == 201:
p = data.get("pricing", {})
expected_discount = round(SERVICE_FEE * 25 / 100)
check("LAUNCH25: discount_cents = 25% of service fee",
p.get("discount_cents") == expected_discount,
f"got {p.get('discount_cents')}, expected {expected_discount}")
check("LAUNCH25: total reduced by discount",
p.get("subtotal_cents", 0) < SERVICE_FEE + p.get("government_fee_cents", 0) + p.get("mailbox_annual_cents", 0),
f"total={p.get('subtotal_cents')}")
# Test 2: FIRST50 — $50 flat off service fee
LOG.info("\n --- FIRST50 ($50 flat) ---")
payload = make_payload(discount_code="FIRST50")
status, data = post_order(payload)
check("FIRST50: HTTP 201", status == 201, f"got {status}: {data.get('error', '')}")
if status == 201:
p = data.get("pricing", {})
check("FIRST50: discount_cents = 5000",
p.get("discount_cents") == 5000,
f"got {p.get('discount_cents')}")
# Test 3: INVALID999 — nonexistent code
LOG.info("\n --- INVALID999 (nonexistent) ---")
payload = make_payload(discount_code="INVALID999")
status, data = post_order(payload)
check("INVALID999: order still creates (201)", status == 201,
f"got {status}: {data.get('error', '')}")
if status == 201:
p = data.get("pricing", {})
check("INVALID999: discount_cents = 0",
p.get("discount_cents", 0) == 0,
f"got {p.get('discount_cents')}")
# Test 4: REF-EXAMPLE — referral partner code (15% off)
LOG.info("\n --- REF-EXAMPLE (15%% referral) ---")
payload = make_payload(discount_code="REF-EXAMPLE")
status, data = post_order(payload)
check("REF-EXAMPLE: HTTP 201", status == 201, f"got {status}: {data.get('error', '')}")
if status == 201:
p = data.get("pricing", {})
expected_discount = round(SERVICE_FEE * 15 / 100)
check("REF-EXAMPLE: discount = 15% of service fee",
p.get("discount_cents") == expected_discount,
f"got {p.get('discount_cents')}, expected {expected_discount}")
# ── B4: Own Canadian address ──────────────────────────────────────────────
def test_own_address():
LOG.info("\n=== B4: Own Canadian Address ===")
payload = make_payload(has_own_ca_address=True)
status, data = post_order(payload)
check("own-addr: HTTP 201", status == 201, f"got {status}: {data.get('error', '')}")
if status != 201:
return
p = data.get("pricing", {})
check("own-addr: mailbox_annual_cents = 0",
p.get("mailbox_annual_cents", -1) == 0,
f"got {p.get('mailbox_annual_cents')}")
# Verify in PG
order_number = data.get("order_number")
if order_number:
try:
import psycopg2
conn = psycopg2.connect(DEV_DB_URL)
cur = conn.cursor()
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",
(order_number,),
)
row = cur.fetchone()
conn.close()
if row:
check("own-addr PG: has_own_ca_address = true", row[0] is True, str(row[0]))
check("own-addr PG: amb_location_slug = null", row[1] is None, str(row[1]))
check("own-addr PG: amb_annual_price_cents = 0", row[2] == 0, str(row[2]))
check("own-addr PG: own_ca_company set", bool(row[3]), str(row[3]))
except Exception as e:
LOG.warning(" PG check skipped: %s", e)
# ── B5: Validation edge cases ─────────────────────────────────────────────
def test_validation():
LOG.info("\n=== B5: Validation Edge Cases ===")
# Missing customer_name
LOG.info("\n --- Missing customer_name ---")
payload = make_payload()
payload.pop("customer_name")
status, data = post_order(payload)
check("no customer_name: 400", status == 400, f"got {status}")
# Named company without name_choice_1
LOG.info("\n --- Named without name_choice_1 ---")
payload = make_payload(company_type="named")
payload.pop("company_name_choice1", None)
status, data = post_order(payload)
check("named no choice1: 400", status == 400, f"got {status}: {data.get('error', '')}")
# Tradename without trade_name
LOG.info("\n --- Tradename without trade_name ---")
payload = make_payload(company_type="numbered_tradename")
payload.pop("trade_name", None)
payload.pop("add_trade_name", None)
status, data = post_order(payload)
check("tradename no name: 400", status == 400, f"got {status}: {data.get('error', '')}")
# Invalid province
LOG.info("\n --- Invalid province ---")
payload = make_payload()
payload["incorporation_province"] = "QC"
status, data = post_order(payload)
check("invalid province: 400", status == 400, f"got {status}: {data.get('error', '')}")
# ── Cleanup ───────────────────────────────────────────────────────────────
def cleanup():
LOG.info("\n=== Cleanup ===")
try:
import psycopg2
conn = psycopg2.connect(DEV_DB_URL)
cur = conn.cursor()
cur.execute("DELETE FROM canada_crtc_orders WHERE customer_email LIKE 'pricing-test+%%@performancewest.net'")
deleted = cur.rowcount
conn.commit()
conn.close()
LOG.info(" Deleted %d test order(s)", deleted)
except Exception as e:
LOG.warning(" Cleanup failed: %s", e)
# ── Main ──────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="CRTC API pricing matrix test")
parser.add_argument("--keep", action="store_true", help="Don't delete test orders after run")
args = parser.parse_args()
LOG.info("=" * 60)
LOG.info(" CRTC API PRICING MATRIX TEST")
LOG.info(" Target: %s", API_URL)
LOG.info("=" * 60)
test_base_pricing()
test_expedited_pricing()
test_discount_codes()
test_own_address()
test_validation()
if not args.keep:
cleanup()
LOG.info("\n" + "=" * 60)
LOG.info(" RESULTS: %d passed, %d failed, %d total", PASS, FAIL, PASS + FAIL)
LOG.info("=" * 60)
if FAIL > 0:
LOG.info("\n Failed checks:")
for r in RESULTS:
if not r["ok"]:
LOG.info("%s%s", r["label"], r["detail"])
LOG.info("")
if FAIL == 0:
LOG.info(" ALL CHECKS PASSED")
else:
LOG.info(" %d CHECK(S) FAILED", FAIL)
LOG.info("=" * 60)
sys.exit(1 if FAIL > 0 else 0)
if __name__ == "__main__":
main()