Fix 6 bugs found in compliance and checkout flows

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) <noreply@anthropic.com>
This commit is contained in:
justin 2026-04-27 09:56:12 -05:00
parent 28d82912f7
commit a7d7fee154
4 changed files with 32 additions and 9 deletions

View file

@ -205,10 +205,10 @@ export async function sendOrderConfirmationEmail(params: OrderConfirmationParams
</td></tr>
</table>
<p style="margin:0 0 16px;font-size:13px;color:#6b7280;line-height:1.5;">
${(order_type === "compliance" || order_type === "compliance_batch") ? `<p style="margin:0 0 16px;font-size:13px;color:#6b7280;line-height:1.5;">
FCC compliance fees are tax deductible as ordinary business expenses under IRC &sect; 162.
A formal receipt will be sent separately.
</p>
</p>` : ""}
<h2 style="margin:0 0 12px;font-size:16px;font-weight:700;color:#111827;">What happens next</h2>
${stepsHtml}
@ -222,7 +222,7 @@ export async function sendOrderConfirmationEmail(params: OrderConfirmationParams
<p style="margin:0;font-size:14px;color:#6b7280;">
Questions? Reply to this email or reach us at
<a href="mailto:support@performancewest.net" style="color:#1e3a5f;">support@performancewest.net</a>
<a href="mailto:info@performancewest.net" style="color:#1e3a5f;">info@performancewest.net</a>
or call <a href="tel:18884110383" style="color:#1e3a5f;">1-888-411-0383</a>.
</p>
`;
@ -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"),

View file

@ -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],
);
}

View file

@ -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<string, unknown>[] = [];
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(

View file

@ -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;