flows/booking-status-flow.md

Booking Status Flow

BẮT BUỘC đọc trước khi code logic liên quan booking status / transition / cross-context event.

Formal state machine cho Booking entity: các states, valid transitions, guards, role matrix, và domain events publish tại từng transition.


1. States

Enum BookingStatus (source: prisma/schema.prisma, booking-status.constants.ts):

State Nghĩa Terminal? Tính tiền / deposit?
PENDING Mới tạo, chờ xác nhận (autoConfirm=false) No Deposit PENDING / AUTHORIZED
CONFIRMED Đã xác nhận, chờ khách đến No Deposit AUTHORIZED hoặc PAID
ARRIVED Khách đã check-in (ngồi chờ) No Deposit AUTHORIZED/PAID
IN_PROGRESS Staff đang phục vụ No Deposit PAID (capture trigger)
COMPLETED Dịch vụ xong, đã thanh toán full Yes Deposit PAID, full paid
CANCELLED Đã hủy (customer hoặc salon) Yes Deposit refunded / void / forfeit
NO_SHOW Khách không đến Yes Deposit forfeit (capture nếu AUTH)

Terminal states không có outgoing transition. Mọi thao tác tiếp theo (refund, note) KHÔNG đổi status.


2. State Machine (formal)

                    ┌─────────┐
       (create) ───▶│ PENDING │
                    └────┬────┘
                         │ confirm
                         ▼
                    ┌───────────┐
         ┌─────────▶│ CONFIRMED │◀────────────┐
         │          └────┬──────┘             │
         │               │ arrive             │
         │               ▼                    │ (skip ARRIVED,
         │          ┌─────────┐               │  direct start)
         │          │ ARRIVED │               │
         │          └────┬────┘               │
         │               │ start              │
         │               ▼                    │
         │          ┌─────────────┐           │
         │          │ IN_PROGRESS │◀──────────┘
         │          └──────┬──────┘
         │                 │ complete
         │                 ▼
         │            ┌───────────┐
         │            │ COMPLETED │  ← TERMINAL
         │            └───────────┘
         │
         ├── cancel (from PENDING/CONFIRMED/ARRIVED) ──▶ CANCELLED ← TERMINAL
         │
         └── mark no-show (from CONFIRMED/ARRIVED) ───▶ NO_SHOW   ← TERMINAL

Walk-in creation: PENDING skipped, start directly at IN_PROGRESS (source=WALK_IN)

Transition table (STAFF role)

From → CONFIRMED → ARRIVED → IN_PROGRESS → COMPLETED → CANCELLED → NO_SHOW
PENDING
CONFIRMED
ARRIVED
IN_PROGRESS
COMPLETED
CANCELLED
NO_SHOW

OWNER / ADMIN: được force chuyển bất kỳ state nào (override tất cả guards trừ terminal states). Dùng cho fix nhầm lẫn, edge case. Phải log lý do khi override.

Code reference

  • Backend: booking-api/src/core/booking/booking-status.constants.tsisValidTransition(), isAdminTransition()
  • Frontend: booking-web/src/lib/booking-status.constants.tsSTATUS_TRANSITIONS, ADMIN_STATUS_OPTIONS

Rule: 2 file phải match. Khi thêm/sửa transition → update cả 2 + test.


3. Guards (pre-conditions)

Ngoài state transition validity, mỗi transition có guards bổ sung:

Transition Guard Reject code
PENDING → CONFIRMED Nếu tenant.depositEnabled → payment status ∈ {AUTHORIZED, PAID} BOOKING_DEPOSIT_REQUIRED
* → CANCELLED (by customer) Trong cancellationHours window BOOKING_CANCELLATION_TOO_LATE
* → CANCELLED (by salon) Không check window (luật consumer)
CONFIRMED/ARRIVED → NO_SHOW now() > booking.startTime + graceMinutes (mặc định 15min) BOOKING_NO_SHOW_TOO_EARLY
IN_PROGRESS → COMPLETED (future) full payment received if POS enabled BOOKING_PAYMENT_NOT_COMPLETE
Any → IN_PROGRESS Resource không conflict với booking khác đang IN_PROGRESS BOOKING_RESOURCE_BUSY

Guards evaluate sau isValidTransition check, trước prisma.booking.update.


4. Role Matrix

Action CUSTOMER STAFF OWNER ADMIN
Create booking (own) ✓ (any customer)
Create walk-in
View booking (own)
View any booking in tenant
Confirm booking
Start / complete
Cancel (own) ✓ (within window)
Cancel (any, in-window)
Cancel (out-of-window override)
Mark no-show
Force any transition (override)
Refund (after capture)
Update booking fields
Delete booking ✓ (soft)

Enforcement:

  • Backend: NestJS @Roles() decorator + guards
  • Frontend: conditional render buttons, nhưng backend là last line of defense

5. Cross-Context Domain Events

Mỗi transition PHẢI publish domain event vào DomainEventOutbox trong cùng transaction với booking update. Payment Context + Notification Context subscribe.

Transition Event Payload
(create) BookingCreated {bookingId, tenantId, customerId, totalAmount, startTime, requiresDeposit}
PENDING → CONFIRMED BookingConfirmed {bookingId, confirmedAt, confirmedBy}
CONFIRMED → ARRIVED BookingArrived {bookingId, arrivedAt}
* → IN_PROGRESS BookingStarted {bookingId, startedAt, startedBy}
IN_PROGRESS → COMPLETED BookingCompleted {bookingId, completedAt, totalAmount}
* → CANCELLED (by customer) BookingCancelled {bookingId, cancelledAt, cancelledBy, reason, byCustomer: true}
* → CANCELLED (by salon) BookingCancelledBySalon {bookingId, cancelledAt, reason}
* → NO_SHOW BookingMarkedNoShow {bookingId, markedAt, markedBy}
(update fields) BookingUpdated {bookingId, changedFields: {...}}

Publishing mechanism

Trong booking service, sau prisma.booking.update:

await this.prisma.$transaction(async (tx) => {
  const updated = await tx.booking.update({...});
  await tx.domainEventOutbox.create({
    data: {
      aggregateId: bookingId,
      aggregateType: 'Booking',
      tenantId,
      eventType: 'BookingConfirmed',
      payload: { bookingId, confirmedAt: new Date(), confirmedBy: userId },
      occurredAt: new Date(),
    },
  });
});
// OutboxPublisher worker picks up async → EventBus.publish

Chưa có DomainEventOutbox model — sẽ add ở Phase 0 Payment work. Trước đó booking service vẫn dùng BookingAuditLog đã có.


6. Side Effects per Transition

Transition Side effect
create Notification: send confirmation email/SMS nếu có customerPhone/Email
→ CONFIRMED Notification: "Booking confirmed" + deposit receipt nếu có
→ ARRIVED Notification (optional): notify staff assigned
→ IN_PROGRESS Payment (manual mode): capture authorization
→ COMPLETED Loyalty: increment stamps / points. Invoice generation
→ CANCELLED (customer) Payment: void / refund theo CancellationRefundPolicy. Notification: confirmation
→ CANCELLED (salon) Payment: full refund always. Notification: apology + refund ETA
→ NO_SHOW Payment: capture (forfeit). Stats: increment customer no-show counter

7. API Endpoint

POST /bookings/:id/status/:status
Body: { reason?: string, force?: boolean }
  • status: target state (uppercase)
  • reason: required khi CANCELLED hoặc khi force=true
  • force: only OWNER/ADMIN, bypass transition validity check

Response:

{
  "success": true,
  "data": {
    "id": "...",
    "status": "CONFIRMED",
    "updatedAt": "...",
    "previousStatus": "PENDING"
  }
}

Errors:

  • 400 BOOKING_INVALID_STATE_TRANSITION — không valid theo state machine
  • 403 INSUFFICIENT_ROLE — role không cho phép
  • 422 BOOKING_CANCELLATION_TOO_LATE — guard fail
  • 422 BOOKING_DEPOSIT_REQUIRED — guard fail
  • 404 BOOKING_NOT_FOUND

8. Frontend UX Conventions

Quick actions (primary buttons)

Shown based on current status (STATUS_TRANSITIONS map). Max 2-3 buttons cùng lúc:

  • PENDING: [Confirm] [Cancel]
  • CONFIRMED: [Mark arrived] [Start] [Cancel] [No show]
  • ARRIVED: [Start] [Cancel] [No show]
  • IN_PROGRESS: [Complete]
  • COMPLETED/CANCELLED/NO_SHOW: no quick actions (terminal)

Button variant:

  • Positive (primary): Confirm, Mark arrived, Start, Complete
  • Negative (destructive): Cancel, No show (+ ConfirmDialog)

Admin override

OWNER/ADMIN có dropdown "Change status" với tất cả 7 states (ADMIN_STATUS_OPTIONS). Khi chọn state ngoài standard transition → modal require reason.

Status display

  • Table list: Badge với STATUS_BADGE_COLOR (shadcn variant)
  • Calendar block: background với STATUS_DOT_COLOR, text với STATUS_TEXT_COLOR
  • Filter: multi-select status với BOOKING_STATUSES

  1. Resource availability check xảy ra tại create + update (time/resource change), KHÔNG tại status transition (trừ → IN_PROGRESS check không conflict với IN_PROGRESS booking khác).
  2. Business hours check xảy ra tại create + update, KHÔNG tại status transition.
  3. Deposit status độc lập với booking status nhưng bị cross-referenced trong guards (CONFIRMED guard).
  4. BookingAuditLog ghi tất cả transitions (by whom, from, to, at, reason) — đã có sẵn.
  5. Soft delete: KHÔNG dùng hard delete cho booking. Thay vì delete → CANCELLED + flag.