A customer who pays for an order and receives no confirmation email has no evidence that the transaction occurred. From their perspective, they may have been charged without a successful order — leading to chargebacks, duplicated purchases, and unnecessary support contacts. This is an iso-25010:2011 reliability failure: the system accepted a payment but failed to complete its contractual obligation to the customer. The confirmation email is also the canonical moment to surface the order number, line items, and delivery estimate — omitting it makes every subsequent customer question ('where is my order?') harder to handle.
Critical because failing to send a confirmation email after a successful payment leaves customers with no proof of purchase and no reference number, directly causing chargebacks and support escalations.
Add a confirmation email trigger in your payment webhook handler at app/api/webhooks/stripe/route.ts, fired immediately after the order transitions to 'confirmed'.
import { sendOrderConfirmationEmail } from '@/lib/email/order-emails'
case 'checkout.session.completed': {
const order = await db.orders.findFirst({
where: { stripeSessionId: event.data.object.id },
include: { items: true, user: true },
})
await transitionOrder(order.id, 'confirmed', 'system', 'Payment received')
await sendOrderConfirmationEmail({
to: order.user.email,
orderNumber: order.id,
items: order.items,
total: order.total,
})
break
}
If you use a job queue like BullMQ or Inngest, enqueue the email as a job rather than awaiting it inline to keep webhook response times under 5 seconds.
ID: ecommerce-order-management.notifications.confirmation-email
Severity: critical
What to look for: Find the code path that transitions an order to confirmed status. This is often triggered by a payment webhook (e.g., Stripe's payment_intent.succeeded or checkout.session.completed). Count the email service calls that fire during this transition. Look for calls to email libraries (Resend, SendGrid, Nodemailer, Postmark) with a template or content that references order details — enumerate which order fields are passed to the email template (at least 3 required: order number, items, total). Also check for indirect patterns: event emitters, job queues, or database triggers. Quote the exact email function name called (e.g., sendOrderConfirmationEmail).
Pass criteria: When an order transitions to confirmed status, at least 1 confirmation email call is triggered to the customer's email address. The email passes at least 3 order data fields (order number, items or line items, and total). The trigger exists in code and is not commented out or wrapped in a feature flag. An email function that is imported but never called in the confirmation path does not count as pass. Report the count even on pass: "Confirmation email sends N order fields."
Fail criteria: No email sending logic is found in or near the order confirmation code path (0 email calls). The order transitions to confirmed but the customer receives no notification. Or an email utility function exists but is never called during the confirmation flow.
Skip (N/A) when: The project uses an external order management system (Shopify, WooCommerce) that natively sends confirmation emails, and this audit is evaluating a custom layer that is not responsible for notifications. No confirmation transition exists in application code.
Detail on fail: "The payment webhook handler at src/app/api/webhooks/stripe/route.ts successfully transitions orders to 'confirmed' status but makes 0 calls to any email service. No confirmation email is sent to the customer after payment."
Cross-reference: If the creation-initial-state check found that orders are created directly in confirmed status, this email trigger should fire at creation time rather than on a separate transition.
Remediation: Add a confirmation email trigger in your payment webhook handler (app/api/webhooks/stripe/route.ts) or order confirmation function in lib/email/order-emails.ts:
// app/api/webhooks/stripe/route.ts
import { sendOrderConfirmationEmail } from '@/lib/email/order-emails'
case 'checkout.session.completed': {
const session = event.data.object
const order = await db.orders.findFirst({
where: { stripeSessionId: session.id },
include: { items: true, user: true },
})
await transitionOrder(order.id, 'confirmed', 'system', 'Payment received')
// Send confirmation email
await sendOrderConfirmationEmail({
to: order.user.email,
orderNumber: order.id,
items: order.items,
total: order.total,
estimatedDelivery: calculateEstimatedDelivery(),
})
break
}
If you are using a job queue (BullMQ, Inngest, etc.), enqueue the email job rather than sending inline to avoid blocking the webhook response.