/** * Rescue Mitchell Allen's batch CB-95BA6C90: the 5 DOT services were paid + the * workers ran, but (a) no customers row (login bug, card order predates fix), * (b) no ERPNext Sales Order (webhook SO bug), and (c) the authorization/signing * emails failed (localhost:25 SMTP bug) so he never got his e-sign links. * * Now that the SMTP + SO fixes are deployed, this: * 1. creates his customers row (so he can log in), * 2. creates the ERPNext SO via the deployed ensureComplianceSalesOrder path, * 3. re-dispatches each service worker so the (now-working) emails go out. * * Run: docker exec performancewest-api-1 node /app/scripts/rescue-mitchell.mjs */ import pg from "pg"; const BATCH = "CB-95BA6C90"; const WORKER_URL = process.env.WORKER_URL || "http://workers:8090"; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); const { rows } = await pool.query( `SELECT order_number, service_slug, customer_email, customer_name, erpnext_sales_order, intake_data FROM compliance_orders WHERE batch_id=$1 ORDER BY created_at`, [BATCH]); if (!rows.length) { console.log("no orders for batch"); process.exit(1); } const email = rows[0].customer_email.toLowerCase().trim(); const name = rows[0].customer_name || "Customer"; let company = null; try { const i = typeof rows[0].intake_data === "string" ? JSON.parse(rows[0].intake_data) : rows[0].intake_data; company = i?.company || i?.legal_name || i?.entity_name || null; } catch {} // 1) customers row (portal login) const c = await pool.query( `INSERT INTO customers (email, name, company) VALUES ($1,$2,$3) ON CONFLICT (email) DO UPDATE SET name=COALESCE(customers.name,EXCLUDED.name), company=COALESCE(customers.company,EXCLUDED.company) RETURNING id, (password_hash IS NOT NULL) AS has_pw`, [email, name, company]); console.log(`[rescue] customers row id=${c.rows[0].id} email=${email} has_password=${c.rows[0].has_pw}`); // 2) ERPNext Sales Order (reuse the deployed handlePaymentComplete SO path is internal; // here call the worker-friendly path: build the SO via the same helper by importing checkout) // Simpler + safe: re-run handlePaymentComplete is guarded (already paid -> returns early), // so directly create the SO using the exported ensureComplianceSalesOrder if available. const checkout = await import("/app/dist/routes/checkout.js"); if (typeof checkout.ensureComplianceSalesOrder === "function") { await checkout.ensureComplianceSalesOrder(BATCH, "compliance_batch", rows, "card"); console.log("[rescue] ensureComplianceSalesOrder ran"); } else { console.log("[rescue] NOTE: ensureComplianceSalesOrder not exported; SO will be built-from-PG by worker (404 warning is harmless)"); } // refresh SO id const after = await pool.query(`SELECT order_number, service_slug, erpnext_sales_order FROM compliance_orders WHERE batch_id=$1`, [BATCH]); const soName = after.rows.find(r => r.erpnext_sales_order)?.erpnext_sales_order || BATCH; console.log(`[rescue] SO = ${soName}`); // 3) re-dispatch each worker so authorization/signing emails resend (now that SMTP works) for (const r of after.rows) { try { const res = await fetch(`${WORKER_URL}/jobs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "process_compliance_service", order_name: r.erpnext_sales_order || soName, order_number: r.order_number, service_slug: r.service_slug, }), }); console.log(`[rescue] re-dispatched ${r.order_number} (${r.service_slug}) -> HTTP ${res.status}`); } catch (e) { console.log(`[rescue] dispatch error ${r.order_number}: ${e.message}`); } } await pool.end(); console.log("[rescue] DONE");