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>
312 lines
11 KiB
Python
312 lines
11 KiB
Python
"""
|
|
E2E test: full CRTC order flow on dev.performancewest.net
|
|
- Fill all form steps
|
|
- Submit order
|
|
- Verify PG record
|
|
- Verify ERPNext Customer + Sales Order
|
|
- Verify Stripe checkout session created
|
|
|
|
Run via: docker cp to workers, then exec.
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
import psycopg2
|
|
import requests
|
|
from playwright.async_api import async_playwright, Page
|
|
|
|
BASE = "https://dev.performancewest.net"
|
|
API = "https://api.dev.performancewest.net"
|
|
DB_URL = os.getenv("DATABASE_URL", "postgresql://pw:pw_dev_2026@127.0.0.1:5432/performancewest")
|
|
|
|
# ERPNext
|
|
ERP_URL = os.getenv("ERPNEXT_URL", "http://207.174.124.71:8080")
|
|
ERP_KEY = os.getenv("ERPNEXT_API_KEY", "")
|
|
ERP_SECRET = os.getenv("ERPNEXT_API_SECRET", "")
|
|
ERP_SITE = os.getenv("ERPNEXT_SITE_NAME", "performancewest.net")
|
|
|
|
UNIQUE = datetime.utcnow().strftime("%H%M%S")
|
|
TEST_EMAIL = f"e2e+{UNIQUE}@performancewest.net"
|
|
TEST_NAME = f"E2E Test {UNIQUE}"
|
|
|
|
|
|
def erp_get(path):
|
|
headers = {
|
|
"Authorization": f"token {ERP_KEY}:{ERP_SECRET}",
|
|
"X-Frappe-Site-Name": ERP_SITE,
|
|
}
|
|
r = requests.get(f"{ERP_URL}{path}", headers=headers, timeout=15)
|
|
return r.json()
|
|
|
|
|
|
async def fill_and_submit(page: Page) -> str:
|
|
"""Fill all 5 steps and submit. Returns the page URL after submit (Stripe redirect or error)."""
|
|
await page.goto(f"{BASE}/order/canada-crtc?test_mode=1", wait_until="domcontentloaded", timeout=60000)
|
|
await page.wait_for_timeout(2000)
|
|
|
|
# ── Step 1: Company type (numbered is default)
|
|
print("Step 1: company type")
|
|
await page.click("#btn-next")
|
|
await page.wait_for_timeout(500)
|
|
|
|
# ── Step 2: Director info
|
|
print("Step 2: director")
|
|
await page.fill("#director_first_name", "John")
|
|
await page.fill("#director_middle_name", "Q")
|
|
await page.fill("#director_last_name", "Testerton")
|
|
await page.select_option("#director_country", "US")
|
|
await page.wait_for_timeout(400)
|
|
await page.fill("#director_street", "742 Evergreen Terrace")
|
|
await page.fill("#director_city", "Springfield")
|
|
await page.select_option("#director_province_select", "IL")
|
|
await page.evaluate("document.getElementById('director_province_select').dispatchEvent(new Event('change',{bubbles:true}))")
|
|
await page.wait_for_timeout(200)
|
|
await page.fill("#director_postal", "62704")
|
|
try:
|
|
await page.select_option("#director_citizenship", "United States", timeout=3000)
|
|
except Exception:
|
|
pass
|
|
await page.click("#btn-next")
|
|
await page.wait_for_timeout(500)
|
|
|
|
# ── Step 3: Services
|
|
print("Step 3: services")
|
|
textarea = await page.query_selector("#service_description")
|
|
if textarea:
|
|
await textarea.fill("VoIP and data reseller services — E2E test order")
|
|
geo = await page.query_selector("#geographic_coverage")
|
|
if geo:
|
|
await geo.fill("Canada-wide + US interconnect")
|
|
await page.click("#btn-next")
|
|
await page.wait_for_timeout(500)
|
|
|
|
# ── Step 4: Identity (test_mode bypasses)
|
|
print("Step 4: identity (test_mode bypass)")
|
|
await page.click("#btn-next")
|
|
await page.wait_for_timeout(500)
|
|
|
|
# ── Step 5: Billing + payment
|
|
print("Step 5: billing contact + payment")
|
|
await page.fill("#customer_name", TEST_NAME)
|
|
await page.fill("#customer_email", TEST_EMAIL)
|
|
await page.fill("#customer_phone", "+12175551234")
|
|
|
|
# Select card payment
|
|
await page.click('input[name="payment_method_choice"][value="card"]')
|
|
await page.wait_for_timeout(200)
|
|
|
|
# Check consent
|
|
await page.check("#consent")
|
|
await page.wait_for_timeout(200)
|
|
|
|
# Submit
|
|
print("Clicking Submit Order...")
|
|
await page.click("#btn-next")
|
|
|
|
# Wait for redirect or error
|
|
for i in range(20):
|
|
await page.wait_for_timeout(1000)
|
|
try:
|
|
url = page.url
|
|
if "checkout.stripe.com" in url:
|
|
print(f" Redirected to Stripe at t+{i+1}s")
|
|
return url
|
|
status = await page.query_selector("#submit-status")
|
|
if status:
|
|
txt = (await status.inner_text()).strip()
|
|
if txt and txt not in ("", "Placing your order...", "Creating your order...", "Redirecting to payment..."):
|
|
return f"ERROR: {txt}"
|
|
except Exception:
|
|
await page.wait_for_timeout(1000)
|
|
return page.url
|
|
|
|
return "TIMEOUT"
|
|
|
|
|
|
def verify_pg_order(order_number: str) -> dict:
|
|
"""Check PG for the order and return all fields."""
|
|
conn = psycopg2.connect(DB_URL)
|
|
try:
|
|
with conn.cursor() as cur:
|
|
cur.execute("SELECT * FROM canada_crtc_orders WHERE order_number = %s", (order_number,))
|
|
cols = [d[0] for d in cur.description]
|
|
row = cur.fetchone()
|
|
if not row:
|
|
return {"error": "Order not found in PG"}
|
|
return dict(zip(cols, row))
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def verify_erp_customer(email: str) -> dict:
|
|
"""Check ERPNext for the customer."""
|
|
data = erp_get(f"/api/resource/Customer?filters=[[\"email_id\",\"=\",\"{email}\"]]&fields=[\"name\",\"customer_name\",\"email_id\"]&limit_page_length=1")
|
|
results = data.get("data", [])
|
|
return results[0] if results else {"error": "Customer not found in ERPNext"}
|
|
|
|
|
|
def verify_erp_sales_order(order_id: str) -> dict:
|
|
"""Check ERPNext for the Sales Order linked to this external order."""
|
|
data = erp_get(f"/api/resource/Sales Order?filters=[[\"custom_external_order_id\",\"=\",\"{order_id}\"]]&fields=[\"name\",\"customer\",\"grand_total\",\"status\",\"workflow_state\",\"custom_external_order_id\",\"custom_payment_gateway\"]&limit_page_length=1")
|
|
results = data.get("data", [])
|
|
if not results:
|
|
return {"error": "Sales Order not found in ERPNext"}
|
|
so = results[0]
|
|
# Get items
|
|
items_data = erp_get(f"/api/resource/Sales Order/{so['name']}?fields=[\"items\"]")
|
|
items = items_data.get("data", {}).get("items", [])
|
|
so["items"] = [{"item_code": i.get("item_code"), "qty": i.get("qty"), "rate": i.get("rate"), "amount": i.get("amount")} for i in items]
|
|
return so
|
|
|
|
|
|
async def main():
|
|
print("=" * 60)
|
|
print(f"E2E FULL ORDER TEST — {datetime.utcnow().isoformat()}")
|
|
print(f"Email: {TEST_EMAIL}")
|
|
print("=" * 60)
|
|
|
|
# Step 1: Submit order via Playwright
|
|
async with async_playwright() as pw:
|
|
browser = await pw.chromium.launch(headless=True)
|
|
page = await browser.new_page()
|
|
|
|
errors = []
|
|
page.on("pageerror", lambda e: errors.append(str(e)))
|
|
|
|
result_url = await fill_and_submit(page)
|
|
await browser.close()
|
|
|
|
if errors:
|
|
print(f"\nPage errors: {errors}")
|
|
|
|
print(f"\nResult: {result_url[:120]}")
|
|
|
|
if "checkout.stripe.com" not in result_url and not result_url.startswith("ERROR"):
|
|
print("FAIL: Did not redirect to Stripe")
|
|
sys.exit(1)
|
|
|
|
# Extract order number from the dev API logs or PG
|
|
# The order was created before the Stripe redirect — find it by email
|
|
print("\n" + "=" * 60)
|
|
print("VERIFICATION")
|
|
print("=" * 60)
|
|
|
|
conn = psycopg2.connect(DB_URL)
|
|
try:
|
|
with conn.cursor() as cur:
|
|
cur.execute("SELECT order_number FROM canada_crtc_orders WHERE customer_email = %s ORDER BY created_at DESC LIMIT 1", (TEST_EMAIL.lower(),))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
print("FAIL: No order found in PG for", TEST_EMAIL)
|
|
sys.exit(1)
|
|
order_number = row[0]
|
|
finally:
|
|
conn.close()
|
|
|
|
print(f"\nOrder Number: {order_number}")
|
|
|
|
# ── Verify PG ──
|
|
print("\n--- PostgreSQL Order Record ---")
|
|
pg = verify_pg_order(order_number)
|
|
if "error" in pg:
|
|
print(f"FAIL: {pg['error']}")
|
|
else:
|
|
checks = {
|
|
"order_number": order_number,
|
|
"customer_name": TEST_NAME,
|
|
"customer_email": TEST_EMAIL.lower(),
|
|
"customer_phone": "+12175551234",
|
|
"company_type": "numbered",
|
|
"director_name": "John Q Testerton",
|
|
"director_first_name": "John",
|
|
"director_middle_name": "Q",
|
|
"director_last_name": "Testerton",
|
|
"services_description": lambda v: "E2E test" in (v or ""),
|
|
"geographic_coverage": lambda v: "Canada" in (v or ""),
|
|
"include_bits": True,
|
|
"payment_status": "pending_payment",
|
|
"status": "received",
|
|
"expedited": False,
|
|
"has_own_ca_address": False,
|
|
}
|
|
pass_count = 0
|
|
fail_count = 0
|
|
for field, expected in checks.items():
|
|
actual = pg.get(field)
|
|
if callable(expected):
|
|
ok = expected(actual)
|
|
else:
|
|
ok = actual == expected
|
|
status = "PASS" if ok else "FAIL"
|
|
if ok:
|
|
pass_count += 1
|
|
else:
|
|
fail_count += 1
|
|
print(f" {status}: {field} = {repr(actual)}" + (f" (expected {repr(expected)})" if not ok else ""))
|
|
|
|
# Check non-null fields
|
|
for field in ["stripe_session_id", "service_fee_cents", "total_cents", "director_address", "mailbox_address"]:
|
|
val = pg.get(field)
|
|
ok = val is not None and val != "" and val != 0
|
|
status = "PASS" if ok else "FAIL"
|
|
if ok:
|
|
pass_count += 1
|
|
else:
|
|
fail_count += 1
|
|
print(f" {status}: {field} is set = {repr(val)[:60]}")
|
|
|
|
# Check erpnext_sales_order
|
|
erp_so_name = pg.get("erpnext_sales_order")
|
|
ok = erp_so_name is not None and erp_so_name != ""
|
|
status = "PASS" if ok else "FAIL"
|
|
if ok:
|
|
pass_count += 1
|
|
else:
|
|
fail_count += 1
|
|
print(f" {status}: erpnext_sales_order = {repr(erp_so_name)}")
|
|
|
|
print(f"\n PG: {pass_count} passed, {fail_count} failed")
|
|
|
|
# ── Verify ERPNext Customer ──
|
|
print("\n--- ERPNext Customer ---")
|
|
cust = verify_erp_customer(TEST_EMAIL.lower())
|
|
if "error" in cust:
|
|
print(f" FAIL: {cust['error']}")
|
|
else:
|
|
print(f" PASS: Customer found — {cust['name']} ({cust['customer_name']}, {cust['email_id']})")
|
|
|
|
# ── Verify ERPNext Sales Order ──
|
|
print("\n--- ERPNext Sales Order ---")
|
|
so = verify_erp_sales_order(order_number)
|
|
if "error" in so:
|
|
print(f" FAIL: {so['error']}")
|
|
else:
|
|
print(f" PASS: Sales Order found — {so['name']}")
|
|
print(f" customer: {so.get('customer')}")
|
|
print(f" grand_total: ${so.get('grand_total')}")
|
|
print(f" status: {so.get('status')}")
|
|
print(f" workflow_state: {so.get('workflow_state')}")
|
|
print(f" payment_gateway: {so.get('custom_payment_gateway')}")
|
|
print(f" external_order_id: {so.get('custom_external_order_id')}")
|
|
print(f" items:")
|
|
for item in so.get("items", []):
|
|
print(f" - {item['item_code']} x{item['qty']} @ ${item['rate']} = ${item['amount']}")
|
|
|
|
# ── Summary ──
|
|
print("\n" + "=" * 60)
|
|
all_ok = (
|
|
"error" not in pg
|
|
and "error" not in cust
|
|
and "error" not in so
|
|
and fail_count == 0
|
|
and "checkout.stripe.com" in result_url
|
|
)
|
|
print(f"RESULT: {'ALL CHECKS PASSED' if all_ok else 'SOME CHECKS FAILED'}")
|
|
print("=" * 60)
|
|
|
|
|
|
asyncio.run(main())
|