# Cancellation state machine

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

## Why it matters

Cancellation that only updates a `status` string without releasing slot capacity leaves the system in a permanently inconsistent state: the booking is logically gone, but the slot's capacity counter still reflects it as active. Every subsequent availability query will report the slot as booked when it is not, blocking new reservations for capacity that has been freed. CWE-841 (improper enforcement of behavioral workflow) applies — the cancellation workflow must atomically reverse all side-effects of booking creation: status change, capacity release, and refund trigger if paid. A double-cancellation guard is equally necessary to prevent a second cancel from decrementing capacity below zero.

## Severity rationale

High because missing capacity release on cancellation permanently blocks available slots, causing revenue loss and incorrect availability displays for every subsequent query.

## Remediation

Implement cancellation as an atomic transaction that updates both the booking status and the slot's capacity counter. Add this in your cancel handler (e.g., `src/app/api/bookings/[id]/cancel/route.ts` or `src/lib/booking-service.ts`).

```typescript
export async function cancelBooking(bookingId: string) {
  const booking = await db.booking.findUniqueOrThrow({ where: { id: bookingId } });
  if (booking.status === 'CANCELLED') throw new Error('Already cancelled');

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

## Detection

- **ID:** `cancellation-state-machine`
- **Label:** Cancellation state machine
- **Severity:** `high`
- **What to look for:** Locate the cancel handler (e.g., `POST /api/bookings/:id/cancel`, `DELETE /api/bookings/:id`, `cancelBooking` function). Count all side effects that must occur on cancellation: (1) status update to `'CANCELLED'`, (2) slot capacity release (decrement count or mark available), (3) refund trigger if paid, (4) notification to customer. Enumerate: "X of 4 cancellation side effects implemented." Also verify which source statuses are allowed to transition to CANCELLED (e.g., only CONFIRMED and PENDING, not already-CANCELLED).
- **Pass criteria:** Cancellation updates status to `'CANCELLED'` AND increments available capacity or marks the slot as available. Availability queries must exclude cancelled bookings (filter by `status != 'CANCELLED'`). At least 2 side effects (status update + capacity release) must be present. The handler must reject cancellation of already-cancelled bookings (guard against double-cancel). Report: `"X of 4 cancellation side effects implemented in [file]. Valid source statuses: [list]."`
- **Fail criteria:** Cancellation only changes the status string but does not reflect in availability queries (slot remains "blocked" in capacity counts). Also fails if no guard against double-cancellation exists.
- **Skip (N/A) when:** Never — cancellation logic is mandatory for any booking system.
- **Detail on fail:** `"The cancelBooking function updates booking.status to 'CANCELLED' but does not decrement the event's current booking count. The slot remains at capacity."`
- **Cross-reference:** The transaction-atomicity check in Conflict Prevention verifies these cancellation mutations are atomic.
- **Cross-reference:** The refund-logic check in Payment Integration verifies refund is triggered during cancellation.
- **Cross-reference:** The history-tracking check in this category verifies the cancellation transition is logged.
- **Remediation:** Ensure cancellation reverses the effects of booking on inventory. Implement in your cancel handler (e.g., `src/app/api/bookings/[id]/cancel/route.ts` or `src/lib/booking-service.ts`).

  ```typescript
  export async function cancelBooking(bookingId: string) {
    const booking = await db.booking.findUniqueOrThrow({
      where: { id: bookingId }
    });

    await db.$transaction([
      // Update booking status
      db.booking.update({
        where: { id: bookingId },
        data: { status: 'CANCELLED', cancelledAt: new Date() }
      }),
      // Free up the slot (increment capacity or update status)
      db.slot.update({
        where: { id: booking.slotId },
        data: { bookedCount: { decrement: 1 } }
      })
    ]);
  }
  ```

## External references

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

Taxons: data-integrity

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