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.
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 |
❌ |
✅ booking-status-flow.md §3 |
[!] Gap P0-2 |
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 |
[!] Hệ thống cho qua, booking CONFIRMED không deposit |
P0-2 |
| Staff confirm khi Payment FAILED (chưa kịp listener cancel) |
[!] Race — staff thắng → booking CONFIRMED, sau đó OnPaymentSettledNegative skip (status != PENDING) |
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: gaps-and-plan.md.