From 6d441a5cc0a7f5e2e682054a33092787d13d5a1c Mon Sep 17 00:00:00 2001 From: justin Date: Fri, 22 May 2026 00:16:47 -0500 Subject: [PATCH] Add email-restricted discount codes and $0 order bypass - discount_codes.allowed_emails: when set, code only valid for listed emails - Flat discounts now replace bundle discount (don't stack) - $0 orders skip all payment gateways, mark paid immediately, redirect to success - FREEDOM249: $249 flat off restricted to 4+ deficiency carriers Co-Authored-By: Claude Opus 4.6 (1M context) --- api/src/routes/checkout.ts | 29 +++++++++++++++++++++++++++++ api/src/routes/compliance-orders.ts | 18 ++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts index 331574c..24a133b 100644 --- a/api/src/routes/checkout.ts +++ b/api/src/routes/checkout.ts @@ -880,6 +880,35 @@ router.post("/api/v1/checkout/create-session", async (req, res) => { }; const paymentMethodTypes = STRIPE_PAYMENT_METHOD_MAP[payment_method] ?? ["card"]; + // ── $0 orders: skip all payment gateways, mark paid immediately ────── + const effectiveTotal = base_cents - discount_cents; + if (effectiveTotal <= 0 && base_cents > 0) { + const zeroTable: Record = { + canada_crtc: "canada_crtc_orders", + formation: "formation_orders", + bundle: "bundle_orders", + compliance: "compliance_orders", + compliance_batch: "compliance_orders", + }; + const zt = zeroTable[order_type]; + if (zt) { + const whereCol = order_type === "compliance_batch" ? "batch_id" : "order_number"; + await pool.query( + `UPDATE ${zt} SET payment_status = 'paid', payment_method = 'free', status = 'pending', surcharge_cents = 0, surcharge_pct = 0 WHERE ${whereCol} = $1`, + [order_id], + ); + } + console.log(`[checkout] $0 order — skipping payment for ${order_type} ${order_id} (discount ${discount_cents} >= base ${base_cents})`); + res.json({ + checkout_url: `${DOMAIN}/order/success?order_id=${order_id}&order_type=${order_type}&free=1`, + session_id: null, + total_cents: 0, + surcharge_cents: 0, + surcharge_pct: 0, + }); + return; + } + if (payment_method === "crypto") { // Crypto orders bypass Stripe — create SHKeeper invoice via API const shkeeperUrl = process.env.SHKEEPER_URL || "http://127.0.0.1:5000"; diff --git a/api/src/routes/compliance-orders.ts b/api/src/routes/compliance-orders.ts index 70fc2ff..9aa91be 100644 --- a/api/src/routes/compliance-orders.ts +++ b/api/src/routes/compliance-orders.ts @@ -984,9 +984,20 @@ router.post("/api/v1/compliance-orders/batch", async (req, res) => { if (discount_code) { try { const disc = await pool.query( - `SELECT discount_type, discount_value FROM discount_codes WHERE code = $1 AND active = true AND (expires_at IS NULL OR expires_at > now())`, + `SELECT discount_type, discount_value, allowed_emails FROM discount_codes WHERE code = $1 AND active = true AND (expires_at IS NULL OR expires_at > now())`, [discount_code.toUpperCase().trim()], ); + if (disc.rows.length > 0) { + // If allowed_emails is set, verify the customer email is in the list + const allowedEmails = disc.rows[0].allowed_emails as string[] | null; + if (allowedEmails && allowedEmails.length > 0) { + const customerEmail = (customer_email || "").toLowerCase().trim(); + if (!allowedEmails.map((e: string) => e.toLowerCase()).includes(customerEmail)) { + // Silently skip — code exists but not valid for this email + disc.rows.length = 0; + } + } + } if (disc.rows.length > 0) { const d = disc.rows[0] as Record; const discType = d.discount_type as string; @@ -1003,7 +1014,10 @@ router.post("/api/v1/compliance-orders/batch", async (req, res) => { promoDiscountCents = Math.round(afterBundle * discValue / 100); } } else if (discType === "flat" && discValue > 0) { - promoDiscountCents = Math.min(discValue, discountableTotal - bundleDiscountCents); + // Flat discount replaces bundle discount (don't stack) + bundleDiscountPct = 0; + bundleDiscountCents = 0; + promoDiscountCents = Math.min(discValue, discountableTotal); } } } catch { /* discount_codes table may not exist */ }