flows/status-matrix/02-happy-path.md
02 · Happy Path Transitions
Các transition thuận: PENDING → CONFIRMED → ARRIVED → IN_PROGRESS → COMPLETED. Không bao gồm cancel / no-show (xem 03-cancel.md, 04-no-show.md).
Endpoint chung: POST /bookings/:id/status/:status (roles: OWNER/STAFF/ADMIN). Flow:
updateStatus() → isValidTransition OR isAdminTransition → guard → prisma.update + outbox enqueue → audit log → notification.
State diagram + capture trigger
stateDiagram-v2
[*] --> PENDING: BookingCreated\n(default)
PENDING --> CONFIRMED: T1\nguard C2: deposit-required (P0-2)\nautoConfirm OR PaymentAuthorized listener
CONFIRMED --> ARRIVED: T2\nstaff bấm "Check-in"
CONFIRMED --> IN_PROGRESS: T3\nstaff skip ARRIVED
ARRIVED --> IN_PROGRESS: T4\nstaff bấm "Start"
IN_PROGRESS --> COMPLETED: T5\nstaff bấm "Complete"
COMPLETED --> [*]
note right of ARRIVED
Capture trigger PRIMARY:
onBookingArrived → MANUAL+AUTHORIZED
Booksy/Timely/Vagaro pattern
end note
note right of COMPLETED
Capture trigger FALLBACK
(nếu skip ARRIVED)
Loyalty: RESERVED→CONSUMED (L4)
TenantCustomer: visitCount++
end note
T1 · PENDING → CONFIRMED
Performer
Allowed?
Path
Ghi chú
CUSTOMER
—
chỉ cancel own
không có endpoint confirm cho customer
STAFF
✅
state machine
không check deposit
OWNER / ADMIN
✅
state machine
không check deposit
SYSTEM (PaymentAuthorized listener)
✅
isAdminTransition branch
skip cancellation window guard
Guards
#
Guard
Code?
Docs?
Status
C1
isValidTransition(PENDING, CONFIRMED)
✅
✅
[x]
C2
Deposit required — nếu depositEnabled + Payment không ở AUTHORIZED/CAPTURED → reject BOOKING_DEPOSIT_REQUIRED (OWNER/ADMIN có thể force=true override với audit reason)
✅
✅ booking-status-flow.md §3
[x] P0-2 shipped
Events + side effects
BookingConfirmed emit (payload: bookingId, confirmedAt, confirmedBy).
Notification: BOOKING_CONFIRMED template gửi customer.
Payment: KHÔNG capture (giữ hold, capture ở ARRIVED).
Loyalty: no-op.
Real-world
Case
Hiện tại
Gap
Customer trả deposit xong → webhook → listener auto-confirm
✅ hoạt động
—
Staff nhấn "Confirm" thủ công khi depositEnabled=false
✅ OK
—
Staff nhấn "Confirm" khi depositEnabled=true nhưng Payment INITIATED
[x] P0-2 shipped — block với BOOKING_DEPOSIT_REQUIRED, OWNER/ADMIN có thể force=true + audit reason
P0-2
Staff confirm khi Payment FAILED (chưa kịp listener cancel)
[x] P1-5 shipped — validateSettingsCombination chặn combo autoConfirm=true && depositEnabled=true ngay tại tenant settings
P1-5
Implementation
T2 · CONFIRMED → ARRIVED
Performer
Allowed?
CUSTOMER
—
STAFF / OWNER / ADMIN
✅
SYSTEM
— (không có event auto-arrive)
Guards
#
Guard
Code?
Docs?
Status
A1
isValidTransition(CONFIRMED, ARRIVED)
✅
✅
[x]
A2
Check-in window — không cho check-in sớm quá X phút trước startTime
❌
❌
[ ] chưa có yêu cầu, nhưng salon thật có thể muốn
Events + side effects
BookingArrived emit.
Payment : onBookingArrived → capture MANUAL+AUTHORIZED payment (primary capture trigger).
Notification: none mặc định (docs mô tả optional).
Stats: none.
Real-world
Case
Hiện tại
Gap
Khách check-in đúng giờ
✅ Payment capture tự động
—
Khách đến sớm 1 tiếng
✅ cho mark Arrived sớm (no guard)
A2 nếu cần
Khách late nhưng đã đến trước khi startTime + grace
✅ mark Arrived bình thường
—
Khách không đến, staff quên mark NoShow
Booking stuck CONFIRMED, Payment hold hết 7 ngày → EXPIRED
Xem 07-edge-cases.md
depositEnabled=false → không có Payment
✅ capture loop findByBookingId trả [], no-op
—
Implementation
T3 · CONFIRMED → IN_PROGRESS (skip ARRIVED)
Path ngắn khi staff/owner bấm "Start" luôn (khách đến, staff cầm máy bấm start thay vì 2 bước).
Performer
Allowed?
STAFF / OWNER / ADMIN
✅
Guards
#
Guard
Code?
Docs?
Status
S1
isValidTransition(CONFIRMED, IN_PROGRESS)
✅
✅
[x]
S2
Resource không conflict IN_PROGRESS khác (docs §3)
❌
✅
[ ] chưa có
Events + side effects
KHÔNG emit event (code booking.service.ts:969 default branch return null).
→ Không có Payment capture ở bước này.
Notification: none.
Real-world
Case
Hiện tại
Gap
Khách đến + staff start luôn (skip Arrived)
Không capture. Sẽ capture ở COMPLETED fallback
[!] Cửa sổ capture dài hơn → rủi ro nếu booking bị hủy giữa chừng
Staff đang bận resource khác, bấm Start sai booking
Không có guard → booking chuyển IN_PROGRESS trước
S2 nên add
Implementation
T4 · ARRIVED → IN_PROGRESS
Performer
Allowed?
STAFF / OWNER / ADMIN
✅
Guards
#
Guard
Code?
Docs?
Status
I1
isValidTransition(ARRIVED, IN_PROGRESS)
✅
✅
[x]
I2
Resource không conflict khác
❌
✅
[ ] như S2
Events + side effects
KHÔNG emit event (cùng default branch).
Payment đã capture ở ARRIVED bước trước.
Notification: none.
Implementation
T5 · IN_PROGRESS → COMPLETED
Performer
Allowed?
STAFF / OWNER / ADMIN
✅
Guards
#
Guard
Code?
Docs?
Status
M1
isValidTransition(IN_PROGRESS, COMPLETED)
✅
✅
[x]
M2
Full payment required khi POS enabled (docs §3)
❌
✅ (future)
[ ] phụ thuộc POS feature
M3
remaining = total − captured + refunded hiển thị cảnh báo nếu > 0
❌ FE
❌
[ ] UX
Events + side effects
BookingCompleted emit.
Payment : onBookingCompleted → capture fallback nếu MANUAL+AUTHORIZED (ví dụ skip Arrived). Idempotent (đã CAPTURED → no-op).
Loyalty : OnBookingCompletedListener (hiện pre-L3 — cần check L4):
RESERVED → CONSUMED nếu có appliedRedemptionId
autoStamp tạo stamp cho card VISIT_BASED qualifying
autoEarn cộng points cho card POINTS_BASED qualifying
TenantCustomer metrics : visitCount++, lastVisit, totalSpent += payableTotal.
Real-world
Case
Hiện tại
Gap
Service xong, khách đã trả full deposit + cash phần còn lại
COMPLETED → stamps/points auto-earn
OK
Service xong, khách nợ phần còn lại
Không guard → COMPLETED mặc dù chưa full thu
M2/M3 nếu cần
"Krev resterende" QR đã capture full
COMPLETED idempotent no-op
OK (Flow 18 payment-flow)
Loyalty L4 listener chưa ship
[~] Code base hiện chỉ có pre-L3 listener on-booking-completed.listener.ts — cần verify RESERVED → CONSUMED flip có chạy chưa
P1-6
Implementation
Summary checklist
PENDING → CONFIRMED
CONFIRMED → ARRIVED
CONFIRMED → IN_PROGRESS
ARRIVED → IN_PROGRESS
IN_PROGRESS → COMPLETED
→ Chi tiết gap + plan: progress/gaps-and-plan.md .