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:
parent
28d82912f7
commit
a7d7fee154
4 changed files with 32 additions and 9 deletions
|
|
@ -205,10 +205,10 @@ export async function sendOrderConfirmationEmail(params: OrderConfirmationParams
|
||||||
</td></tr>
|
</td></tr>
|
||||||
</table>
|
</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 § 162.
|
FCC compliance fees are tax deductible as ordinary business expenses under IRC § 162.
|
||||||
A formal receipt will be sent separately.
|
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>
|
<h2 style="margin:0 0 12px;font-size:16px;font-weight:700;color:#111827;">What happens next</h2>
|
||||||
${stepsHtml}
|
${stepsHtml}
|
||||||
|
|
@ -222,7 +222,7 @@ export async function sendOrderConfirmationEmail(params: OrderConfirmationParams
|
||||||
|
|
||||||
<p style="margin:0;font-size:14px;color:#6b7280;">
|
<p style="margin:0;font-size:14px;color:#6b7280;">
|
||||||
Questions? Reply to this email or reach us at
|
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>.
|
or call <a href="tel:18884110383" style="color:#1e3a5f;">1-888-411-0383</a>.
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
|
|
@ -243,7 +243,7 @@ export async function sendOrderConfirmationEmail(params: OrderConfirmationParams
|
||||||
`What happens next:`,
|
`What happens next:`,
|
||||||
...nextSteps.map((s, i) => `${i + 1}. ${s}`),
|
...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.`,
|
`Performance West Inc.`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
|
|
|
||||||
|
|
@ -989,16 +989,19 @@ router.post("/api/v1/checkout/create-session", async (req, res) => {
|
||||||
formation: "formation_orders",
|
formation: "formation_orders",
|
||||||
bundle: "bundle_orders",
|
bundle: "bundle_orders",
|
||||||
compliance: "compliance_orders",
|
compliance: "compliance_orders",
|
||||||
|
compliance_batch: "compliance_orders",
|
||||||
};
|
};
|
||||||
const table = tableMap[order_type];
|
const table = tableMap[order_type];
|
||||||
if (table) {
|
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(
|
await pool.query(
|
||||||
`UPDATE ${table}
|
`UPDATE ${table}
|
||||||
SET stripe_session_id = $1,
|
SET stripe_session_id = $1,
|
||||||
payment_method = $2,
|
payment_method = $2,
|
||||||
surcharge_pct = $3,
|
surcharge_pct = $3,
|
||||||
surcharge_cents = $4
|
surcharge_cents = $4
|
||||||
WHERE order_number = $5`,
|
WHERE ${whereCol} = $5`,
|
||||||
[session.id, payment_method, surcharge_pct, surcharge_cents, order_id],
|
[session.id, payment_method, surcharge_pct, surcharge_cents, order_id],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -925,6 +925,13 @@ router.post("/api/v1/compliance-orders/batch", async (req, res) => {
|
||||||
return;
|
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)
|
// Split services into discountable vs non-discountable (e.g., RA services)
|
||||||
const discountableServices = services.filter(s => COMPLIANCE_SERVICES[s].discountable);
|
const discountableServices = services.filter(s => COMPLIANCE_SERVICES[s].discountable);
|
||||||
const nonDiscountableServices = 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 {
|
try {
|
||||||
const orders: Record<string, unknown>[] = [];
|
const orders: Record<string, unknown>[] = [];
|
||||||
|
let discountDistributed = 0;
|
||||||
|
const discountableCount = discountableServices.length;
|
||||||
|
let discountableIdx = 0;
|
||||||
|
|
||||||
for (const slug of services) {
|
for (const slug of services) {
|
||||||
const svc = COMPLIANCE_SERVICES[slug];
|
const svc = COMPLIANCE_SERVICES[slug];
|
||||||
// Distribute discount proportionally — only across discountable services
|
// Distribute discount proportionally — only across discountable services
|
||||||
const svcDiscount = svc.discountable
|
// Last discountable service absorbs rounding remainder
|
||||||
? Math.round(totalDiscountCents * svc.price_cents / (discountableTotal || 1))
|
let svcDiscount = 0;
|
||||||
: 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 orderNumber = generateOrderNumber();
|
||||||
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,9 @@ const STRIPE_IDENTITY_WEBHOOK_SECRET =
|
||||||
(process.env.NODE_ENV !== "production" && process.env.STRIPE_TEST_IDENTITY_WEBHOOK_SECRET?.trim()) ||
|
(process.env.NODE_ENV !== "production" && process.env.STRIPE_TEST_IDENTITY_WEBHOOK_SECRET?.trim()) ||
|
||||||
process.env.STRIPE_IDENTITY_WEBHOOK_SECRET ||
|
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
|
// 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;
|
const stripe = STRIPE_SECRET_KEY ? new Stripe(STRIPE_SECRET_KEY, { apiVersion: "2026-03-25.dahlia" as any }) : null;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue