Appointment Booking App
Overview
The appointment app lets customers run a booking system through LINE. Its data model has five parts.
| Entity | Meaning |
|---|---|
| Location | A branch or service site |
| Service | A bookable service, tied to a location |
| Staff | A staff member providing the service, tied to a location |
| Journey | The booking path or steps the end user sees |
| Booking | An 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
- Create a location with
POST /api/apps/appointment/locations. - Add services to that location with
POST /api/apps/appointment/locations/:id/services. - Add staff to that location with
POST /api/apps/appointment/locations/:id/staff. - Design the booking journey with
POST /api/apps/appointment/journeys.
End Users Book
- 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.
- The system inserts a new row into the
bookingtable.
Admins Manage Bookings
GET /api/apps/appointment/bookingswithFilterBookingDtolists reservations.GET /api/apps/appointment/bookings/:idreturns a reservation's details.PUT /api/apps/appointment/bookings/:id/statuschanges 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.
- The status change publishes to the RabbitMQ queue
Shared CRUD Pattern
Every sub-entity in service_appointment.go follows the same shape:
_getCls()pullslineOaId,organizationId,selfId, andlangout of CLS.findAllandfindAllByLocationbuild the where and order clauses, filter out deleted rows, and return a bare array rather than an envelope.findByIdmatches on the id together withlineOaId; a miss raises a NotFoundException.create,update, andremove, where remove is a soft delete.- These entities carry
gorm.DeletedAt, so GORM addsdeleted_date IS NULLand 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)
| Method | Route | Handler | Policy |
|---|---|---|---|
| GET | /api/apps/appointment/bookings | ct.bookingFindAll | readAll line-oa |
| GET | /api/apps/appointment/bookings/:id | ct.bookingFindById | read line-oa |
| PUT | /api/apps/appointment/bookings/:id/status | ct.bookingUpdateStatus | update line-oa |
Journeys (public group + JwtLoginAuth)
| Method | Route | Handler |
|---|---|---|
| GET | /api/apps/appointment/journeys | ct.journeyFindAll |
| GET | /api/apps/appointment/journeys/:id | ct.journeyFindById |
| POST | /api/apps/appointment/journeys | ct.journeyCreate |
| PUT | /api/apps/appointment/journeys/:id | ct.journeyUpdate |
| DELETE | /api/apps/appointment/journeys/:id | ct.journeyRemove |
Locations
| Method | Route | Handler | Group |
|---|---|---|---|
| GET | /api/apps/appointment/locations | ct.locationFindAll | authed (no policy) |
| POST | /api/apps/appointment/locations | ct.locationCreate | authed |
| GET | /api/apps/appointment/locations/:id | ct.locationFindById | authed |
| PUT | /api/apps/appointment/locations/:id | ct.locationUpdate | authed |
| DELETE | /api/apps/appointment/locations/:id | ct.locationRemove | authed |
Services and Staff (public group + JwtLoginAuth)
| Method | Route | Handler |
|---|---|---|
| GET | /api/apps/appointment/locations/:id/services | ct.serviceFindAllByLocation |
| POST | /api/apps/appointment/locations/:id/services | ct.serviceCreate |
| GET | /api/apps/appointment/services/:id | ct.serviceFindById |
| PUT | /api/apps/appointment/services/:id | ct.serviceUpdate |
| DELETE | /api/apps/appointment/services/:id | ct.serviceRemove |
| GET | /api/apps/appointment/locations/:id/staff | ct.staffFindAllByLocation |
| POST | /api/apps/appointment/locations/:id/staff | ct.staffCreate |
| GET | /api/apps/appointment/staff/:id | ct.staffFindById |
| PUT | /api/apps/appointment/staff/:id | ct.staffUpdate |
| DELETE | /api/apps/appointment/staff/:id | ct.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
- Access control — The app must be enabled for the organization first, enforced by
AppEnabledGuard(see the Add-on Apps Platform). Policy metadata usesPolicyModuleLineOa. - Tables —
booking,location,service,staff,journey,line_oa_app, andline_oa - RabbitMQ — The
booking_event_triggerqueue, published on booking status changes. - Related modules — Add-on Apps Platform, Automated Trigger Rules, the Loyalty & Rewards App, and the Bulletin Board App.