Skip to main content

Appointment Booking App

Overview

The appointment app lets customers run a booking system through LINE. Its data model has five parts.

EntityMeaning
LocationA branch or service site
ServiceA bookable service, tied to a location
StaffA staff member providing the service, tied to a location
JourneyThe booking path or steps the end user sees
BookingAn actual reservation

The cms-api side covers configuration and reservation management; end users make their bookings through client-web and client-api.

A note on inconsistent guards, carried over verbatim from the original TypeScript: booking endpoints sit on the authed group with AppEnabledGuard; journey, service, and staff endpoints sit on the public group with JwtLoginAuth; and some location routes sit on authed with no policy at all.

Business Flow

Initial Setup

  1. Create a location with POST /api/apps/appointment/locations.
  2. Add services to that location with POST /api/apps/appointment/locations/:id/services.
  3. Add staff to that location with POST /api/apps/appointment/locations/:id/staff.
  4. Design the booking journey with POST /api/apps/appointment/journeys.

End Users Book

  1. The user opens the LIFF app from LINE and picks a location, service, staff member, and time before confirming. This path runs through client-api, not the endpoints documented here.
  2. The system inserts a new row into the booking table.

Admins Manage Bookings

  1. GET /api/apps/appointment/bookings with FilterBookingDto lists reservations.
  2. GET /api/apps/appointment/bookings/:id returns a reservation's details.
  3. PUT /api/apps/appointment/bookings/:id/status changes the status — confirmed, cancelled, completed, and so on.
    • The status change publishes to the RabbitMQ queue booking_event_trigger, with a payload kept byte-for-byte identical to the original, key ordering included.
    • This is what allows Automated Trigger Rules to notify the customer afterwards.

Shared CRUD Pattern

Every sub-entity in service_appointment.go follows the same shape:

  • _getCls() pulls lineOaId, organizationId, selfId, and lang out of CLS.
  • findAll and findAllByLocation build the where and order clauses, filter out deleted rows, and return a bare array rather than an envelope.
  • findById matches on the id together with lineOaId; a miss raises a NotFoundException.
  • create, update, and remove, where remove is a soft delete.
  • These entities carry gorm.DeletedAt, so GORM adds deleted_date IS NULL and converts Delete into a soft delete automatically.

Key Files & Functions

The code lives in internal/modules/apps/, made up of controller.go, service.go, service_appointment.go, and guard.go.

Bookings (authed group + AppEnabledGuard)

MethodRouteHandlerPolicy
GET/api/apps/appointment/bookingsct.bookingFindAllreadAll line-oa
GET/api/apps/appointment/bookings/:idct.bookingFindByIdread line-oa
PUT/api/apps/appointment/bookings/:id/statusct.bookingUpdateStatusupdate line-oa

Journeys (public group + JwtLoginAuth)

MethodRouteHandler
GET/api/apps/appointment/journeysct.journeyFindAll
GET/api/apps/appointment/journeys/:idct.journeyFindById
POST/api/apps/appointment/journeysct.journeyCreate
PUT/api/apps/appointment/journeys/:idct.journeyUpdate
DELETE/api/apps/appointment/journeys/:idct.journeyRemove

Locations

MethodRouteHandlerGroup
GET/api/apps/appointment/locationsct.locationFindAllauthed (no policy)
POST/api/apps/appointment/locationsct.locationCreateauthed
GET/api/apps/appointment/locations/:idct.locationFindByIdauthed
PUT/api/apps/appointment/locations/:idct.locationUpdateauthed
DELETE/api/apps/appointment/locations/:idct.locationRemoveauthed

Services and Staff (public group + JwtLoginAuth)

MethodRouteHandler
GET/api/apps/appointment/locations/:id/servicesct.serviceFindAllByLocation
POST/api/apps/appointment/locations/:id/servicesct.serviceCreate
GET/api/apps/appointment/services/:idct.serviceFindById
PUT/api/apps/appointment/services/:idct.serviceUpdate
DELETE/api/apps/appointment/services/:idct.serviceRemove
GET/api/apps/appointment/locations/:id/staffct.staffFindAllByLocation
POST/api/apps/appointment/locations/:id/staffct.staffCreate
GET/api/apps/appointment/staff/:idct.staffFindById
PUT/api/apps/appointment/staff/:idct.staffUpdate
DELETE/api/apps/appointment/staff/:idct.staffRemove

A routing note: these routes use :id instead of :locationId because gin requires parameter names to be consistent within the same route subtree. The original TypeScript used :locationId.

Connections to Other Services