Booking Journey Data & the Available-Slot Engine
Overview
The read half of the appointment feature: three read-only endpoints that supply everything the booking screen needs to render — the journey structure (steps and fields configured in the CMS) along with its location, services, and staff; the available time slots for a given day; and the list of dates that still have capacity.
At the centre sits the SlotEngine, which generates time slots from business hours and service duration, then subtracts existing bookings. It is written as pure logic, testable independently of HTTP.
Business Flow
None of these three endpoints requires authentication — unlike the booking-creation endpoint — and all of them begin by resolving the journey.
GET /api/appointment/public/:token
- Find the active journey by
public_token— not found returns 404 withJourney not found. - Load the location referenced by
journey.location_id— not found returns 404 withLocation not found. - Load the active services and active staff for that location.
- Return
{journey, location, services, staff}.
GET /api/appointment/public/:token/slots
Accepts serviceId and date as query parameters. The journey is resolved first (a 404 propagates), then the engine is called with the locationId taken from the journey. serviceId is coerced the way JavaScript's Number() would coerce it — an unparseable value becomes 0, which matches no row and yields an empty array.
GET /api/appointment/public/:token/dates
Accepts serviceId and daysAhead, where daysAhead defaults to 30. The loop runs forward from today (in UTC) for that many days and collects each date with at least one free slot. Dates are returned in YYYY-MM-DD UTC form.
SlotEngine.GetAvailableSlots(locationID, serviceID, date) — seven steps
- Load the location's configuration —
working_hoursandblocked_dates. No row returns an empty array. - If the date falls inside
blocked_dates, return an empty array. - Determine the weekday by parsing the date as UTC midnight, mirroring JavaScript's
new Date("YYYY-MM-DD"), producing both the lowercase full name (monday) and the three-letter key (mon). - Read opening and closing times from
working_hours, which supports two shapes: an array of objects carryingday,enabled,openTimeoropen, andcloseTimeorclose; or an object keyed by day. A day with no configuration, or a falsyenabled, returns an empty array. - Load the service's
duration_minutesandmax_bookings_per_slot— missing values return an empty array. - Generate slots by stepping forward from the opening time in
durationMinutesincrements, emitting anHH:MMvalue only while start plus duration still fits before closing time. - Check per-slot capacity by counting overlapping bookings using the condition
slotStart < bookingEnd && slotEnd > bookingStart.- Full slots return
{time, available:false, reason:"booked", remaining:0}. - Open slots return
{time, available:true, remaining}with noreasonkey.
- Full slots return
IsSlotAvailable builds the day's slots and checks whether the requested startTime is available; if no such slot exists at all it returns false. This function is the gate used when creating a booking.
A parity note worth knowing: the engine ignores staff entirely — staffId is passed in but never used, matching the original source. Staff availability is checked during the auto-assign step of booking creation instead.
Key Files & Functions
| Route | Handler |
|---|---|
GET /api/appointment/public/:token | internal/appointment/handler.go → (*Handler).GetJourneyByToken |
GET /api/appointment/public/:token/slots | (*Handler).GetAvailableSlots |
GET /api/appointment/public/:token/dates | (*Handler).GetAvailableDates |
internal/appointment/register.go—Register(r, deps)internal/appointment/service.go—(*ServiceLayer).GetJourneyByToken,JourneyViewinternal/appointment/slotengine.go—NewSlotEngine,GetAvailableSlots,GetAvailableDates,IsSlotAvailable,weekday,resolveWorkingHours,generateTimeSlots,timeToMinutes,splitHM,truthy,firstString, and theSlottypeinternal/appointment/repository.go—FindActiveJourneyByToken,FindLocationByID,FindActiveServicesByLocation,FindActiveStaffByLocation,FindLocationSlotConfig,FindServiceDuration,FindExistingBookingsinternal/appointment/handler.go—jsNumberInt, which reproduces JavaScript'sNumber()coercion
Connections to Other Services
- Database — tables
appointment.journey,appointment.location(jsonbworking_hoursandblocked_dates),appointment.service(duration_minutes,max_bookings_per_slot,requires_staff),appointment.staff(jsonbservice_ids), andappointment.booking - Related features — consumed by booking creation, which calls
IsSlotAvailableas its double-booking guard - client-web — corresponds to the
appointment-bookingfeature