Skip to main content

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 → PlayerPhase 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

RoleAction
PlayerRequests withdrawal; present at counter
Shop ManagerEnters credits to redeem + cash paid to Player; confirms redeem from Shop portal
CashierEnters credits to redeem + cash paid to Player; pays cash after completed (or after Manager approval)
SystemDebits player wallet, credits shop wallet, writes cash_transactions, updates shift Cash Out

Two execution channels

ChannelRoleConditionsAPI
Cashier portalCashierActive shift; cash onlyPOST /cashier/players/:id/redeem
Shop portalShop ManagerShift not requiredPOST /shop/players/:id/credits (action: deduct)

Required information

FieldRequiredDescription
PlayerBelongs to shop
Credits to redeem≤ player balance; credits debited from Player wallet
Cash paid to PlayerCash Shop pays directly to Player; recorded in ledger cash_received
Payout methodCounter: 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

  1. Player balance ≥ credits to redeem.
  2. Cashier: active shift; pay cash after completed / approve.
  3. cashDelta = −cash_paid (cash paid to Player) → Cash Out in Expected Closing Cash.
  4. No OTP at counter (player present) — differs from online withdrawal.
  5. After complete: no edit / cancel / reverse (escalate to Manager).
  6. Shop Manager approve/reject via Shop portal when pending.

Amount formulas

MetricFormula
credit_amountCredits to redeem (wallet transfer)
expected_cash / cash_receivedCash Shop pays Player (cash_paid)
Gross marginNot 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)

FieldValue
flowshop_player_redemption
sellerplayer / player uuid
buyershop / shop uuid
credit_amountCredits to redeem
expected_cash / cash_receivedCash paid to Player
statuspaid_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

MethodEndpointBody
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

MethodEndpointDescription
GET/api/shop/cash-transactions?flow=shop_player_redemptionList redemptions
GET/api/shop/cash-transactions/:idDetail + approval context
GET/api/shop/cash-transactions/exportCSV (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

CriterionPhase 2 DepositPhase 3 Redemption
Flow enumshop_player_depositshop_player_redemption
Cash directionPlayer → Shop (Cash In)Shop → Player (Cash Out)
Credit directionShop → PlayerPlayer → Shop
MarginYes (shop.costRate)No
ApprovalNoPossible (threshold)
Payment methodsCash (+ card/bank shop portal)Cash only
OTPNoNo (counter)

Current status

ComponentStatus
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.