Cashier Management — Gap Analysis (AC Review)
Tài liệu đối chiếu Acceptance Criteria tổng quát với implementation hiện tại trong:
- Backend:
kioskgaming_backend/src/modules/cashier-management/ - Cashier portal:
kioskgaming_cashier/ - Spec: spec.md
- QC: QC checklist
Ngày review: 2026-06-10
Tổng quan nhanh
| Nhóm | Trạng thái |
|---|---|
| Core flow (login, shift, opening, deposit/redeem, blind close) | ~70% có |
| Enforcement chặt (device, limit, handover, auto-lock UI) | Còn gap |
| Shop-side (tạo cashier, no-show, reconciliation) | Phần lớn ở Shop portal — Cashier chỉ dùng |
Cashier portal hiện có 7 màn hình: Login, My Shifts, Opening Cash, Active Shift, Cash Drop, Closing, Handover — khớp QC ship gate.
Acceptance Criteria — Ma trận đối chiếu
| # | Acceptance Criteria | Trạng thái | Ghi chú |
|---|---|---|---|
| 1 | Shop tạo Cashier thuộc đúng một Shop | ✅ Backend | Model shopId; Shop UI chỉ tạo username/password |
| 2 | Cashier login bằng email, username hoặc phone | ✅ Backend / ⚠️ Shop UI | findByIdentifier; Shop chưa form email/phone |
| 3 | Shop tạo shift với ngày, time, Cashier, device | ✅ | cashierShiftScheduleBizService |
| 4 | Chặn Cashier overlap shift | ✅ | SHIFT_OVERLAP 409 |
| 5 | Chặn device overlap shift | ✅ | Cùng logic overlap |
| 6 | Cashier chỉ start shift trên đúng device assign | ⚠️ Một phần | Chỉ check fingerprint nếu có; thiếu so deviceId |
| 7 | Cashier nhập opening cash trước giao dịch | ✅ | Start shift bắt buộc denomination |
| 8 | Cashier chỉ cash tx với Player | ✅ | paymentMethod = 'cash' cứng |
| 9 | Deposit không yêu cầu Player OTP | ✅ | Counter flow không OTP |
| 10 | Redemption theo limit và approval | ✅ Backend / ⚠️ FE | Logic có; UI hiển thị sai khi pending |
| 11 | Mọi tx lưu cashierId, shiftId, shopId, deviceId | ✅ | CashierTransaction model |
| 12 | Cashier chỉ xem tx shift của mình | ✅ | listByCashier filter |
| 13 | Cashier không sửa/hủy/reverse tx | ✅ | Không có API |
| 14 | Closing count blind count | ✅ | blindCloseShift + ClosingCountPage |
| 15 | Tự động tính Expected + Discrepancy | ✅ | expectedClosingCashService |
| 16 | Discrepancy → Shop Manager review | ✅ | pending_manager_review + notify |
| 17 | No-show chỉ Shop xử lý, không notify Admin | ✅ | noShowJob → shop only |
| 18 | Device dùng bởi nhiều Cashier ở shift khác | ✅ | Overlap validation cho phép reuse tuần tự |
| 19 | Block Device ≠ Suspend Cashier | ✅ | Tách service |
| 20 | Handover theo denomination + xác nhận người nhận | ⚠️ Một phần | API có; flow cross-cashier chưa đúng |
| 21 | Auto-Locked vẫn reconciliation | ⚠️ Backend ✅ / FE ❌ | BE cho close; FE block auto_locked |
| 22 | Mọi action quan trọng có audit log | ⚠️ Một phần | Thiếu create/suspend cashier, revoke device |
Đã đáp ứng — Không cần thêm ngay
Authentication
- Login bằng
identifier(username / email / phone):CashierRepository.findByIdentifier,cashierAuthBizService.login - Cashier portal:
CashierLoginPagegửiidentifier+deviceFingerprint
Shift scheduling & overlap
- Tạo shift scheduled với cashier + device + time:
cashierShiftScheduleBizService - Chặn overlap cashier và device:
_assertNoOverlap→409 SHIFT_OVERLAP
Start shift & opening cash
- Start shift với denomination:
cashierShiftOpsBizService.startShift - UI:
OpeningCashPage→POST /cashier/shifts/:id/start - Opening cash bắt buộc trước khi shift
active→ transaction chỉ khi shift active
Transactions
- Deposit cash, không OTP:
cashierTransactionBizService.depositCredits(paymentMethod = 'cash') - Redemption approval:
cashierRedemptionApprovalBizService(threshold + daily limit qua env) - Ghi đủ metadata:
cashierId,shiftId,shopId,deviceIdtrênCashierTransaction - Không có API sửa/hủy/reverse giao dịch
Blind closing & discrepancy
- Blind close:
blindCloseShift— expected chỉ trả sau submit - UI:
ClosingCountPagekhông hiện expected trước submit - Discrepancy →
pending_manager_review+ notify shop (notifyShiftDiscrepancy)
Jobs & notifications
- No-show:
noShowJob→ statusno_show, audit, notify shop (không admin) - Auto-lock:
autoLockJob→ statusauto_locked, notify shop - Worker:
cashierManagementWorker.js(tick 60s)
Device & cashier status
- Block device revoke sessions:
cashierDeviceBizService.setDeviceStatus - Suspend cashier revoke sessions + suspend shift:
shopCashierManagementBizService._setStatus - Hai luồng tách biệt
Audit (phần đã có)
- Shift started/closed, deposit, redeem, cash drop, handover initiate/confirm, redemption approve/reject, shift reconciled
Gap quan trọng — Cashier cần bổ sung
P0 — Auto-Locked: Cashier không close được trên UI
Vấn đề: Backend blindCloseShift cho phép active, auto_locked, suspended. Frontend shiftAccess.ts chỉ coi active là operable (SHIFT_JOIN_STATUSES = ['active']).
Hệ quả: ClosingCountPage, CashDropPage, ActiveShiftPage redirect/block khi auto_locked.
Vi phạm AC: Shift bị Auto-Locked vẫn phải thực hiện reconciliation.
Cần làm:
- Mở rộng
canJoinShifthoặc thêmcanCloseShiftgồmauto_locked(và có thểsuspended) - CTA "Close shift" trên
MyShiftsPagekhi shiftauto_locked - Banner thông báo shift đã bị khóa
File liên quan:
kioskgaming_cashier/src/utils/shiftAccess.tskioskgaming_cashier/src/pages/ClosingCountPage.tsxkioskgaming_cashier/src/pages/MyShiftsPage.tsx
P0 — Start shift trên đúng device assign (chưa chặt)
Vấn đề: startShift chỉ:
- Check device assigned có
status === 'active' - Check fingerprint nếu cả request và device record đều có fingerprint
Thiếu:
- So sánh
deviceIdtừ request/session vớishift.deviceId - Bắt buộc fingerprint khi device đã có fingerprint
- Reject khi device chưa được shop approve (nếu assign device pending)
File: kioskgaming_backend/src/modules/cashier-management/services/biz/cashierShiftOpsBizService.js (khoảng dòng 102–116)
Cần thêm:
if (shift.deviceId && deviceId && String(deviceId) !== String(shift.deviceId)) {
throw { code: 'DEVICE_MISMATCH', statusCode: 403 };
}
P1 — Redemption pending approval — UX sai
Vấn đề: Backend trả pendingApproval: true khi cần duyệt. ActiveShiftPage luôn toast "Redeem completed".
Cần làm:
- Kiểm tra
res.data.pendingApprovaltrongdoRedeem - Hiển thị trạng thái
pending_approvaltrong danh sách giao dịch - Toast: "Chờ Shop Manager duyệt" thay vì completed
- Không cho cashier coi là đã trả tiền cho player
File: kioskgaming_cashier/src/pages/ActiveShiftPage.tsx
P1 — Handover giữa 2 Cashier — flow chưa đúng spec
Spec (§10): Cashier A bàn giao → Cashier B (hoặc Manager) xác nhận. Closing A → Opening B.
| Vấn đề | Chi tiết |
|---|---|
| UI initiate chỉ list shift của chính cashier | assignedShifts.filter(s => s.id !== id) — không chọn shift Cashier B |
| Confirm không verify người nhận | confirmHandover không check cashierId === pending.toCashierId |
| Không đóng shift A sau handover | Chỉ tạo record, không transition shift A |
| Không notify handover discrepancy | Spec §15 yêu cầu notify Shop Manager |
Cần làm:
- API/UI chọn incoming shift theo shop (shift cashier khác, scheduled, cùng device nếu cần)
- Verify receiver khi confirm
- Đóng shift A / chuẩn bị mở shift B theo spec
- Notify nếu
discrepancy !== 0
File:
kioskgaming_backend/src/modules/cashier-management/services/biz/cashierHandoverBizService.jskioskgaming_cashier/src/pages/HandoverPage.tsx
P2 — Shop tạo Cashier thiếu email/phone
Vấn đề: Model có email, phone, loginMethod. updateCashierProfile hỗ trợ patch. Shop UI chỉ tạo username + password (CashiersPage.tsx).
Ảnh hưởng AC: Cashier login bằng email/username/phone — backend sẵn sàng nhưng shop không nhập email/phone khi tạo.
Cần làm (Shop portal):
- Form tạo/sửa cashier:
fullName,email,phone,loginMethod - Validate unique email/phone trong shop
P2 — Transaction limit cho Deposit — chưa có
Spec §5.1: Hệ thống kiểm tra transaction limit.
Module cashier không gọi transactionLimitsValidationService. Redemption có threshold/daily qua env; deposit chưa có.
Cần làm: Validate min/max per-tx và daily deposit limit trong depositCredits.
P2 — Audit log chưa đủ (spec §14)
Thiếu:
cashier_createdcashier_suspended/cashier_terminated/cashier_unsuspendeddevice_access_revoked
shopCashierManagementBizService suspend/terminate/create không gọi auditBiz dù đã inject.
P3 — Revoke Cashier access trên device — chưa implement
Có bảng cashier_device_access + CashierDeviceAccessRepository, nhưng không wire trong bootstrapCashierManagement — không API, không enforce khi login/start shift.
Spec §11.2: Revoke quyền một cashier trên một device; device vẫn active cho cashier khác.
P3 — Auto-lock — thiếu cảnh báo và notify Cashier
Spec §9:
- Cảnh báo Cashier trước scheduled end
- Closing grace period cho giao dịch đang xử lý
- Notify Cashier + Shop Manager
Hiện tại: Job set auto_locked + Telegram shop (notifyAutoLock). Không in-app warning, không notify cashier.
Cần làm (Cashier FE):
- Banner countdown trước
scheduledEndAt - Banner khi
auto_locked - (Tùy chọn) polling/WebSocket cho trạng thái shift
Gap nhỏ — Nên fix
| Gap | Ghi chú |
|---|---|
loginMethod không enforce | Auth không check method đã cấu hình |
| Logout chỉ xóa localStorage | Không gọi cashier-auth/logout — session server còn đến hết TTL |
| Player balance không hiển thị | Deposit/redeem dễ nhập sai số |
Shift pending_reconciliation / completed | Cashier xem được nhưng UI chưa summary reconciliation rõ |
| Handover initiate không yêu cầu closing trước | Spec: A dừng giao dịch + closing count trước handover |
| Device pending approval UX | Start fail nếu shop chưa approve — cần hướng dẫn rõ hơn |
| Test coverage mỏng | Chủ yếu unit helper/state machine; thiếu integration tx/device/handover |
openedAt set khi tạo scheduled shift | openingCashBalance: 0, openedAt: scheduledStartAt — có thể gây nhầm reporting |
Acceptance Criteria nên bổ sung vào checklist
Các mục spec đã có nhưng list AC tổng quát chưa nêu:
- Device phải được Shop approve trước khi start shift trên device đó.
- Cashier nhận cảnh báo trước scheduled end và khi shift auto-locked.
- Redemption pending approval — Cashier thấy trạng thái chờ duyệt, không coi là hoàn tất.
- Handover cross-cashier — A chọn shift của B; chỉ B (hoặc Manager) được confirm.
- Deposit transaction limits — per-tx và/hoặc daily.
- Cashier xem balance Player khi deposit/redeem (read-only).
- Revoke device access per cashier — tách với block device.
- Server-side logout — revoke session khi Sign out.
- Cashier xem shift report sau completed (read-only, không thấy shop balance).
- Shop Portal deposit vs Cashier deposit — hai luồng song song: Shop giữ CDN add credits; Cashier deposit gắn shift/cash ledger.
Đề xuất ưu tiên triển khai
flowchart TD
P0A[Fix auto_locked closing UI] --> P0B[Device ID validation on start]
P0B --> P1A[Redemption pending UX]
P1A --> P1B[Handover cross-cashier flow]
P1B --> P2A[Deposit limits + audit gaps]
P2A --> P3A[Auto-lock warnings + cashier notify]
| Ưu tiên | Hạng mục | Lý do |
|---|---|---|
| P0 | Auto-locked → cho close trên Cashier app | Block reconciliation hàng ngày |
| P0 | Device ID match shift.deviceId | Security / fraud |
| P1 | Redemption pending approval UX | Tránh trả tiền sai |
| P1 | Handover A→B đúng spec | Ca kế tiếp không start được |
| P2 | Shop form email/phone cashier | Login AC chưa usable thực tế |
| P2 | Deposit transaction limits | Compliance |
| P3 | Audit suspend/create, revoke device access | Audit trail đầy đủ |
| P3 | Auto-lock warnings + notify cashier | Spec §9 |
File map — Cashier module
Backend (kioskgaming_backend/src/modules/cashier-management/)
| File | Vai trò |
|---|---|
services/biz/cashierAuthBizService.js | Login/logout |
services/biz/cashierShiftScheduleBizService.js | Tạo/sửa shift, overlap |
services/biz/cashierShiftOpsBizService.js | Start shift, blind close |
services/biz/cashierTransactionBizService.js | Deposit/redeem |
services/biz/cashierRedemptionApprovalBizService.js | Approval rules |
services/biz/cashierHandoverBizService.js | Handover |
services/biz/cashierCashDropBizService.js | Cash drop |
services/biz/cashierReconciliationBizService.js | Manager reconcile |
jobs/noShowJob.js | No-show detection |
jobs/autoLockJob.js | Auto-lock |
utils/shiftStateMachine.js | Status transitions |
ports/cashierNotifyPort.js | Telegram notifications |
Cashier portal (kioskgaming_cashier/src/)
| File | Màn hình |
|---|---|
pages/CashierLoginPage.tsx | Login |
pages/MyShiftsPage.tsx | Danh sách shift |
pages/OpeningCashPage.tsx | Opening cash / start |
pages/ActiveShiftPage.tsx | Deposit/redeem + tx list |
pages/CashDropPage.tsx | Cash drop |
pages/ClosingCountPage.tsx | Blind close |
pages/HandoverPage.tsx | Handover |
utils/shiftAccess.ts | Quyền truy cập theo status |
services/cashierApi.ts | API client |
Kết luận
Phần Cashier core (login, assigned shifts, opening cash, deposit/redeem cash, blind close, cash drop, list tx shift mình) đã có khung backend + FE tương đối đầy đủ.
Cần bổ sung chủ yếu ở enforcement + UX nghiệp vụ:
- Device assignment chặt hơn (
deviceId===shift.deviceId) - Auto-locked reconciliation trên Cashier app
- Handover giữa hai cashier
- Redemption pending state trên UI
- Transaction limits cho deposit
- Notify/cảnh báo auto-lock cho cashier
- Shop tạo cashier đủ email/phone để login AC hoạt động thực tế