Booking Engine

Architecture: Six focused services with the BookingService/SchedulingService overlap resolved by giving SchedulingService exactly one job, the transactional double-booking guard , while BookingService owns the create/reschedule/cancel workflow around it.

59 unit tests, 24 integration tests
This commit is contained in:
Stefan Lazic committed 2026-08-02 18:36:39 +01:00
1 parent 090646fdfd
commit 8b63cc66a9
28 files changed
+4853 -15

No files matched your search

+31 -12
View File
@@ -61,10 +61,16 @@ enum StaffStatus {
INACTIVE
}
/// PENDING -> CONFIRMED -> CHECKED_IN -> IN_PROGRESS -> COMPLETED is the
/// happy path. CANCELLED is reachable from PENDING/CONFIRMED/CHECKED_IN.
/// NO_SHOW is reachable only from CONFIRMED (once CHECKED_IN, the customer
/// did show up). COMPLETED/CANCELLED/NO_SHOW are terminal. The transition
/// map itself lives in services/scheduling/appointment.service.ts, not
/// here — this enum is just the vocabulary.
enum AppointmentStatus {
PENDING_APPROVAL
PENDING
CONFIRMED
ARRIVED
CHECKED_IN
IN_PROGRESS
COMPLETED
CANCELLED
@@ -226,15 +232,19 @@ model BookingRule {
businessId String @unique
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade)
minNoticeMinutes Int @default(120)
maxAdvanceDays Int @default(60)
cancellationWindowHours Int @default(24)
rescheduleWindowHours Int @default(24)
slotIntervalMinutes Int @default(30)
bufferBeforeMinutes Int @default(0)
bufferAfterMinutes Int @default(5)
/// null = no daily cap.
maxAppointmentsPerDay Int?
minNoticeMinutes Int @default(120)
maxAdvanceDays Int @default(60)
cancellationWindowHours Int @default(24)
rescheduleWindowHours Int @default(24)
slotIntervalMinutes Int @default(30)
bufferBeforeMinutes Int @default(0)
bufferAfterMinutes Int @default(5)
/// null = no daily cap. Business-wide total across all staff.
maxAppointmentsPerDay Int?
/// null = no per-staff cap. Distinct from maxAppointmentsPerDay — this
/// limits how many appointments any single staff member can take in a
/// day, independent of the shop-wide total.
maxAppointmentsPerStaffPerDay Int?
customerCanChooseStaff Boolean @default(true)
allowGuestBooking Boolean @default(true)
@@ -281,6 +291,9 @@ model User {
staffProfile StaffProfile?
notifications Notification[]
notificationPreferences NotificationPreference[]
/// Whoever actually cancelled an appointment — the customer themselves,
/// or the staff member who did it on their behalf.
cancelledAppointments Appointment[] @relation("AppointmentCancelledBy")
}
model Account {
@@ -577,8 +590,14 @@ model Appointment {
startAt DateTime
endAt DateTime
notes String? @db.Text
notes String? @db.Text
/// Cancellation is a status transition, never a delete — these three
/// fields are only ever set together, when status becomes CANCELLED.
cancellationReason String?
cancelledAt DateTime?
cancelledByUserId String?
cancelledByUser User? @relation("AppointmentCancelledBy", fields: [cancelledByUserId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt