Skip to main content

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

  1. Find the active journey by public_token — not found returns 404 with Journey not found.
  2. Load the location referenced by journey.location_id — not found returns 404 with Location not found.
  3. Load the active services and active staff for that location.
  4. 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

  1. Load the location's configuration — working_hours and blocked_dates. No row returns an empty array.
  2. If the date falls inside blocked_dates, return an empty array.
  3. 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).
  4. Read opening and closing times from working_hours, which supports two shapes: an array of objects carrying day, enabled, openTime or open, and closeTime or close; or an object keyed by day. A day with no configuration, or a falsy enabled, returns an empty array.
  5. Load the service's duration_minutes and max_bookings_per_slot — missing values return an empty array.
  6. Generate slots by stepping forward from the opening time in durationMinutes increments, emitting an HH:MM value only while start plus duration still fits before closing time.
  7. 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 no reason key.

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 entirelystaffId 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

RouteHandler
GET /api/appointment/public/:tokeninternal/appointment/handler.go(*Handler).GetJourneyByToken
GET /api/appointment/public/:token/slots(*Handler).GetAvailableSlots
GET /api/appointment/public/:token/dates(*Handler).GetAvailableDates
  • internal/appointment/register.goRegister(r, deps)
  • internal/appointment/service.go(*ServiceLayer).GetJourneyByToken, JourneyView
  • internal/appointment/slotengine.goNewSlotEngine, GetAvailableSlots, GetAvailableDates, IsSlotAvailable, weekday, resolveWorkingHours, generateTimeSlots, timeToMinutes, splitHM, truthy, firstString, and the Slot type
  • internal/appointment/repository.goFindActiveJourneyByToken, FindLocationByID, FindActiveServicesByLocation, FindActiveStaffByLocation, FindLocationSlotConfig, FindServiceDuration, FindExistingBookings
  • internal/appointment/handler.gojsNumberInt, which reproduces JavaScript's Number() coercion

Connections to Other Services

  • Database — tables appointment.journey, appointment.location (jsonb working_hours and blocked_dates), appointment.service (duration_minutes, max_bookings_per_slot, requires_staff), appointment.staff (jsonb service_ids), and appointment.booking
  • Related features — consumed by booking creation, which calls IsSlotAvailable as its double-booking guard
  • client-web — corresponds to the appointment-booking feature