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.
High because missing capacity release on cancellation permanently blocks available slots, causing revenue loss and incorrect availability displays for every subsequent query.
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).
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 } }
}),
]);
}
ID: booking-flow-lifecycle.lifecycle.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).
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 } }
})
]);
}