From baa40443de77ebf77f7f305beb817677a524cb42 Mon Sep 17 00:00:00 2001 From: justin Date: Tue, 9 Jun 2026 00:23:15 -0500 Subject: [PATCH] fix(checkout): create ERPNext Sales Order for compliance_batch orders Batch orders (CB-XXXX, used by the trucking new-carrier flow and any multi- service cart) never created an ERPNext Sales Order -- the SO-creation branch was gated to order_type 'compliance' only. So those paid orders never reached ERPNext for fulfillment/accounting (0 of all paid batch orders had an erpnext_sales_order). Added a compliance_batch branch that creates ONE Sales Order with a line item per service in the batch (+ government-fee + processing- fee lines), then stamps the SO name on every batch row. Non-blocking like the others. Also created the missing ERPNext Items the new slugs reference (DOT-NEW-CARRIER-BUNDLE, LLC-FORMATION, CORP-FORMATION, NEW-CARRIER-BUNDLE which was missing too, GOVERNMENT-FILING-FEE). --- api/src/routes/checkout.ts | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts index 51db552..02e0689 100644 --- a/api/src/routes/checkout.ts +++ b/api/src/routes/checkout.ts @@ -966,6 +966,75 @@ router.post("/api/v1/checkout/create-session", async (req, res) => { } } + // ── Create ERPNext Sales Order (compliance BATCH) ─────────────────────── + // A batch (CB-XXXX) is one customer paying for several services at once, so + // it becomes ONE Sales Order with a line item per service (plus the + // processing-fee line). Previously batches created no SO at all, so trucking + // new-carrier orders (which always use the batch path) never reached ERPNext. + if (order_type === "compliance_batch" && erpnextCustomer) { + try { + const { COMPLIANCE_SERVICES } = await import("./compliance-orders.js"); + const { rows: batchRows } = await pool.query( + `SELECT * FROM compliance_orders WHERE batch_id = $1 ORDER BY created_at`, + [order_id], + ); + + const lineItems = batchRows.map((o: Record) => { + const info = COMPLIANCE_SERVICES[(o.service_slug as string) || ""]; + const svcCents = (o.service_fee_cents as number) || 0; + const govCents = (o.gov_fee_cents as number) || 0; + const items: Array<{ item_code: string; description: string; qty: number; rate: number }> = [{ + item_code: info?.erpnext_item || "COMPLIANCE-SERVICE", + description: (o.service_name as string) || info?.name || "Compliance Service", + qty: 1, + rate: toDollars(svcCents), + }]; + if (govCents > 0) { + items.push({ + item_code: "GOVERNMENT-FILING-FEE", + description: (o.gov_fee_label as string) || "Government filing fee", + qty: 1, + rate: toDollars(govCents), + }); + } + return items; + }).flat(); + + if (surcharge_cents > 0) { + lineItems.push({ + item_code: "PAYMENT-PROCESSING-FEE", + description: `${GATEWAY_LABELS[payment_method] || payment_method} ${surcharge_pct}%`, + qty: 1, + rate: toDollars(surcharge_cents), + }); + } + + const so = (await createResource("Sales Order", { + customer: erpnextCustomer, + delivery_date: new Date(Date.now() + 30 * 86400000).toISOString().split("T")[0], + custom_external_order_id: order_id, + custom_order_type: "compliance_batch", + custom_payment_gateway: GATEWAY_LABELS[payment_method] || payment_method, + custom_surcharge_pct: surcharge_pct, + workflow_state: "Received", + items: lineItems, + })) as { name: string }; + + try { + await callMethod("frappe.client.submit", { doc: { doctype: "Sales Order", name: so.name } }); + } catch { /* submit may fail if workflow doesn't require it */ } + + await pool.query( + `UPDATE compliance_orders SET erpnext_sales_order = $1 WHERE batch_id = $2`, + [so.name, order_id], + ); + + console.log(`[checkout] Created ERPNext Sales Order ${so.name} for batch ${order_id} (${lineItems.length} line items)`); + } catch (soErr) { + console.warn("[checkout] Batch Sales Order creation failed (non-blocking):", soErr); + } + } + // ── Create ERPNext Sales Order (US business formation) ────────────────── if (order_type === "formation" && erpnextCustomer) { try {