Booking Flow & Settings Guide
BẮT BUỘC đọc trước khi viết code liên quan booking hoặc tenant settings.
Booking Create Flow
User fills BookingDrawer form
→ Frontend: skill validation (resource.skills vs serviceId)
→ Frontend: zod schema (items.min(1), resourceId.min(1), date required)
→ POST /bookings (BookingController.create)
→ normalizeItems(): backward-compat serviceId → items[]
→ resolveItems(): lookup services from DB, snapshot duration + price
→ calculateTimes: startTime + totalDuration → endTime
→ parseTenantSettings(tenant.settings)
→ validateBusinessHours(settings, startTime, endTime) ← MUST PASS
→ shouldSkipConflict(settings, forceOverlap, userRole)
├─ allowDoubleBooking=true → skip all conflict checks
└─ forceOverlap=true + OWNER/ADMIN → skip conflict checks
→ validateBookingMode(settings, items) ← MUST PASS
→ For each item with resourceId:
├─ validateResourceSkill(resourceId, serviceId)
└─ checkConflict(tenantId, resourceId, startTime, endTime)
→ resolveInitialStatus(settings)
├─ autoConfirm=true → CONFIRMED
└─ autoConfirm=false → PENDING
→ prisma.booking.create(...)
→ fire-and-forget notification
Booking Update Flow
User edits in BookingDrawer
→ Frontend: skill validation + zod schema
→ PATCH /bookings/:id (BookingController.update)
→ findById(): verify booking exists + tenant ownership
→ Payment guard: isPaid only for IN_PROGRESS/COMPLETED
→ parseTenantSettings(tenant.settings)
→ validateBusinessHours() if time is changing ← MUST PASS
→ shouldSkipConflict(settings)
→ If items provided: resolveItems + validateResourceSkill + checkConflict
→ If scalar only: checkConflict when resourceId/time changing
→ prisma.booking.update(...)
Walk-In Flow
POST /bookings/walk-in (OWNER/STAFF/ADMIN only)
→ Validate walkInEnabled setting ← MUST PASS
→ validateResourceSkill
→ validateBusinessHours ← MUST PASS
→ Create with status=IN_PROGRESS, startTime=now
Status Transition Flow
POST /bookings/:id/status/:status
→ isValidTransition(current, new)
→ If cancelling: validate cancellationHours window ← MUST PASS
→ prisma.booking.update(status)
→ Notify on CONFIRMED/CANCELLED
Tenant Settings Reference
Mỗi setting PHẢI được enforce ở cả API lẫn UI. KHÔNG có dead settings.
| Setting | Type | API Enforcement | UI Enforcement |
|---|---|---|---|
businessHours |
BusinessHours[] | Block booking ngoài giờ (create + update + walkIn) | Calendar viewport, TimePicker min/max, grey-out off-hours slots |
allowDoubleBooking |
boolean | Skip conflict check (create + update) | Settings toggle |
autoConfirm |
boolean | Initial status CONFIRMED vs PENDING (create) | Settings toggle |
bookingMode |
'assigned_only' | 'allow_unassigned' | Require resourceId khi assigned_only (create) | BookingDrawer: resourceId required/optional based on mode |
walkInEnabled |
boolean | Block walk-in endpoint khi disabled | Hide/show walk-in button |
cancellationHours |
number | Block cancel khi quá gần startTime | Show warning, disable cancel button khi hết deadline |
currency |
string | Display metadata (không validate prices) | Format money display (useCurrency + useFormatMoney) |
posEnabled |
boolean | Gate POS features (future) | Show/hide POS tab (future) |
depositEnabled |
boolean | Trigger payment flow khi tạo booking (publish BookingCreated → Payment context init) |
Show deposit field in booking summary, block submit nếu provider chưa config |
depositType |
'percentage' | 'fixed' | Pass vào FeeCalculationPolicy khi init payment |
Settings UI conditional input (% vs minor units) |
depositValue |
number (int) | Compute deposit amount (percentage 0–100, fixed minor units) | Settings UI validate (≤100 nếu %) |
Rules
- Settings data PHẢI đầy đủ trong DB — KHÔNG fallback, KHÔNG default ở runtime
- Tenant create flow (service/seed) PHẢI include đầy đủ settings từ
getDefaultSettings() - Mỗi setting mới thêm PHẢI có: API enforcement + UI enforcement + docs update
- Business hours so sánh bằng local time (salon timezone), KHÔNG dùng UTC
Settings Enforcement Status
✅ Enforced
businessHours— API + UIallowDoubleBooking— APIautoConfirm— APIcurrency— UI (display)
⚠️ Needs Implementation
bookingMode— API phải reject unassigned khiassigned_only; UI phải required resourceIdwalkInEnabled— API phải check trước tạo walk-in; UI phải ẩn walk-in buttoncancellationHours— API phải check trước cancel; UI phải show deadlineposEnabled— Feature chưa build (keep setting, implement later)depositEnabled/depositType/depositValue— UI admin done; Payment context implementation pending (xem../architecture/payment-architecture.md)
Integration với Payment Context
Chi tiết:
../architecture/payment-architecture.md,payment-flow.md
Booking Context publish domain events tại các lifecycle moment. Payment Context listen + react độc lập. Booking KHÔNG biết Payment tồn tại, KHÔNG direct call Payment service.
Events Booking publish
| Event | Trigger point | Payload (key fields) |
|---|---|---|
BookingCreated |
Sau prisma.booking.create success |
bookingId, tenantId, totalAmount, startTime, requiresDeposit |
BookingConfirmed |
Sau status transition → CONFIRMED | bookingId, confirmedAt |
BookingCancelled |
Sau status transition → CANCELLED (by customer) | bookingId, cancelledAt, reason |
BookingCancelledBySalon |
Sau cancel with bySalon=true |
bookingId, cancelledAt, reason |
BookingMarkedNoShow |
Sau status transition → NO_SHOW | bookingId, markedAt |
BookingCompleted |
Sau status transition → COMPLETED | bookingId, completedAt |
Mechanism
- Booking service raise event:
eventBus.publish(new BookingCreated({...})) - Event writes vào
DomainEventOutboxcùng transaction với booking row (xem architecture §8) OutboxPublisherworker poll → dispatch via EventBus- Payment Context listeners process
Events Payment publish (Booking listen)
| Event | Booking reaction |
|---|---|
PaymentCaptured |
booking.depositStatus = PAID |
PaymentAuthorized |
booking.depositStatus = AUTHORIZED |
PaymentRefunded |
booking.depositStatus = REFUNDED |
PaymentVoided |
booking.depositStatus = VOIDED |
PaymentFailed |
booking.depositStatus = PAYMENT_FAILED |
Booking không block chờ Payment
Booking create response KHÔNG chờ Payment init. Flow async qua outbox. Frontend poll hoặc WebSocket push để nhận update status.
depositStatus field trên Booking
Không phải enum strict — string với values: NOT_REQUIRED | PENDING | AUTHORIZED | PAID | PARTIALLY_REFUNDED | REFUNDED | VOIDED | PAYMENT_FAILED | EXPIRED.
Booking Context chỉ read-only update dựa trên events; business logic deposit thuộc Payment Context.