# Payment capture timing

- **Pattern:** `ab-000554` (`booking-flow-lifecycle.payment.payment-capture-timing`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000554
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000554)

## Why it matters

Charging a card before the booking record is safely persisted and confirmed creates a class of support incidents that requires manual refunds: "I was charged but have no booking." CWE-841 (behavioral workflow violation) and CWE-362 both apply — the payment and booking writes are implicitly ordered, and that ordering is not enforced by the code. Stripe's authorize-then-capture flow (`capture_method: 'manual'`) exists precisely for this scenario: authorize the payment (reserve funds) while processing the booking, then capture (collect funds) only after the booking is guaranteed. Any other ordering introduces a window where money moves without a corresponding booking.

## Severity rationale

High because premature capture means every downstream booking failure — DB error, webhook delay, constraint violation — produces a charged customer with no reservation, requiring manual refund per incident.

## Remediation

Use `capture_method: 'manual'` on the PaymentIntent and capture only after the booking record is confirmed. Implement this in `src/app/api/checkout/route.ts` or `src/lib/payment-service.ts`.

```typescript
// 1. Persist booking first
const booking = await db.booking.create({
  data: { ...bookingData, status: 'PENDING_PAYMENT' }
});

// 2. Authorize (no money moves yet)
const intent = await stripe.paymentIntents.create({
  amount,
  currency: 'usd',
  capture_method: 'manual',
  metadata: { bookingId: booking.id },
});

// 3. Confirm booking, then capture via webhook
// stripe.paymentIntents.capture(intent.id) — called in the webhook handler
```

## Detection

- **ID:** `payment-capture-timing`
- **Label:** Payment capture timing
- **Severity:** `high`
- **What to look for:** Trace the order of operations in the checkout/payment flow: booking creation vs. payment charge. Before evaluating, extract and quote the sequence of API calls in the payment handler — identify which call creates the booking record and which call initiates the payment charge. The ideal pattern is: Create Booking (status=PENDING_PAYMENT) then Authorize Payment then Confirm Booking then Capture Payment. Count the number of steps and verify the ordering.
- **Pass criteria:** Payment is `captured` (money taken) only *after* the booking is successfully written and confirmed. At least 1 payment flow must use an "authorize and capture" pattern (Stripe `paymentIntents.create` with `capture_method: 'manual'`, or equivalent), not "charge immediately" (`charges.create` with immediate capture). The booking record must exist before the payment API call. Report: `"Payment flow: [step1] -> [step2] -> [step3] in [file]. Capture method: [manual|automatic]."`
- **Fail criteria:** Card is charged immediately before the booking record is safely created and confirmed. Do NOT pass when `capture_method` is `'automatic'` (the default) and the booking record does not exist before the charge — this means money is taken before the booking is guaranteed.
- **Skip (N/A) when:** No payment integration (no Stripe, PayPal, Square, or other payment SDK in `package.json` dependencies).
- **Detail on fail:** `"Payment is captured immediately upon user form submission. If the subsequent booking record insertion fails, a manual refund is required."`
- **Cross-reference:** The confirmation-record check in Booking Creation verifies the booking record is created before payment initiation.
- **Cross-reference:** The failure-handling check in this category verifies cleanup when payment fails after booking creation.
- **Cross-reference:** The payment-booking-coupling check in this category verifies the booking ID is passed to the payment provider.
- **Remediation:** Implement authorize-then-capture flow in your checkout handler (e.g., `src/app/api/checkout/route.ts` or `src/lib/payment-service.ts`).

  ```typescript
  // 1. Create booking (not yet charged)
  const booking = await db.booking.create({
    data: { ...bookingData, status: 'PENDING_PAYMENT' }
  });

  // 2. Authorize payment
  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    metadata: { bookingId: booking.id }
  });

  // 3. Confirm booking
  await db.booking.update({
    where: { id: booking.id },
    data: { status: 'CONFIRMED', paymentIntentId: paymentIntent.id }
  });

  // 4. Capture happens in webhook after successful confirmation
  ```

## External references

- cwe CWE-841
- cwe CWE-362
- iso-25010 functional-suitability.functional-correctness

Taxons: data-integrity, error-resilience

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