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).
This commit is contained in:
justin 2026-06-09 00:23:15 -05:00
parent 90bccfda32
commit baa40443de

View file

@ -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<string, any>) => {
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) ────────────────── // ── Create ERPNext Sales Order (US business formation) ──────────────────
if (order_type === "formation" && erpnextCustomer) { if (order_type === "formation" && erpnextCustomer) {
try { try {