Creating a Booking
Overview
The only write endpoint in the appointment domain. It accepts booking details from the LIFF page, authenticates the person booking via their LIFF token, auto-assigns a staff member when required, re-checks that the slot is genuinely still free, computes the end time, writes the row to appointment.booking, and publishes two events to RabbitMQ — one for notifications and one to trigger workflows.
A rate limit of 5 requests per 60 seconds per IP applies, matching the original throttle configuration.
Business Flow
POST /api/appointment/public/:token/book (rate limit 5/60s)
Request body: {serviceId, staffId?, bookingDate, startTime, notes?, formData?}
- Authenticate, following the original controller's ladder exactly:
- With an
x-liff-tokenheader present, callverifyAccessTokenwith whichever access token is available; on failure, retry with the LIFF token; on a second failure, return LINE's error as a 401. - Without that header but with
x-liff-access-token, callverifyAccessTokenusing that value. - With neither, return 401 with
No authentication token provided. - Note: this path uses
verifyAccessTokenalone and performs no channel binding, unlike the bulletin and loyalty domains. That is deliberate parity with the source. - The resulting
userIdis the profile'ssubvalue.
- With an
- An empty body leaves every field at its zero value without erroring; malformed JSON returns 400 with
Invalid request body. The original source had no DTO validation on this route. - Resolve the journey from
:token— not found returns 404 withJourney not found. - Load the service by
serviceId— not found returns 404 withService not found. - Auto-assign a staff member when none was chosen and the service has
requires_staffset:- Fetch eligible staff at the location, then keep only those who can perform this service by checking the
service_idsjsonb, accepting both numeric and string ids. - Pick the first person with no overlapping booking that day, using the condition
slotStart < bookingEnd && slotEnd > bookingStart. - If nobody is free,
staffIdstays null and the booking still goes through.
- Fetch eligible staff at the location, then keep only those who can perform this service by checking the
- Re-check slot availability with
SlotEngine.IsSlotAvailable— if it is taken, return 409 withSelected time slot is no longer available. This is the guard against two people booking simultaneously. - Compute
endTimeasstartTimeplusservice.duration_minutes, formatted as two-digit hours and minutes. - Determine the initial status:
journey.auto_confirmset givesconfirmed(with the confirmed flag set); otherwisepending. - Insert the
appointment.bookingrow withjourneyId,locationId,serviceId,staffId,userId,lineOaId,organizationId,bookingDate,startTime,endTime,status,notes, andformData. - Publish two messages, best-effort — all errors are swallowed, so a RabbitMQ outage never fails the booking.
- Queue
booking_notification— payload{bookingId, journeyId, locationId, serviceId, staffId, userId, lineOaId, organizationId, status} - Queue
booking_event_trigger— payload{type:"booking_created", bookingId, serviceId, serviceName, staffId, bookingDate, startTime, endTime, locationId, journeyId, status, userId, lineOaId, organizationId}
- Queue
- Return the saved
Bookingentity with status 201.
Key Files & Functions
| Item | Value |
|---|---|
| Route | POST /api/appointment/public/:token/book (rate limit 5/60s) |
| Register | internal/appointment/register.go → Register(r, deps), binding middleware.RouteRateLimit(rdb, 5, 60); without Redis the route is mounted without a limiter |
| Handler | internal/appointment/handler.go → (*Handler).CreateBooking, resolveUserID, bindJSON |
| Service | internal/appointment/service.go → (*ServiceLayer).CreateBooking, publish, staffHasService |
| Repository | internal/appointment/repository.go → FindServiceByID, FindEligibleStaffByLocation, FindStaffBookingsForDate, InsertBooking |
| Entity | internal/appointment/entity.go → Booking, BookingBody, InsertBookingInput, intSlice, jsonMap |
Connections to Other Services
- Database — tables
appointment.journey,appointment.service,appointment.staff, andappointment.booking - SlotEngine — calls
IsSlotAvailablefrom the journey and available-slots feature - RabbitMQ — the
booking_notificationandbooking_event_triggerqueues, named viadeps.Config.RabbitMQ.QueueBookingNotificationandQueueBookingEventTrigger, consumed by line-management-worker-go - LINE Platform — reached through
internal/linehttpviaVerifyAccessToken - client-web — corresponds to the
appointment-bookingfeature