From a7d7fee15455748f781c574b5ddafb89a1406c0e Mon Sep 17 00:00:00 2001 From: justin Date: Mon, 27 Apr 2026 09:56:12 -0500 Subject: [PATCH] Fix 6 bugs found in compliance and checkout flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. CRITICAL: Add compliance_batch to stripe session tableMap — session IDs weren't being stored for batch orders 2. CRITICAL: Fix batch orders using order_number instead of batch_id when storing stripe_session_id 3. MAJOR: Tax deductibility note only shows for compliance orders, not CRTC/formation/bundles 4. MAJOR: Identity verification fallback changed from localhost:4321 to performancewest.net with warning log 5. MEDIUM: Fix discount rounding — last service absorbs remainder to prevent cent loss across batch orders 6. LOW: Validate at least one paid service in batch orders 7. Standardize support email to info@performancewest.net everywhere Co-Authored-By: Claude Opus 4.6 (1M context) --- api/src/email.ts | 8 ++++---- api/src/routes/checkout.ts | 5 ++++- api/src/routes/compliance-orders.ts | 24 +++++++++++++++++++++--- api/src/routes/identity.ts | 4 +++- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/api/src/email.ts b/api/src/email.ts index a2ebd35..23625f6 100644 --- a/api/src/email.ts +++ b/api/src/email.ts @@ -205,10 +205,10 @@ export async function sendOrderConfirmationEmail(params: OrderConfirmationParams -

+ ${(order_type === "compliance" || order_type === "compliance_batch") ? `

FCC compliance fees are tax deductible as ordinary business expenses under IRC § 162. A formal receipt will be sent separately. -

+

` : ""}

What happens next

${stepsHtml} @@ -222,7 +222,7 @@ export async function sendOrderConfirmationEmail(params: OrderConfirmationParams

Questions? Reply to this email or reach us at - support@performancewest.net + info@performancewest.net or call 1-888-411-0383.

`; @@ -243,7 +243,7 @@ export async function sendOrderConfirmationEmail(params: OrderConfirmationParams `What happens next:`, ...nextSteps.map((s, i) => `${i + 1}. ${s}`), ``, - `Questions? Email support@performancewest.net or call 1-888-411-0383.`, + `Questions? Email info@performancewest.net or call 1-888-411-0383.`, ``, `Performance West Inc.`, ].join("\n"), diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts index d11f4e0..913d67b 100644 --- a/api/src/routes/checkout.ts +++ b/api/src/routes/checkout.ts @@ -989,16 +989,19 @@ router.post("/api/v1/checkout/create-session", async (req, res) => { formation: "formation_orders", bundle: "bundle_orders", compliance: "compliance_orders", + compliance_batch: "compliance_orders", }; const table = tableMap[order_type]; if (table) { + // 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 order_number = $5`, + WHERE ${whereCol} = $5`, [session.id, payment_method, surcharge_pct, surcharge_cents, order_id], ); } diff --git a/api/src/routes/compliance-orders.ts b/api/src/routes/compliance-orders.ts index 0ed73af..80f0722 100644 --- a/api/src/routes/compliance-orders.ts +++ b/api/src/routes/compliance-orders.ts @@ -925,6 +925,13 @@ router.post("/api/v1/compliance-orders/batch", async (req, res) => { return; } + // At least one paid service required + const hasPaidService = services.some(s => COMPLIANCE_SERVICES[s].price_cents > 0); + if (!hasPaidService) { + res.status(400).json({ error: "At least one paid service is required." }); + return; + } + // Split services into discountable vs non-discountable (e.g., RA services) const discountableServices = services.filter(s => COMPLIANCE_SERVICES[s].discountable); const nonDiscountableServices = services.filter(s => !COMPLIANCE_SERVICES[s].discountable); @@ -965,13 +972,24 @@ router.post("/api/v1/compliance-orders/batch", async (req, res) => { try { const orders: Record[] = []; + let discountDistributed = 0; + const discountableCount = discountableServices.length; + let discountableIdx = 0; for (const slug of services) { const svc = COMPLIANCE_SERVICES[slug]; // Distribute discount proportionally — only across discountable services - const svcDiscount = svc.discountable - ? Math.round(totalDiscountCents * svc.price_cents / (discountableTotal || 1)) - : 0; + // Last discountable service absorbs rounding remainder + let svcDiscount = 0; + if (svc.discountable && totalDiscountCents > 0) { + discountableIdx++; + if (discountableIdx === discountableCount) { + svcDiscount = totalDiscountCents - discountDistributed; + } else { + svcDiscount = Math.round(totalDiscountCents * svc.price_cents / (discountableTotal || 1)); + } + discountDistributed += svcDiscount; + } const orderNumber = generateOrderNumber(); const result = await pool.query( diff --git a/api/src/routes/identity.ts b/api/src/routes/identity.ts index e838add..48ab2c2 100644 --- a/api/src/routes/identity.ts +++ b/api/src/routes/identity.ts @@ -46,7 +46,9 @@ const STRIPE_IDENTITY_WEBHOOK_SECRET = (process.env.NODE_ENV !== "production" && process.env.STRIPE_TEST_IDENTITY_WEBHOOK_SECRET?.trim()) || process.env.STRIPE_IDENTITY_WEBHOOK_SECRET || ""; -const DOMAIN = process.env.DOMAIN ? `https://${process.env.DOMAIN}` : "http://localhost:4321"; +const DOMAIN = process.env.DOMAIN + ? `https://${process.env.DOMAIN}` + : (() => { console.error("[identity] WARNING: DOMAIN env var not set — identity verification return URLs will fail"); return "https://performancewest.net"; })(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const stripe = STRIPE_SECRET_KEY ? new Stripe(STRIPE_SECRET_KEY, { apiVersion: "2026-03-25.dahlia" as any }) : null;