flows/booking-flow.md

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

  1. Settings data PHẢI đầy đủ trong DB — KHÔNG fallback, KHÔNG default ở runtime
  2. Tenant create flow (service/seed) PHẢI include đầy đủ settings từ getDefaultSettings()
  3. Mỗi setting mới thêm PHẢI có: API enforcement + UI enforcement + docs update
  4. Business hours so sánh bằng local time (salon timezone), KHÔNG dùng UTC

Settings Enforcement Status

✅ Enforced

  • businessHours — API + UI
  • allowDoubleBooking — API
  • autoConfirm — API
  • currency — UI (display)

⚠️ Needs Implementation

  • bookingMode — API phải reject unassigned khi assigned_only; UI phải required resourceId
  • walkInEnabled — API phải check trước tạo walk-in; UI phải ẩn walk-in button
  • cancellationHours — API phải check trước cancel; UI phải show deadline
  • posEnabled — 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

  1. Booking service raise event: eventBus.publish(new BookingCreated({...}))
  2. Event writes vào DomainEventOutbox cùng transaction với booking row (xem architecture §8)
  3. OutboxPublisher worker poll → dispatch via EventBus
  4. 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.