Player → Shop/Cashier (Counter Redemption)
Phase 3 — separate from Phase 2 (deposit). Player redeems credits for cash at counter; Shop pays Player the corresponding cash amount.
Player requests withdrawal
→ Shop/Cashier enters credits to redeem + cash paid to Player
→ credits debited from Player, credited to Shop CDN wallet
→ Shop pays cash to Player
→ cash ledger (Cash Out) + reporting
Deposit flow (Player pays cash, receives credits): Shop/Cashier → Player — Phase 2.
Mockup: Phase 2 — Redemptions tab · Shift: phase3-shift · Cashier spec: Cashier Management spec
Position in the system
flowchart LR
P2["Phase 2\nDeposit"] --> P3["Phase 3\nRedemption"]
P3 --> SHIFT["Shift Cash Out\n+ reconciliation"]
Ship after Phase 2 — shares cash_transactions schema and biz hook pattern, but business logic and UI are separate because:
- Cash direction reversed (Shop pays cash)
- Has approval threshold (Cashier)
- Tightly linked to shift Cash Out and blind closing
- No shop margin — redemption at 1:1 face value
Who does what
| Role | Action |
|---|---|
| Player | Requests withdrawal; present at counter |
| Shop Manager | Enters credits to redeem + cash paid to Player; confirms redeem from Shop portal |
| Cashier | Enters credits to redeem + cash paid to Player; pays cash after completed (or after Manager approval) |
| System | Debits player wallet, credits shop wallet, writes cash_transactions, updates shift Cash Out |
Two execution channels
| Channel | Role | Conditions | API |
|---|---|---|---|
| Cashier portal | Cashier | Active shift; cash only | POST /cashier/players/:id/redeem |
| Shop portal | Shop Manager | Shift not required | POST /shop/players/:id/credits (action: deduct) |
Required information
| Field | Required | Description |
|---|---|---|
| Player | ✅ | Belongs to shop |
| Credits to redeem | ✅ | ≤ player balance; credits debited from Player wallet |
| Cash paid to Player | ✅ | Cash Shop pays directly to Player; recorded in ledger cash_received |
| Payout method | ✅ | Counter: cash only |
V1 typically 1:1 (30 credits → $30.00) — UI may pre-fill cash = credit, but Shop enters and confirms both so reporting records exact cash paid.
shop.costRate does not apply to redemption — no gross margin like deposit.
Approval (Cashier)
Redemption may be pending approval if threshold exceeded (per-transaction or daily limit — env):
Cashier enters amount
→ needsApproval?
YES → CashierTransaction (pending_approval) + approval record
→ player not debited yet, no cash paid, cashDelta = 0
NO → transfer immediately → CashierTransaction (completed), cashDelta = −amount
→ Manager approve → transfer + completed
Important for ledger: create cash_transactions only when wallet transfer actually runs (completed), not at pending_approval.
Business rules
- Player balance ≥ credits to redeem.
- Cashier: active shift; pay cash after completed / approve.
cashDelta = −cash_paid(cash paid to Player) → Cash Out in Expected Closing Cash.- No OTP at counter (player present) — differs from online withdrawal.
- After complete: no edit / cancel / reverse (escalate to Manager).
- Shop Manager approve/reject via Shop portal when pending.
Amount formulas
| Metric | Formula |
|---|---|
| credit_amount | Credits to redeem (wallet transfer) |
| expected_cash / cash_received | Cash Shop pays Player (cash_paid) |
| Gross margin | Not applicable |
Example: redeem 30 credits, pay $30.00 cash → Player −30, Shop CDN +30, ledger Cash Out $30.00.
Reporting and shift reconciliation use cash_received (cash paid), not inferred from credits.
Proposed ledger (cash_transactions)
| Field | Value |
|---|---|
| flow | shop_player_redemption |
| seller | player / player uuid |
| buyer | shop / shop uuid |
| credit_amount | Credits to redeem |
| expected_cash / cash_received | Cash paid to Player |
| status | paid_full (auto on completed) |
Links: payment_transaction_id, wallet txs, cashier_transaction_id, shift_id, metadata.approvalId (if any).
Current backend
Cashier: cashierTransactionBizService.redeemCredits
→ (optional) cashierRedemptionApprovalBizService
→ playerCreditPort.transferCreditsAtCounter (redeem)
→ shopPlayerBizService._adjustCreditsDeduct
→ walletTransferService (player → shop)
→ PaymentTransaction (type: withdrawal, provider: shop_player_cash)
→ CashierTransaction (type: redeem_credits)
Shop: adjustPlayerCredits (action: deduct)
→ same _adjustCreditsDeduct
API
Existing
| Method | Endpoint | Body |
|---|---|---|
POST | /api/cashier/players/:id/redeem | { "amount": number, "cash_paid": number } |
POST | /api/shop/players/:id/credits | { "action": "deduct", "amount": N, "cash_paid": number } |
amount= credits to redeem;cash_paid= cash paid to Player. V1 usually equal; both required.
Phase 3 target
| Method | Endpoint | Description |
|---|---|---|
GET | /api/shop/cash-transactions?flow=shop_player_redemption | List redemptions |
GET | /api/shop/cash-transactions/:id | Detail + approval context |
GET | /api/shop/cash-transactions/export | CSV (filter flow redemption) |
Shop Manager approve API: see Cashier spec § Redemption.
Proposed UI (Shop portal)
Tab Payable — Redemptions on mockup phase2-counter:
- Summary: Counter Cash Out (today) — SUM
cash_received, pending approval count - Redeem modal: 2 fields — credits to redeem + cash paid to Player
- Table: player, credits redeemed, cash paid, cashier, status (Settled / Pending approval)
- Detail: link shift, approval timeline
Shift integration: phase3-shift mockup — Cash Out in Expected Closing Cash.
Phase 2 vs Phase 3 comparison
| Criterion | Phase 2 Deposit | Phase 3 Redemption |
|---|---|---|
| Flow enum | shop_player_deposit | shop_player_redemption |
| Cash direction | Player → Shop (Cash In) | Shop → Player (Cash Out) |
| Credit direction | Shop → Player | Player → Shop |
| Margin | Yes (shop.costRate) | No |
| Approval | No | Possible (threshold) |
| Payment methods | Cash (+ card/bank shop portal) | Cash only |
| OTP | No | No (counter) |
Current status
| Component | Status |
|---|---|
| Wallet transfer player → shop | ✅ |
Cashier redeem + approval + cashDelta | ✅ |
| Shop portal manual deduct | ✅ |
cash_transactions flow shop_player_redemption | ❌ |
| Shop portal redemption ledger | ❌ |
Tasks: tasks-player-to-shop-cashier
Dependency: Ship after Phase 2 deposit ledger — schema player owner type and cashier/shift columns shared.
Related
- Phase 2: Shop/Cashier → Player
- Schema: schema-cash-transactions
- Shift reconciliation: Cashier spec § Expected Closing Cash