# Rescheduling is atomic

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

## Why it matters

Non-atomic reschedules split a logically single operation into two sequential database calls: first release the old slot, then reserve the new one. If the new slot reservation fails (conflict, timeout, or error), the old slot has already been released — and the booking is now in an invalid state with no slot attached. CWE-362 (race condition) and CWE-841 both apply. The customer ends up with a booking record that points to no slot, or a booking that was moved to a slot that was simultaneously taken by another user. A transaction wrapping both mutations prevents this by rolling back the release if the reservation fails.

## Severity rationale

Medium because non-atomic reschedules produce booking records in an invalid intermediate state on any failure during the two-step operation.

## Remediation

Wrap the availability recheck, old-slot release, and new-slot reservation in a single transaction in `src/lib/booking-service.ts`.

```typescript
await db.$transaction(async (tx) => {
  const conflict = await tx.booking.findFirst({
    where: { slotId: newSlotId, status: { not: 'CANCELLED' } }
  });
  if (conflict) throw new ConflictError('New slot unavailable');

  await tx.booking.update({
    where: { id: bookingId },
    data: { slotId: newSlotId, startTime: newStartTime, status: 'CONFIRMED' }
  });
  // slot capacity adjustments go here too, inside the same transaction
});
```

## Detection

- **ID:** `rescheduling-logic`
- **Label:** Rescheduling is atomic
- **Severity:** `medium`
- **What to look for:** Rescheduling should be treated as an atomic "Release Old Slot + Reserve New Slot" operation. Before evaluating, extract and quote the reschedule function or handler. Count all database mutations in the reschedule flow and verify they are wrapped in a single transaction. Enumerate: "X mutations in reschedule flow; Y of X are inside a transaction."
- **Pass criteria:** The full reschedule operation (validate new availability, release old slot, book new slot) happens in a single transaction with at least 2 mutations. If the new slot reservation fails, the old slot must not be released. All mutations must be inside the same `$transaction`, `BEGIN...COMMIT`, or equivalent block. Report: `"Y of X reschedule mutations wrapped in transaction in [file:line]."`
- **Fail criteria:** Old slot is released, then new slot is requested (risk of losing both if new fails). Or separate database calls without a wrapping transaction. Do NOT pass when the old booking is updated first and the new slot check happens in a separate non-transactional call.
- **Skip (N/A) when:** Modifications/rescheduling are explicitly not allowed (no update endpoint exists).
- **Detail on fail:** `"Rescheduling is not atomic. The old booking status is set to RESCHEDULED, then the new booking creation is attempted separately. If creation fails, the user has no booking."`
- **Cross-reference:** The transaction-atomicity check in Conflict Prevention verifies general transaction usage; this check is specific to reschedule flows.
- **Cross-reference:** The modification-validation check in this category verifies the same availability checks are applied during reschedule.
- **Cross-reference:** The slot-availability-recheck check in Conflict Prevention verifies the new slot availability is verified inside the transaction.
- **Remediation:** Wrap the reschedule operation in a single transaction in your booking service (e.g., `src/lib/booking-service.ts`).

  ```typescript
  await db.$transaction(async (tx) => {
    // Check new slot available
    const conflict = await tx.booking.findFirst({
      where: { slotId: newSlotId, status: { not: 'CANCELLED' } }
    });
    if (conflict) throw new Error("New slot unavailable");

    // Atomically update
    const oldBooking = await tx.booking.update({
      where: { id: bookingId },
      data: { status: 'RESCHEDULED', slotId: newSlotId, startTime: newStartTime }
    });
  });
  ```

## External references

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

Taxons: data-integrity

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