# Failure handling releases resources

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

## Why it matters

A failed payment that leaves a `PENDING_PAYMENT` or `HELD` booking blocks the slot indefinitely: the booking appears in capacity counts, the slot shows as unavailable to other users, and the original customer cannot retry because the slot appears taken. CWE-459 (incomplete cleanup on error) names this pattern precisely. Every payment failure path — declined card, network timeout, webhook failure — must release both the booking status and the slot hold atomically. A slot left blocked by a failed payment is functionally equivalent to a permanently deleted slot from the user's perspective.

## Severity rationale

High because unrecovered failed-payment state permanently blocks slots for all future customers, directly reducing available inventory without any corresponding revenue.

## Remediation

Handle payment failures in your Stripe webhook handler (e.g., `src/app/api/webhooks/stripe/route.ts`) with an atomic transaction that updates both the booking and the slot.

```typescript
// In payment_intent.payment_failed webhook handler
const bookingId = event.data.object.metadata.bookingId;
const booking = await db.booking.findUniqueOrThrow({ where: { id: bookingId } });

await db.$transaction([
  db.booking.update({
    where: { id: bookingId },
    data: { status: 'FAILED', failureReason: 'payment_declined' }
  }),
  db.slot.update({
    where: { id: booking.slotId },
    data: { bookedCount: { decrement: 1 } }
  }),
]);
```

## Detection

- **ID:** `failure-handling`
- **Label:** Failure handling releases resources
- **Severity:** `high`
- **What to look for:** If payment fails (declined card, timeout, etc.), check that the temporarily locked/held booking and slot are immediately released or cancelled. Count all payment failure paths: webhook failure handler (e.g., `payment_intent.payment_failed`), try/catch around payment API calls, and timeout handlers. For each path, verify both the booking status is updated AND the slot is released. Enumerate: "X of Y payment failure paths release both booking and slot."
- **Pass criteria:** Failure paths in payment processing explicitly set booking status to `'FAILED'` or `'CANCELLED'` AND free up the slot (decrement capacity or release hold). Both resources must be cleaned up in every failure path. The user must be able to immediately rebook. At least 1 explicit failure handler must exist. Report: `"X of Y payment failure paths release resources in [file]. Booking set to [status], slot [released/not released]."`
- **Fail criteria:** Failed payments leave "PENDING_PAYMENT" or "HELD" bookings that block slots indefinitely. Users cannot retry. Also fails if the booking status is updated but the slot is not released.
- **Skip (N/A) when:** No payment integration (no Stripe, PayPal, Square, or other payment SDK in `package.json` dependencies).
- **Detail on fail:** `"Payment failure block updates the booking status to 'FAILED' but does not appear to revert the slot hold, leaving it blocked."`
- **Cross-reference:** The time-slot-lock check in Booking Creation verifies the hold mechanism that must be released on failure.
- **Cross-reference:** The transaction-atomicity check in Conflict Prevention verifies this cleanup happens atomically.
- **Cross-reference:** The payment-capture-timing check in this category verifies the ordering that determines what needs cleanup.
- **Remediation:** Clean up resources on payment failure in your webhook handler (e.g., `src/app/api/webhooks/stripe/route.ts`) or error handler.

  ```typescript
  // In payment failure webhook or error handler
  await db.$transaction([
    db.booking.update({
      where: { id: bookingId },
      data: { status: 'FAILED', failureReason: 'payment_declined' }
    }),
    // Release the slot hold
    db.slot.update({
      where: { id: booking.slotId },
      data: { bookedCount: { decrement: 1 } }
    })
  ]);
  ```

## External references

- cwe CWE-459
- iso-25010 reliability.recoverability

Taxons: error-resilience

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