flows/status-matrix/04-no-show.md

04 · No-Show Transition

CONFIRMED/ARRIVED → NO_SHOW. Khi khách không đến và staff đã đợi qua thời gian grace.

Code ref: booking.service.ts status switch case NO_SHOW + payment-integration.service.ts:onBookingNoShow.


1. State machine

PENDING      → NO_SHOW ❌
CONFIRMED    → NO_SHOW ✅
ARRIVED      → NO_SHOW ✅ (edge case: khách check-in rồi biến mất?)
IN_PROGRESS  → NO_SHOW ❌
COMPLETED    → NO_SHOW ❌

OWNER/ADMIN bypass qua isAdminTransition nhưng no-show từ PENDING hoặc IN_PROGRESS không có business meaning thực.


2. Performer matrix

Performer Allowed? Ghi chú
CUSTOMER
STAFF primary path
OWNER / ADMIN + force
SYSTEM ✅ (hiện chưa có listener auto-no-show) nice-to-have cron: auto mark no-show sau startTime + grace

3. Guards

# Guard Code? Docs? Status
N1 isValidTransition(CONFIRMED/ARRIVED, NO_SHOW) [x]
N2 Grace periodnow() > startTime + graceMinutes (docs default 15min) [ ] Chưa implement. Staff có thể mark no-show ngay trước giờ booking.
N3 Không có outstanding payment command đang pending [ ] —

4. Events + side effects

Event

BookingMarkedNoShow {
  bookingId, tenantId, markedAt,
  idempotencyKey: `bk-${bookingId}-noshow`,
}

Docs payment-flow.md §ref table ghi thêm markedBy, nhưng code hiện không có field đó trong payload.

Payment (onBookingNoShow)

for (payment of findByBookingId) {
  if (payment.status !== AUTHORIZED) continue;
  dispatch CapturePaymentCommand (forfeit = capture full)
}
Payment status Action
INITIATED no-op (nothing to capture)
AUTHORIZED capture full — forfeit deposit
CAPTURED (auto mode) no-op — salon đã giữ tiền
PARTIALLY_REFUNDED / REFUNDED no-op
VOIDED / FAILED / EXPIRED no-op

Loyalty

Hiện không có listener onBookingMarkedNoShow trong Loyalty context. → Nếu booking có appliedRedemptionId = RESERVED và rơi vào NO_SHOW:

  • Stamps: không restore (forfeit, khớp payment policy)
  • Points: không clawback (ledger burn lost)
  • Redemption row: stuck ở RESERVED foreverlistByCustomer query có thể return orphan

→ Gap P1-6: Loyalty L4 listener cho NO_SHOW path (status RESERVED → FORFEITED hoặc CANCELLED).

Stats

Docs ghi "customer no-show counter++". Code chưa verify:

grep -rn "noShow\|no_show" booking-api/src/core/customer* 2>/dev/null

→ Gap: Stats counter nếu chưa có → P2-6.

Notification

Recipient Event
Customer Email/SMS "Bạn đã vắng mặt, deposit đã bị giữ lại làm phí" (nice-to-have)
Staff

→ Chưa implement trong enqueueNotification switch (booking.service.ts:858 chỉ cho CONFIRMED/CANCELLED).


5. Real-world cases

# Case Hiện tại Expected Gap
1 Khách không đến sau 15 phút Staff bấm tay "No show" Auto mark + email khách [ ] Auto cron (nice-to-have)
2 Khách đến trễ 10 phút, staff đã bấm NoShow [!] Không có grace guard → staff đã bấm quá sớm Grace period enforce N2 guard cần add
3 Khách check-in rồi đi ra cửa hàng khác → biến mất Staff bấm NoShow từ ARRIVED VOID/FORFEIT payment tuỳ state [~] Payment đã CAPTURED ở ARRIVED → onBookingNoShow skip (status !== AUTHORIZED). Salon giữ tiền. OK.
4 Auto-confirmed booking, khách never pay, never show Booking CONFIRMED (đã bị listener confirm từ PaymentAuthorized? Không — payment không authorize), cần staff bấm NoShow hoặc cron [x] Sẽ bị auto-cancel qua expiry listener khi Payment EXPIRED (nếu depositEnabled)
5 Khách có lịch sử no-show nhiều lần Không có counter → salon không thấy trend [ ] Stats counter + display (P2-6)
6 Loyalty redemption RESERVED + khách no-show Orphan RESERVED [!] L4 cần cancel + forfeit (P1-6)
7 Staff lỡ tay bấm NoShow Không undo được (terminal) [ ] OWNER force NO_SHOW → CANCELLED (nếu sau đó khách gọi giải thích) — cần force path P0-1

6. Edge cases

6a. Auto-capture mode (FULL_PAYMENT intent future)

Khi captureMode=AUTO, Payment ở CAPTURED ngay sau khách trả. Staff mark NoShow → no-op (đã có tiền). OK.

6b. Khách cancel sau khi bị mark NoShow

Terminal — không transition được. Payment đã forfeit (capture). Customer khiếu nại → OWNER phải:

  1. Refund manual qua Payment UI (POST /payments/:id/refund), KHÔNG đổi booking status.
  2. Log reason trong audit.

6c. Race: auto no-show cron vs staff mark COMPLETED

Không áp dụng hiện tại vì chưa có auto cron. Nếu add sau:

  • Cron check CONFIRMED + now > startTime + graceMinutes + X → mark NoShow
  • Staff đang click "Arrived" cùng lúc → race
  • Giải pháp: cron dùng isValidTransition(CONFIRMED, NO_SHOW) — nếu status đã ARRIVED/IN_PROGRESS thì throw → swallow → skip

7. Checklist no-show

State machine

  • CONFIRMED/ARRIVED → NO_SHOW (staff)
  • OWNER force bypass
  • Terminal

Guards

  • N2 grace period — 15 phút sau startTime mới cho bấm
  • N3 outstanding payment check (nếu cần)

Events

  • BookingMarkedNoShow emit
  • [~] Payload thiếu markedBy so với docs

Payment integration

  • Capture AUTHORIZED (forfeit)
  • Skip các status khác

Loyalty integration

  • L4 listener cho NO_SHOW — restore / forfeit redemption RESERVED (P1-6)

Stats

  • Customer no-show counter increment (P2-6)
  • Display counter trong customer detail UI

Notification

  • Email khách "bạn đã vắng mặt, deposit bị giữ" (P2-7)

Automation

  • Cron auto-mark NoShow sau startTime + grace + 30min (P3 nice-to-have)

Admin UX

  • "Undo NoShow" path (OWNER force NO_SHOW → CANCELLED + refund) — P0-1 force flag + P2-5 UX

gaps-and-plan.md cho priorities.