Skip to main content

Appointment Booking

Column-level detail for the entire appointment schema (location/service/staff/journey/booking)

Overview

The appointment schema has 5 tables. In reference order: location (branches) → service (bookable services) / staff (providers) / journey (booking flow configuration with the public LIFF token), all feeding into booking (actual reservations). This schema is applied separately with psql rather than through Prisma (migrations 001-004).

Table appointment.location

ColumnTypeNullableDefaultDescription
idSERIALNOauto incrementPrimary key
line_oa_idINTEGERNO-LINE OA this branch belongs to
organization_idINTEGERNO-Owning organization
nameVARCHAR(255)NO-Branch name
descriptionTEXTYES-Branch description
addressTEXTYES-Branch address
phoneVARCHAR(50)YES-Contact phone number
working_hoursJSONBNO'{}'Per-day opening hours, used to compute bookable slots
blocked_datesJSONBYES'[]'Dates the branch is closed or not accepting bookings
timezoneVARCHAR(50)NO'Asia/Bangkok'Branch timezone
statusVARCHAR(20)NO'active'Record status
sort_orderINTEGERNO0Display order
created_byINTEGERYES-Creator user id
updated_byINTEGERYES-Last updater user id
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPCreation timestamp
updated_dateTIMESTAMPTZ(3)YES-Last update timestamp
deleted_dateTIMESTAMPTZ(3)YES-Soft-delete timestamp

Table appointment.service

ColumnTypeNullableDefaultDescription
idSERIALNOauto incrementPrimary key
location_idINTEGERNO-FK to appointment.location(id), ON DELETE CASCADE
line_oa_idINTEGERNO-LINE OA this service belongs to
organization_idINTEGERNO-Owning organization
nameVARCHAR(255)NO-Service name
descriptionTEXTYES-Service description
duration_minutesINTEGERNO30Service duration in minutes, used to derive the end time
max_bookings_per_slotINTEGERNO1Maximum bookings allowed in one time slot
requires_staffBOOLEANNOfalseWhether a provider must be chosen for this service
priceDECIMAL(10,2)YES-Service price
currencyVARCHAR(10)YES'THB'Price currency
statusVARCHAR(20)NO'active'Record status
sort_orderINTEGERNO0Display order
created_byINTEGERYES-Creator user id
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPCreation timestamp
updated_dateTIMESTAMPTZ(3)YES-Last update timestamp
deleted_dateTIMESTAMPTZ(3)YES-Soft-delete timestamp

Table appointment.staff

ColumnTypeNullableDefaultDescription
idSERIALNOauto incrementPrimary key
location_idINTEGERNO-FK to appointment.location(id), ON DELETE CASCADE
line_oa_idINTEGERNO-LINE OA this provider belongs to
organization_idINTEGERNO-Owning organization
nameVARCHAR(255)NO-Provider name
titleVARCHAR(255)YES-Job title shown next to the name
avatar_urlVARCHAR(500)YES-Profile picture URL
service_idsJSONBYES'[]'Ids of the services this provider can deliver
statusVARCHAR(20)NO'active'Record status
sort_orderINTEGERNO0Display order
created_byINTEGERYES-Creator user id
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPCreation timestamp
updated_dateTIMESTAMPTZ(3)YES-Last update timestamp
deleted_dateTIMESTAMPTZ(3)YES-Soft-delete timestamp

Table appointment.journey

ColumnTypeNullableDefaultDescription
idSERIALNOauto incrementPrimary key
location_idINTEGERNO-FK to appointment.location(id), ON DELETE CASCADE
line_oa_idINTEGERNO-LINE OA this flow belongs to
organization_idINTEGERNO-Owning organization
nameVARCHAR(255)NO-Booking flow name
descriptionTEXTYES-Flow description
tokenVARCHAR(64)NO-Public token that opens the LIFF booking page (unique)
auto_confirmBOOLEANNOtrueConfirm bookings automatically, or hold for admin approval
stepsJSONBYES-Step / form definition shown during booking
statusVARCHAR(20)NO'active'Record status
created_byINTEGERYES-Creator user id
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPCreation timestamp
updated_dateTIMESTAMPTZ(3)YES-Last update timestamp
deleted_dateTIMESTAMPTZ(3)YES-Soft-delete timestamp
reminder_hours_beforeINTEGERYES24How many hours before the appointment the reminder is sent (migration 003)
no_show_grace_minutesINTEGERYES30Grace period in minutes before a booking counts as a no-show (migration 003)

Table appointment.booking

ColumnTypeNullableDefaultDescription
idSERIALNOauto incrementPrimary key
journey_idINTEGERNO-FK to appointment.journey(id) — the flow used to book
location_idINTEGERNO-FK to appointment.location(id) — the branch booked
service_idINTEGERNO-FK to appointment.service(id) — the service booked
staff_idINTEGERYES-FK to appointment.staff(id) — the provider, if chosen
user_idVARCHAR(255)NO-LINE user id of the person booking
line_oa_idINTEGERNO-LINE OA this booking belongs to
organization_idINTEGERNO-Owning organization
booking_dateDATENO-Appointment date
start_timeTIMENO-Appointment start time
end_timeTIMENO-Appointment end time
statusVARCHAR(20)NO'pending'Booking status
notesTEXTYES-Notes from the customer or the admin
form_dataJSONBYES-Answers to the form defined in journey.steps
cancel_reasonTEXTYES-Cancellation reason
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPCreation timestamp
updated_dateTIMESTAMPTZ(3)YES-Last update timestamp
confirmed_dateTIMESTAMPTZ(3)YES-When the booking was confirmed
cancelled_dateTIMESTAMPTZ(3)YES-When the booking was cancelled
completed_dateTIMESTAMPTZ(3)YES-When the service was completed
reminder_sentBOOLEANYESfalseWhether the advance reminder has been sent (migration 003)

Notes

  • Foreign keys: service.location_id, staff.location_id and journey.location_id all point at appointment.location(id) with ON DELETE CASCADE, so deleting a branch removes its children. The four FKs on booking (journey_id, location_id, service_id, staff_id) are not cascading, so master data cannot be deleted out from under existing reservations
  • Unique: appointment.journey.token is unique both as a column constraint and via index idx_appt_journey_token, since it is the public key that opens the booking page
  • Partial indexes: the status-filtered indexes on location, service, staff and journey all carry WHERE deleted_date IS NULL, matching queries that only look at live rows. idx_appt_booking_reminder carries WHERE reminder_sent = false so the reminder worker scans only rows still awaiting a notification
  • Triggers (migration 004): trg_booking_event_insert and trg_booking_event_update on appointment.booking call appointment.notify_booking_event(), which issues a pg_notify on the booking_event channel (type booking_created on INSERT, booking_status_changed when status changes), consumed by the pg-listener worker. The same function also attempts an INSERT into event_outbox for reliability, silently skipping if that table does not exist
  • booking.user_id stores the LINE user id as a string; it is not a foreign key to line_user in the public schema