Skip to main content

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?}

  1. Authenticate, following the original controller's ladder exactly:
    • With an x-liff-token header present, call verifyAccessToken with 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, call verifyAccessToken using that value.
    • With neither, return 401 with No authentication token provided.
    • Note: this path uses verifyAccessToken alone and performs no channel binding, unlike the bulletin and loyalty domains. That is deliberate parity with the source.
    • The resulting userId is the profile's sub value.
  2. 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.
  3. Resolve the journey from :token — not found returns 404 with Journey not found.
  4. Load the service by serviceId — not found returns 404 with Service not found.
  5. Auto-assign a staff member when none was chosen and the service has requires_staff set:
    • Fetch eligible staff at the location, then keep only those who can perform this service by checking the service_ids jsonb, 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, staffId stays null and the booking still goes through.
  6. Re-check slot availability with SlotEngine.IsSlotAvailable — if it is taken, return 409 with Selected time slot is no longer available. This is the guard against two people booking simultaneously.
  7. Compute endTime as startTime plus service.duration_minutes, formatted as two-digit hours and minutes.
  8. Determine the initial status: journey.auto_confirm set gives confirmed (with the confirmed flag set); otherwise pending.
  9. Insert the appointment.booking row with journeyId, locationId, serviceId, staffId, userId, lineOaId, organizationId, bookingDate, startTime, endTime, status, notes, and formData.
  10. 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}
  11. Return the saved Booking entity with status 201.

Key Files & Functions

ItemValue
RoutePOST /api/appointment/public/:token/book (rate limit 5/60s)
Registerinternal/appointment/register.goRegister(r, deps), binding middleware.RouteRateLimit(rdb, 5, 60); without Redis the route is mounted without a limiter
Handlerinternal/appointment/handler.go(*Handler).CreateBooking, resolveUserID, bindJSON
Serviceinternal/appointment/service.go(*ServiceLayer).CreateBooking, publish, staffHasService
Repositoryinternal/appointment/repository.goFindServiceByID, FindEligibleStaffByLocation, FindStaffBookingsForDate, InsertBooking
Entityinternal/appointment/entity.goBooking, BookingBody, InsertBookingInput, intSlice, jsonMap

Connections to Other Services

  • Database — tables appointment.journey, appointment.service, appointment.staff, and appointment.booking
  • SlotEngine — calls IsSlotAvailable from the journey and available-slots feature
  • RabbitMQ — the booking_notification and booking_event_trigger queues, named via deps.Config.RabbitMQ.QueueBookingNotification and QueueBookingEventTrigger, consumed by line-management-worker-go
  • LINE Platform — reached through internal/linehttp via VerifyAccessToken
  • client-web — corresponds to the appointment-booking feature