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.
Medium because non-atomic reschedules produce booking records in an invalid intermediate state on any failure during the two-step operation.
Wrap the availability recheck, old-slot release, and new-slot reservation in a single transaction in src/lib/booking-service.ts.
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
});
ID: booking-flow-lifecycle.lifecycle.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).
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 }
});
});