Skip to main content

Code Review — Backend Cash Settlement V1

Reviewer perspective: Senior Engineer / Architect
Scope: Backend kioskgaming_backend — 3 cash settlement V1 flows
Review date: 2026-06-20
Status: V1 implemented, shippable to staging/internal; hardening needed before production scale


1. Executive summary

Three cash settlement V1 flows were implemented using the same model:

  1. Validate buyer + cost rate snapshot
  2. Wallet transfer (credit buyer CDN wallet)
  3. PaymentTransaction (legacy audit)
  4. cash_transactions + cash_transaction_events (new ledger)
  5. List/detail/export API per portal scope

Overall verdict: Conditional approve — V1 business logic correct, good atomicity, reasonable reuse of cashTransactionBizService. However the 3 flows are not equal in code quality (Admin→SA best; Agent→Shop inherits much old technical debt). There are 5 cross-cutting issues affecting all 3 flows that should be fixed in one refactor PR.

FlowFlow enumMaturityShip V1?
Admin → Super Agentadmin_super_agent★★★★☆
Super Agent → Agentsuper_agent_agent★★★☆☆
Agent → Shopagent_shop★★★☆☆✅ (with caveats)

2. Architecture overview

sequenceDiagram
participant Portal as Portal_API
participant Biz as Flow_BizService
participant Wallet as walletTransferService
participant Pay as PaymentTransaction
participant Cash as cashTransactionBizService
participant DB as cash_transactions

Portal->>Biz: POST cash-credits / cash-funding
Biz->>Biz: validate buyer + cost rate
Biz->>Wallet: transfer credits
Biz->>Pay: create audit row
Biz->>Cash: createCashTransactionRecord
Cash->>DB: insert + event created
Biz-->>Portal: payment + wallet + cash_transaction

File map

LayerAdmin → SASA → AgentAgent → Shop
BizsuperAgentCreditPurchaseBizService.applyAdminCashFundingsuperAgentCreditDistributionBizService.distribute / distributeCashCreditscreditDistributionBizService.distribute / distributeCashCredits
Create APIadminSuperAgentManagementController.fundSuperAgentCashsuperAgentCashSettlementController.createAgentCashCreditsagentCashSettlementController.createShopCashCredits
Ledger APIadmin/cashTransactionControllersuperAgentCashSettlementController (scoped)agentCashSettlementController (scoped)
SharedcashTransactionBizService
Constantsconstants/cashTransaction.js

API endpoints

PortalCreateListDetailExport
AdminPOST /api/admin/super-agents/:id/cash-fundingGET /api/admin/cash-transactionsGET /api/admin/cash-transactions/:idGET /api/admin/cash-transactions/export
Super AgentPOST /api/super-agent/agents/:id/cash-creditsGET /api/super-agent/cash-transactionsGET /api/super-agent/cash-transactions/:idGET /api/super-agent/cash-transactions/export
AgentPOST /api/agent/shops/:id/cash-creditsGET /api/agent/cash-transactionsGET /api/agent/cash-transactions/:idGET /api/agent/cash-transactions/export

Legacy endpoints still exist:

  • POST /api/super-agent/agents/:id/credits/distribute → calls distribute() (after refactor also creates cash_transaction)
  • POST /api/agent/credits/distribute → calls distribute() (same)

3. Review by flow

3.1 Admin → Super Agent (admin_super_agent)

Main file: superAgentCreditPurchaseBizService.jsapplyAdminCashFunding

Strengths

  • Centralized validator: validateAdminSuperAgentPayload() — validates credits, rate, payment method, received_at in one place.
  • PaymentTransaction sync paymentMethod: uses validated paymentMethod variable, not hardcoded.
  • Dependency injection: paymentTransactionCore + superAgentCore via setDependencies — easy to test.
  • Correct wallet flow: credit from platform (null → super_agent wallet), debitWalletTransactionId = null valid (platform has no wallet debit).
  • Buyer balance after: refresh via walletCoreService.findByOwner — more accurate than other two flows.
  • Super agent status check: status !== 'active' → reject.
  • Backward compat payload: supports legacy (superAgentId, amount, context) and new object payload.

Weaknesses

#IssueSeverity
A1V1 UI locks cash_received = expected, but validator still allows cash_received ≠ expected if client sends different value — server-side V1 lock not enforcedP2
A2sellerType = platform, sellerId = null — list/filter by seller difficult; admin list OK since no seller scopeP3
A3Withdraw (applyAdminCashWithdrawal) does not create cash_transaction — asymmetry if full audit trail needed laterP3

Tests

  • superAgentAdminCashFunding.test.js — happy path + integration mock
  • cashTransactionBizService.test.js — validator + formatRowForApi

Coverage: good for happy path; missing inactive SA, insufficient platform credit tests.


3.2 Super Agent → Agent (super_agent_agent)

Main file: superAgentCreditDistributionBizService.jsdistribute / distributeCashCredits

Strengths

  • Ownership check: assertManagedAgent(superAgentId, agentId) — agent must belong to SA.
  • Terminated agent blocked: status === 'terminated' → reject.
  • Cost rate snapshot: buyerCostRate = agent.costRate (buyer = agent).
  • V1 locked: cashReceived = expectedCash.
  • Full links: payment + debit SA wallet + credit agent wallet.
  • Controller scope: list/detail enforce seller_type=super_agent, seller_id=req.superAgent.id.

Weaknesses

#IssueSeverity
S1PaymentTransaction.paymentMethod hardcoded 'cash' while cash_transactions.paymentMethod comes from payload — audit mismatchP1
S2No check for agent suspended — only terminatedP2
S3No SA balance check before transfer — relies on walletTransferService.transfer throw (message may not be user-friendly)P2
S4receivedAt: new Date(String(...)) does not validate Invalid DateP2
S5Legacy POST /credits/distribute still active, creates cash tx missing metadata (payment_method, external_ref)P2
S6performedByType: 'super_agent' uses string literal instead of HIERARCHY_OWNER_TYPESP3
S7buyerBalanceAfter from wt.transaction?.balanceAfter — no wallet refresh like Admin flowP3

Tests

  • superAgentCreditDistributionBizService.test.js — happy path + terminated agent

Coverage: missing wrong agent, zero cost rate, insufficient balance, paymentMethod sync.


3.3 Agent → Shop (agent_shop)

Main file: creditDistributionBizService.jsdistribute / distributeCashCredits

Strengths

  • Shop ownership: shop.agentId !== agentId → reject.
  • Cost rate snapshot: buyerCostRate = shop.costRate.
  • V1 locked: cashReceived = expectedCash.
  • Pattern mirrors SA→Agent: same structure create cash tx + event.
  • Controller scope: same as SA portal.

Weaknesses

#IssueSeverity
G1PaymentTransaction.paymentMethod hardcoded 'cash' — same bug as S1P1
G2No shop status validation (terminated / suspended) — API still distributesP1
G3global.db directly for Agent + PaymentTransaction — architecture mismatch, hard to test/migrateP2
G4PaymentTransaction.create directly instead of paymentTransactionCoreServiceP2
G5Legacy POST /api/agent/credits/distribute + Dashboard/ShopsPage still used → low quality cash txP2
G6Route param inconsistency: :id (cash-credits) vs :shop_id (withdraw, detail)P3
G7No shared validateAgentShopPayload — validation duplicated inlineP2

Tests

  • creditDistributionCashTx.test.js — 5 cases (happy, wrong agent, zero rate, wrapper, insufficient balance)

Coverage: better than SA flow on edge cases; missing shop status, legacy distribute path.


4. Shared infrastructure — cashTransactionBizService

File: cashTransactionBizService.js

Strengths

  • Single entry createCashTransactionRecord + auto created event
  • generateTransactionCode format CTX-YYYYMMDD-NNNN
  • buildListWhere supports filter flow, seller, buyer, date range, search
  • formatRowForApi normalizes snake_case/camelCase
  • Export CSV cap 10k rows

Cross-cutting issues (affects all 3 flows)

#IssueSeverityDetails
X1getCashTransactionDetailperformedBy only looks up AdminP1SA/Agent detail page does not show performer name
X2generateTransactionCode race conditionP2count + 1 without lock — concurrent requests may duplicate code
X3List summary calculated on current pageP2totalExpected/totalReceived in summary only aggregate current page items, not full filter
X4Partitioned table — created_at optional on detailP2Not requiring created_at → slow scan at scale
X5Only validateAdminSuperAgentPayload exists — missing validators for SA→Agent and Agent→ShopP2Validation logic scattered across biz services
X6global.db in getModels()P2Coupling, hard to unit test real integration

5. Comparison matrix — 3 flows

CriterionAdmin → SASA → AgentAgent → Shop
Flow enumadmin_super_agentsuper_agent_agentagent_shop
Sellerplatform (null)super_agentagent
Buyersuper_agentagentshop
Cost rate snapshotSA.costRateAgent.costRateShop.costRate
V1 cash_received lockUI lock; server allows overrideServer lock (= expected)Server lock (= expected)
Wallet debit side— (platform mint)SA walletAgent wallet
Wallet credit sideSA walletAgent walletShop wallet
Payment provideradmin_cashsuper_agent_cashagent_cash
PaymentMethod sync❌ hardcode cash❌ hardcode cash
Shared validator❌ inline❌ inline
Buyer status checkSA activeAgent terminated only❌ none
Core service pattern✅ injected✅ partial❌ global.db
Legacy endpoint/credits/distribute/credits/distribute
Controller scope (list)Admin all flowsSA scopedAgent scoped
Controller scope (detail)Admin allSA 403 checkAgent 403 check
Unit tests★★★★★★★★★★★

6. Security & authorization

CheckAdminSuper AgentAgent
Auth middleware✅ admin✅ superAgent✅ agent
Create: ownershipAdmin can fund any SASA can only fund managed agentsAgent can only fund own shops
List: scope enforcedAll (permission-based frontend)✅ force seller_id✅ force seller_id
Detail: scope check❌ (admin sees all)✅ 403 if wrong seller✅ 403 if wrong seller
IDOR on cash tx UUIDAdmin OK by designMitigated by seller checkMitigated by seller check

Note: Admin detail does not need seller scope — correct by design. SA/Agent detail seller check is a good pattern to keep.


Each V1 cash transaction should have:

cash_transactions
├── payment_transaction_id → PaymentTransaction (audit)
├── credit_wallet_transaction_id → WalletTransaction (buyer credit)
├── debit_wallet_transaction_id → WalletTransaction (seller debit, null for Admin→SA)
└── cash_transaction_events[] → event type 'created'
Flowdebit_wallet_txcredit_wallet_txpayment_tx
Admin → SAnullSA credit
SA → AgentSA debitAgent credit
Agent → ShopAgent debitShop credit

USD wallet: all 3 flows use walletFlow: 'none' on PaymentTransaction — shop/agent USD wallet unaffected. ✅


8. Test coverage gaps

ScenarioAdmin→SASA→AgentAgent→Shop
Happy path + cash tx links
Inactive/terminated buyerpartial✅ terminated
Zero cost rate✅ validator
Wrong ownershipN/Apartial
Insufficient seller balance
paymentMethod passthrough❌ (assert)
Legacy distribute creates cash txN/A
Controller scope 403❌ integration❌ integration❌ integration
Invalid received_at

9. Recommendations by priority

P1 — Fix before wide production (1 PR, ~1–2 days)

  1. Sync paymentMethod into PaymentTransaction for SA→Agent and Agent→Shop (copy Admin→SA pattern).
  2. Block distribute for inactive buyer/seller:
    • SA→Agent: add suspended
    • Agent→Shop: add terminated + suspended for shop; optional agent suspended check
  3. getCashTransactionDetail — resolve performedBy by type:
    // performedByType → lookup Admin | SuperAgent | Agent

P2 — Shared cash settlement refactor (1 PR, ~2–3 days)

  1. Extract validateCashSettlementPayload(payload, buyer, options) shared across 3 flows (credits, payment_method, received_at, external_reference, V1 lock cash_received).
  2. Deprecate legacy distribute endpoints (410 or internal redirect to cash-credits with defaults).
  3. Fix listCashTransactions summary — separate aggregate query, not reduce on page items.
  4. Migrate Agent→Shop to paymentTransactionCoreService + inject dependencies (remove global.db).

P3 — Post-V1 hardening

  1. Idempotency key (Idempotency-Key header or client request_id) dedupe double-submit.
  2. generateTransactionCode — use DB sequence or advisory lock instead of count+1.
  3. Require created_at on detail API for partitioned table performance.
  4. Standardize route params (:shop_id everywhere).
  5. Admin cash withdrawal → create cash_transaction if full ledger needed.

10. Conclusion

Three cash settlement V1 flows meet business goals:

  • Record cash credit sales with cost rate snapshot
  • Transfer CDN wallet correctly across hierarchy
  • Ledger cash_transactions + event audit
  • Portal list/detail/export by scope

Highlight: Admin→SA flow is the best reference implementation — use as template to refactor the other two flows.

Main risks if P1 not fixed:

  • Payment audit (PaymentTransaction) and cash ledger (cash_transactions) diverge on payment method
  • API still distributes to inactive shop/agent
  • Detail page does not show performer for SA/Agent flows

Recommended next step: One PR "Cash Settlement Hardening P1" bundling 3 cross-cutting fixes, then PR "Cash Settlement Refactor P2" to unify validator + deprecate legacy.


Appendix — File reference

kioskgaming_backend/src/
├── constants/cashTransaction.js
├── services/biz/
│ ├── cashTransactionBizService.js # shared ledger
│ ├── superAgentCreditPurchaseBizService.js # Admin → SA
│ ├── superAgentCreditDistributionBizService.js # SA → Agent
│ └── creditDistributionBizService.js # Agent → Shop
├── controllers/
│ ├── admin/cashTransactionController.js
│ ├── superAgent/
│ │ ├── adminSuperAgentManagementController.js # fundSuperAgentCash
│ │ └── superAgentCashSettlementController.js
│ └── agent/agentCashSettlementController.js
├── routes/
│ ├── admin/cashTransactions.js
│ ├── admin/superAgents.js # POST :id/cash-funding
│ ├── superAgent.js
│ └── agent.js
└── tests/unit/
├── cashTransactionBizService.test.js
├── superAgentAdminCashFunding.test.js
├── superAgentCreditDistributionBizService.test.js
└── creditDistributionCashTx.test.js