""" 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())