diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts index 69e9693..a34fd30 100644 --- a/api/src/routes/checkout.ts +++ b/api/src/routes/checkout.ts @@ -308,10 +308,12 @@ export async function ensureComplianceSalesOrder( const { COMPLIANCE_SERVICES } = await import("./compliance-orders.js"); const surchargePct = Number(first.surcharge_pct || 0); let surchargeCents = 0; + let discountCents = 0; const lineItems = rows.map((o: Record) => { const info = COMPLIANCE_SERVICES[(o.service_slug as string) || ""]; surchargeCents += Number(o.surcharge_cents || 0); + discountCents += Number(o.discount_cents || 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", @@ -348,6 +350,9 @@ export async function ensureComplianceSalesOrder( custom_surcharge_pct: surchargePct, workflow_state: "Received", items: lineItems, + // Reflect the promo/bundle discount so the SO grand total matches what the + // customer actually paid (line items are full price; discount applied here). + ...(discountCents > 0 ? { apply_discount_on: "Grand Total", discount_amount: toDollars(discountCents) } : {}), }).catch(async (e: unknown) => { // Resilience: if a service's ERPNext Item is missing, the SO would 404. // Retry once with every line item remapped to the generic COMPLIANCE-SERVICE @@ -366,6 +371,7 @@ export async function ensureComplianceSalesOrder( custom_surcharge_pct: surchargePct, workflow_state: "Received", items: fallback, + ...(discountCents > 0 ? { apply_discount_on: "Grand Total", discount_amount: toDollars(discountCents) } : {}), }); } throw e; @@ -1512,17 +1518,40 @@ router.post("/api/v1/checkout/create-session", async (req, res) => { }; const table = tableMap[order_type]; if (table) { - // For batch orders, update by batch_id; otherwise by order_number + // For batch orders, update by batch_id; otherwise by order_number. const whereCol = order_type === "compliance_batch" ? "batch_id" : "order_number"; - await pool.query( - `UPDATE ${table} - SET stripe_session_id = $1, - payment_method = $2, - surcharge_pct = $3, - surcharge_cents = $4 - WHERE ${whereCol} = $5`, - [session.id, payment_method, surcharge_pct, surcharge_cents, order_id], - ); + if (order_type === "compliance_batch") { + // A batch has ONE surcharge for the whole order, but it is stored per + // row. Writing the full surcharge_cents to every row makes anything that + // SUMS the per-row field (e.g. the Telegram order notification) over- + // count by Nx. Split the single surcharge across the rows so the per-row + // values sum to the true total (remainder on the first row). + const { rows: brows } = await pool.query( + `SELECT order_number FROM ${table} WHERE batch_id = $1 ORDER BY created_at`, + [order_id], + ); + const n = brows.length || 1; + const per = Math.floor(surcharge_cents / n); + const remainder = surcharge_cents - per * n; + for (let i = 0; i < brows.length; i++) { + const rowSurcharge = per + (i === 0 ? remainder : 0); + await pool.query( + `UPDATE ${table} + SET stripe_session_id = $1, payment_method = $2, + surcharge_pct = $3, surcharge_cents = $4 + WHERE order_number = $5`, + [session.id, payment_method, surcharge_pct, rowSurcharge, brows[i].order_number], + ); + } + } else { + await pool.query( + `UPDATE ${table} + SET stripe_session_id = $1, payment_method = $2, + surcharge_pct = $3, surcharge_cents = $4 + WHERE ${whereCol} = $5`, + [session.id, payment_method, surcharge_pct, surcharge_cents, order_id], + ); + } } console.log(`[checkout] Stripe session ${session.id} created for ${order_type} ${order_id}`);