# Order confirmation email sent on state transition to confirmed

- **Pattern:** `ab-001145` (`ecommerce-order-management.notifications.confirmation-email`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001145
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001145)

## Why it matters

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.

## Severity rationale

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.

## Remediation

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'.

```ts
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.

## Detection

- **ID:** `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`:

  ```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.

## External references

- iso-25010 reliability

Taxons: error-resilience

HTML version: https://auditbuffet.com/patterns/ab-001145
