A recurring schedule exception that does not override the calendar in real time is an invisible lie. The host marks next Monday as closed; the customer sees open slots for next Monday; the booking confirms; the host is unavailable. This ISO 25010 functional-correctness failure occurs when the exception record is stored correctly but the slot generation query does not join against the exceptions table before returning availability. The result is phantom slots: the system thinks it is correctly managing exceptions, but customers book into them anyway.
Low because the failure requires a recurring-schedule-plus-exceptions setup to trigger, but when it does, it creates confirmed bookings on dates the host explicitly marked unavailable.
In your slot generation function (typically src/lib/availability.ts or your availability API route), query exceptions in the same request as the recurring rule and filter before returning results.
// src/lib/availability.ts
export async function getRecurringSlots(recurringId: string, start: Date, end: Date) {
const [rec, exceptions] = await Promise.all([
db.recurringAvailability.findUnique({ where: { id: recurringId } }),
db.availabilityExceptions.findMany({
where: { recurringId, exceptionDate: { gte: start, lte: end } },
}),
])
if (!rec) return []
const blocked = new Set(exceptions.map(e => e.exceptionDate.toISOString().slice(0, 10)))
const occurrences = rrulestr(rec.rrule, { dtstart: start }).between(start, end)
return occurrences.filter(d => !blocked.has(d.toISOString().slice(0, 10)))
}
The exception filter must run synchronously in the slot generation path, not as a background job or deferred reconciliation task.
ID: booking-calendar-availability.recurring.exception-handling
Severity: low
What to look for: Count all code paths that generate slots from recurring rules. For each, verify that exception dates are queried and filtered before returning availability. Look for an exceptions table (e.g., availability_exceptions, schedule_exceptions) or an exceptions array within the recurring rule model. Trace the slot generation flow: RRULE parse -> generate occurrences -> filter by exceptions -> return available slots. Examine files matching src/lib/*recurring*, src/lib/*schedule*, src/lib/*availability*, src/app/api/availability*.
Pass criteria: Count all slot generation paths from recurring rules. 100% of slot generation paths must query and apply exceptions before returning slots. At least 1 exception storage mechanism must exist (database table, JSON array, or exception column). The exception filter must run at query time, not as a deferred background job. Report: "X of Y recurring slot paths apply exception filters. Exception storage: [table/array/column]."
Fail criteria: Calendar shows the default recurring pattern even on exception dates, or no exception querying exists in the slot generation code.
Skip (N/A) when: Platform does not support recurring schedules, or recurring schedules do not support exceptions.
Detail on fail: Example: "Recurring schedule: Mon-Fri 9-5. Exception added: next Monday, closed. Calendar still shows Mon-Fri 9-5 for next Monday."
Remediation: Apply exceptions when fetching slots in src/lib/availability.ts or your slot-generation service. Query the availability_exceptions table and filter before returning:
const exceptions = await db.availabilityExceptions.findMany({
where: { recurringId, exceptionDate: { gte: start, lte: end } },
})
const blocked = new Set(exceptions.map(e => e.exceptionDate.toISOString()))
return slots.filter(s => !blocked.has(s.date.toISOString()))
Cross-reference: Check rule-based-storage — rule-based storage is the prerequisite; exceptions override specific instances of the rule.
Cross-reference: Check recurring-edit-scope — "edit this only" creates an exception; this check verifies exceptions are applied.
Cross-reference: Check data-freshness — exception changes must be reflected on the next calendar load.