Cashier Management — Gap Analysis (AC Review)
Document cross-referencing Overall acceptance criteria with current implementation in:
- Backend:
kioskgaming_backend/src/modules/cashier-management/ - Cashier portal:
kioskgaming_cashier/ - Spec: spec.md
- QC: QC checklist
Review date: 2026-06-10
Quick overview
| Group | Status |
|---|---|
| Core flow (login, shift, opening, deposit/redeem, blind close) | ~70% present |
| Strict enforcement (device, limit, handover, auto-lock UI) | Gaps remain |
| Shop-side (create cashier, no-show, reconciliation) | Mostly in Shop portal — Cashier only consumes |
Cashier portal currently has 7 screens: Login, My Shifts, Opening Cash, Active Shift, Cash Drop, Closing, Handover — matches QC ship gate.
Acceptance Criteria — Cross-reference matrix
| # | Acceptance Criteria | Status | Notes |
|---|---|---|---|
| 1 | Shop creates Cashier belonging to exactly one Shop | ✅ Backend | Model shopId; Shop UI only creates username/password |
| 2 | Cashier login with email, username, or phone | ✅ Backend / ⚠️ Shop UI | findByIdentifier; Shop form missing email/phone |
| 3 | Shop creates shift with date, time, Cashier, device | ✅ | cashierShiftScheduleBizService |
| 4 | Block Cashier shift overlap | ✅ | SHIFT_OVERLAP 409 |
| 5 | Block device shift overlap | ✅ | Same overlap logic |
| 6 | Cashier only starts shift on assigned device | ⚠️ Partial | Only checks fingerprint if present; missing deviceId comparison |
| 7 | Cashier enters opening cash before transactions | ✅ | Start shift requires denomination |
| 8 | Cashier only cash tx with Player | ✅ | paymentMethod = 'cash' hardcoded |
| 9 | Deposit does not require Player OTP | ✅ | Counter flow has no OTP |
| 10 | Redemption per limit and approval | ✅ Backend / ⚠️ FE | Logic exists; UI shows wrong state when pending |
| 11 | Every tx stores cashierId, shiftId, shopId, deviceId | ✅ | CashierTransaction model |
| 12 | Cashier only views own shift tx | ✅ | listByCashier filter |
| 13 | Cashier cannot edit/cancel/reverse tx | ✅ | No API |
| 14 | Closing count is blind count | ✅ | blindCloseShift + ClosingCountPage |
| 15 | Auto-calculate Expected + Discrepancy | ✅ | expectedClosingCashService |
| 16 | Discrepancy → Shop Manager review | ✅ | pending_manager_review + notify |
| 17 | No-show handled by Shop only, no Admin notify | ✅ | noShowJob → shop only |
| 18 | Device used by multiple Cashiers across shifts | ✅ | Overlap validation allows sequential reuse |
| 19 | Block Device ≠ Suspend Cashier | ✅ | Separate services |
| 20 | Handover by denomination + recipient confirmation | ⚠️ Partial | API exists; cross-cashier flow not correct |
| 21 | Auto-Locked still requires reconciliation | ⚠️ Backend ✅ / FE ❌ | BE allows close; FE blocks auto_locked |
| 22 | Every important action has audit log | ⚠️ Partial | Missing create/suspend cashier, revoke device |
Already met — No immediate work needed
Authentication
- Login with
identifier(username / email / phone):CashierRepository.findByIdentifier,cashierAuthBizService.login - Cashier portal:
CashierLoginPagesendsidentifier+deviceFingerprint
Shift scheduling & overlap
- Create scheduled shift with cashier + device + time:
cashierShiftScheduleBizService - Block cashier and device overlap:
_assertNoOverlap→409 SHIFT_OVERLAP
Start shift & opening cash
- Start shift with denomination:
cashierShiftOpsBizService.startShift - UI:
OpeningCashPage→POST /cashier/shifts/:id/start - Opening cash required before shift
active→ transactions only when shift active
Transactions
- Deposit cash, no OTP:
cashierTransactionBizService.depositCredits(paymentMethod = 'cash') - Redemption approval:
cashierRedemptionApprovalBizService(threshold + daily limit via env) - Full metadata recorded:
cashierId,shiftId,shopId,deviceIdonCashierTransaction - No API to edit/cancel/reverse transactions
Blind closing & discrepancy
- Blind close:
blindCloseShift— expected returned only after submit - UI:
ClosingCountPagedoes not show expected before submit - Discrepancy →
pending_manager_review+ notify shop (notifyShiftDiscrepancy)
Jobs & notifications
- No-show:
noShowJob→ statusno_show, audit, notify shop (not admin) - Auto-lock:
autoLockJob→ statusauto_locked, notify shop - Worker:
cashierManagementWorker.js(60s tick)
Device & cashier status
- Block device revokes sessions:
cashierDeviceBizService.setDeviceStatus - Suspend cashier revokes sessions + suspend shift:
shopCashierManagementBizService._setStatus - Two flows are separate
Audit (existing portion)
- Shift started/closed, deposit, redeem, cash drop, handover initiate/confirm, redemption approve/reject, shift reconciled
Important gaps — Cashier needs work
P0 — Auto-Locked: Cashier cannot close on UI
Issue: Backend blindCloseShift allows active, auto_locked, suspended. Frontend shiftAccess.ts only treats active as operable (SHIFT_JOIN_STATUSES = ['active']).
Impact: ClosingCountPage, CashDropPage, ActiveShiftPage redirect/block when auto_locked.
AC violation: Auto-Locked shifts must still perform reconciliation.
Required work:
- Extend
canJoinShiftor addcanCloseShiftto includeauto_locked(and possiblysuspended) - CTA "Close shift" on
MyShiftsPagewhen shift isauto_locked - Banner indicating shift is locked
Related files:
kioskgaming_cashier/src/utils/shiftAccess.tskioskgaming_cashier/src/pages/ClosingCountPage.tsxkioskgaming_cashier/src/pages/MyShiftsPage.tsx
P0 — Start shift on correct assigned device (not strict enough)
Issue: startShift only:
- Checks assigned device has
status === 'active' - Checks fingerprint if both request and device record have fingerprint
Missing:
- Compare
deviceIdfrom request/session withshift.deviceId - Require fingerprint when device already has fingerprint
- Reject when device not yet approved by shop (if assigned device is pending)
File: kioskgaming_backend/src/modules/cashier-management/services/biz/cashierShiftOpsBizService.js (around lines 102–116)
Add:
if (shift.deviceId && deviceId && String(deviceId) !== String(shift.deviceId)) {
throw { code: 'DEVICE_MISMATCH', statusCode: 403 };
}
P1 — Redemption pending approval — wrong UX
Issue: Backend returns pendingApproval: true when approval needed. ActiveShiftPage always toasts "Redeem completed".
Required work:
- Check
res.data.pendingApprovalindoRedeem - Show
pending_approvalstatus in transaction list - Toast: "Awaiting Shop Manager approval" instead of completed
- Do not let cashier treat as cash already paid to player
File: kioskgaming_cashier/src/pages/ActiveShiftPage.tsx
P1 — Handover between 2 Cashiers — flow does not match spec
Spec (§10): Cashier A hands over → Cashier B (or Manager) confirms. Closing A → Opening B.
| Issue | Detail |
|---|---|
| UI initiate only lists own shifts | assignedShifts.filter(s => s.id !== id) — cannot select Cashier B's shift |
| Confirm does not verify recipient | confirmHandover does not check cashierId === pending.toCashierId |
| Does not close shift A after handover | Only creates record, no shift A transition |
| No handover discrepancy notify | Spec §15 requires Shop Manager notification |
Required work:
- API/UI select incoming shift by shop (other cashier's shift, scheduled, same device if needed)
- Verify receiver on confirm
- Close shift A / prepare open shift B per spec
- Notify if
discrepancy !== 0
Files:
kioskgaming_backend/src/modules/cashier-management/services/biz/cashierHandoverBizService.jskioskgaming_cashier/src/pages/HandoverPage.tsx
P2 — Shop creates Cashier missing email/phone
Issue: Model has email, phone, loginMethod. updateCashierProfile supports patch. Shop UI only creates username + password (CashiersPage.tsx).
AC impact: Cashier login with email/username/phone — backend ready but shop cannot enter email/phone on create.
Required work (Shop portal):
- Create/edit cashier form:
fullName,email,phone,loginMethod - Validate unique email/phone within shop
P2 — Transaction limit for Deposit — not implemented
Spec §5.1: System checks transaction limit.
Cashier module does not call transactionLimitsValidationService. Redemption has threshold/daily via env; deposit has none.
Required work: Validate min/max per-tx and daily deposit limit in depositCredits.
P2 — Audit log incomplete (spec §14)
Missing:
cashier_createdcashier_suspended/cashier_terminated/cashier_unsuspendeddevice_access_revoked
shopCashierManagementBizService suspend/terminate/create does not call auditBiz despite injection.
P3 — Revoke Cashier access on device — not implemented
Table cashier_device_access + CashierDeviceAccessRepository exist, but not wired in bootstrapCashierManagement — no API, no enforce on login/start shift.
Spec §11.2: Revoke one cashier's access on one device; device stays active for other cashiers.
P3 — Auto-lock — missing warning and Cashier notify
Spec §9:
- Warn Cashier before scheduled end
- Closing grace period for in-progress transactions
- Notify Cashier + Shop Manager
Current: Job sets auto_locked + Telegram shop (notifyAutoLock). No in-app warning, no cashier notify.
Required work (Cashier FE):
- Countdown banner before
scheduledEndAt - Banner when
auto_locked - (Optional) polling/WebSocket for shift status
Minor gaps — Should fix
| Gap | Notes |
|---|---|
loginMethod not enforced | Auth does not check configured method |
| Logout only clears localStorage | Does not call cashier-auth/logout — server session remains until TTL |
| Player balance not displayed | Deposit/redeem prone to wrong amounts |
Shift pending_reconciliation / completed | Cashier can view but UI lacks clear reconciliation summary |
| Handover initiate does not require closing first | Spec: A stops transactions + closing count before handover |
| Device pending approval UX | Start fails if shop not approved — needs clearer guidance |
| Thin test coverage | Mostly unit helper/state machine; missing integration tx/device/handover |
openedAt set when creating scheduled shift | openingCashBalance: 0, openedAt: scheduledStartAt — may confuse reporting |
Acceptance criteria to add to checklist
Items in spec but not in overall AC list:
- Device must be Shop-approved before starting shift on that device.
- Cashier receives warning before scheduled end and when shift auto-locked.
- Redemption pending approval — Cashier sees pending state, not treated as complete.
- Handover cross-cashier — A selects B's shift; only B (or Manager) can confirm.
- Deposit transaction limits — per-tx and/or daily.
- Cashier views Player balance on deposit/redeem (read-only).
- Revoke device access per cashier — separate from block device.
- Server-side logout — revoke session on Sign out.
- Cashier views shift report after completed (read-only, no shop balance).
- Shop Portal deposit vs Cashier deposit — two parallel flows: Shop keeps CDN add credits; Cashier deposit links shift/cash ledger.
Implementation priority proposal
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]
| Priority | Item | Reason |
|---|---|---|
| P0 | Auto-locked → allow close on Cashier app | Blocks daily reconciliation |
| P0 | Device ID match shift.deviceId | Security / fraud |
| P1 | Redemption pending approval UX | Prevents wrong cash payout |
| P1 | Handover A→B per spec | Next shift cannot start |
| P2 | Shop form email/phone cashier | Login AC not usable in practice |
| P2 | Deposit transaction limits | Compliance |
| P3 | Audit suspend/create, revoke device access | Full audit trail |
| P3 | Auto-lock warnings + notify cashier | Spec §9 |
File map — Cashier module
Backend (kioskgaming_backend/src/modules/cashier-management/)
| File | Role |
|---|---|
services/biz/cashierAuthBizService.js | Login/logout |
services/biz/cashierShiftScheduleBizService.js | Create/edit 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 | Screen |
|---|---|
pages/CashierLoginPage.tsx | Login |
pages/MyShiftsPage.tsx | Shift list |
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 | Access by status |
services/cashierApi.ts | API client |
Conclusion
Cashier core (login, assigned shifts, opening cash, deposit/redeem cash, blind close, cash drop, list own shift tx) has a relatively complete backend + FE framework.
Main gaps are in enforcement + business UX:
- Stricter device assignment (
deviceId===shift.deviceId) - Auto-locked reconciliation on Cashier app
- Handover between two cashiers
- Redemption pending state on UI
- Transaction limits for deposit
- Auto-lock notify/warning for cashier
- Shop creates cashier with email/phone so login AC works in practice