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.ts—isValidTransition(),isAdminTransition() - Frontend:
booking-web/src/lib/booking-status.constants.ts—STATUS_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 khiforce=trueforce: 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 machine403 INSUFFICIENT_ROLE— role không cho phép422 BOOKING_CANCELLATION_TOO_LATE— guard fail422 BOOKING_DEPOSIT_REQUIRED— guard fail404 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ớiSTATUS_TEXT_COLOR - Filter: multi-select status với
BOOKING_STATUSES
9. Related Invariants
- Resource availability check xảy ra tại create + update (time/resource change), KHÔNG tại status transition (trừ
→ IN_PROGRESScheck không conflict với IN_PROGRESS booking khác). - Business hours check xảy ra tại create + update, KHÔNG tại status transition.
- Deposit status độc lập với booking status nhưng bị cross-referenced trong guards (CONFIRMED guard).
- BookingAuditLog ghi tất cả transitions (by whom, from, to, at, reason) — đã có sẵn.
- Soft delete: KHÔNG dùng hard delete cho booking. Thay vì delete → CANCELLED + flag.
10. Related Docs
booking-flow.md— Create/update flow, settings enforcementpayment-flow.md— Payment reactions to booking events../architecture/api-design.md— REST conventions../rules/development-rules.md— Testing, git, code quality