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:
28 files changed
+4853
-15
No files matched your search
+31
-12
@@ -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
|
||||
|
||||
Reference in new issue
Block a user