Availability engine
AvailabilityService.listAvailableSlots(args) is the single source of truth for “is
this slot bookable?” — used by the flow’s offer_slots block AND the hosted page’s
slot grid.
Pipeline
1. Load service duration + buffer + capacity
2. Load business hours per-day open/close + breaks + holidays
3. Load CONFIRMED bookings in the date range
4. Generate candidates step through each day's open window
5. Filter drop past slots, holiday-day slots, conflictsSlot step
Defaults to the service’s own duration (clean half-hour grid for a 30-min haircut → 9:00, 9:30, 10:00…). Caller can pass a denser step (e.g. 15 minutes) for finer control.
What blocks a slot
- Existing CONFIRMED booking at the same time (capacity-aware)
- Past times — anything before “now”
- Holiday date
- Day disabled
- Outside open hours
- Inside a lunch / cleaning break
Buffer time after a booking blocks the next slot from starting too soon. Buffer across breaks is allowed — appointment ends at 13:00, turnover runs into the lunch hour, doesn’t matter.
Capacity
Phase 1: capacity always 1, so a single conflicting booking blocks the slot. Phase 2
counts partySize across overlapping bookings vs. capacity, so 4 people can share
a “table for 4” slot.
Window cap
Hard cap of 60 days per query. Anything wider is rejected — keeps the slot grid quick to compute even on long-window flows.
Tests
The pure helpers (projectToTimezone, wallClockToUtc, enumerateLocalDays,
generateDailySlots, filterOutBookedSlots, filterOutPast) are unit-tested with
24 cases covering DST transitions, holiday boundaries, capacity > 1, breaks, and
half-open intervals.