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:
- Validate buyer + cost rate snapshot
- Wallet transfer (credit buyer CDN wallet)
PaymentTransaction(legacy audit)cash_transactions+cash_transaction_events(new ledger)- 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.
| Flow | Flow enum | Maturity | Ship V1? |
|---|---|---|---|
| Admin → Super Agent | admin_super_agent | ★★★★☆ | ✅ |
| Super Agent → Agent | super_agent_agent | ★★★☆☆ | ✅ |
| Agent → Shop | agent_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
| Layer | Admin → SA | SA → Agent | Agent → Shop |
|---|---|---|---|
| Biz | superAgentCreditPurchaseBizService.applyAdminCashFunding | superAgentCreditDistributionBizService.distribute / distributeCashCredits | creditDistributionBizService.distribute / distributeCashCredits |
| Create API | adminSuperAgentManagementController.fundSuperAgentCash | superAgentCashSettlementController.createAgentCashCredits | agentCashSettlementController.createShopCashCredits |
| Ledger API | admin/cashTransactionController | superAgentCashSettlementController (scoped) | agentCashSettlementController (scoped) |
| Shared | cashTransactionBizService | ↑ | ↑ |
| Constants | constants/cashTransaction.js | ↑ | ↑ |
API endpoints
| Portal | Create | List | Detail | Export |
|---|---|---|---|---|
| Admin | POST /api/admin/super-agents/:id/cash-funding | GET /api/admin/cash-transactions | GET /api/admin/cash-transactions/:id | GET /api/admin/cash-transactions/export |
| Super Agent | POST /api/super-agent/agents/:id/cash-credits | GET /api/super-agent/cash-transactions | GET /api/super-agent/cash-transactions/:id | GET /api/super-agent/cash-transactions/export |
| Agent | POST /api/agent/shops/:id/cash-credits | GET /api/agent/cash-transactions | GET /api/agent/cash-transactions/:id | GET /api/agent/cash-transactions/export |
Legacy endpoints still exist:
POST /api/super-agent/agents/:id/credits/distribute→ callsdistribute()(after refactor also createscash_transaction)POST /api/agent/credits/distribute→ callsdistribute()(same)
3. Review by flow
3.1 Admin → Super Agent (admin_super_agent)
Main file: superAgentCreditPurchaseBizService.js → applyAdminCashFunding
Strengths
- Centralized validator:
validateAdminSuperAgentPayload()— validates credits, rate, payment method, received_at in one place. - PaymentTransaction sync paymentMethod: uses validated
paymentMethodvariable, not hardcoded. - Dependency injection:
paymentTransactionCore+superAgentCoreviasetDependencies— easy to test. - Correct wallet flow: credit from platform (
null→ super_agent wallet),debitWalletTransactionId = nullvalid (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
| # | Issue | Severity |
|---|---|---|
| A1 | V1 UI locks cash_received = expected, but validator still allows cash_received ≠ expected if client sends different value — server-side V1 lock not enforced | P2 |
| A2 | sellerType = platform, sellerId = null — list/filter by seller difficult; admin list OK since no seller scope | P3 |
| A3 | Withdraw (applyAdminCashWithdrawal) does not create cash_transaction — asymmetry if full audit trail needed later | P3 |
Tests
superAgentAdminCashFunding.test.js— happy path + integration mockcashTransactionBizService.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.js → distribute / 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
| # | Issue | Severity |
|---|---|---|
| S1 | PaymentTransaction.paymentMethod hardcoded 'cash' while cash_transactions.paymentMethod comes from payload — audit mismatch | P1 |
| S2 | No check for agent suspended — only terminated | P2 |
| S3 | No SA balance check before transfer — relies on walletTransferService.transfer throw (message may not be user-friendly) | P2 |
| S4 | receivedAt: new Date(String(...)) does not validate Invalid Date | P2 |
| S5 | Legacy POST /credits/distribute still active, creates cash tx missing metadata (payment_method, external_ref) | P2 |
| S6 | performedByType: 'super_agent' uses string literal instead of HIERARCHY_OWNER_TYPES | P3 |
| S7 | buyerBalanceAfter from wt.transaction?.balanceAfter — no wallet refresh like Admin flow | P3 |
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.js → distribute / 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
| # | Issue | Severity |
|---|---|---|
| G1 | PaymentTransaction.paymentMethod hardcoded 'cash' — same bug as S1 | P1 |
| G2 | No shop status validation (terminated / suspended) — API still distributes | P1 |
| G3 | global.db directly for Agent + PaymentTransaction — architecture mismatch, hard to test/migrate | P2 |
| G4 | PaymentTransaction.create directly instead of paymentTransactionCoreService | P2 |
| G5 | Legacy POST /api/agent/credits/distribute + Dashboard/ShopsPage still used → low quality cash tx | P2 |
| G6 | Route param inconsistency: :id (cash-credits) vs :shop_id (withdraw, detail) | P3 |
| G7 | No shared validateAgentShopPayload — validation duplicated inline | P2 |
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+ autocreatedevent generateTransactionCodeformatCTX-YYYYMMDD-NNNNbuildListWheresupports filter flow, seller, buyer, date range, searchformatRowForApinormalizes snake_case/camelCase- Export CSV cap 10k rows
Cross-cutting issues (affects all 3 flows)
| # | Issue | Severity | Details |
|---|---|---|---|
| X1 | getCashTransactionDetail — performedBy only looks up Admin | P1 | SA/Agent detail page does not show performer name |
| X2 | generateTransactionCode race condition | P2 | count + 1 without lock — concurrent requests may duplicate code |
| X3 | List summary calculated on current page | P2 | totalExpected/totalReceived in summary only aggregate current page items, not full filter |
| X4 | Partitioned table — created_at optional on detail | P2 | Not requiring created_at → slow scan at scale |
| X5 | Only validateAdminSuperAgentPayload exists — missing validators for SA→Agent and Agent→Shop | P2 | Validation logic scattered across biz services |
| X6 | global.db in getModels() | P2 | Coupling, hard to unit test real integration |
5. Comparison matrix — 3 flows
| Criterion | Admin → SA | SA → Agent | Agent → Shop |
|---|---|---|---|
| Flow enum | admin_super_agent | super_agent_agent | agent_shop |
| Seller | platform (null) | super_agent | agent |
| Buyer | super_agent | agent | shop |
| Cost rate snapshot | SA.costRate | Agent.costRate | Shop.costRate |
| V1 cash_received lock | UI lock; server allows override | Server lock (= expected) | Server lock (= expected) |
| Wallet debit side | — (platform mint) | SA wallet | Agent wallet |
| Wallet credit side | SA wallet | Agent wallet | Shop wallet |
| Payment provider | admin_cash | super_agent_cash | agent_cash |
| PaymentMethod sync | ✅ | ❌ hardcode cash | ❌ hardcode cash |
| Shared validator | ✅ | ❌ inline | ❌ inline |
| Buyer status check | SA active | Agent terminated only | ❌ none |
| Core service pattern | ✅ injected | ✅ partial | ❌ global.db |
| Legacy endpoint | — | /credits/distribute | /credits/distribute |
| Controller scope (list) | Admin all flows | SA scoped | Agent scoped |
| Controller scope (detail) | Admin all | SA 403 check | Agent 403 check |
| Unit tests | ★★★★ | ★★★ | ★★★★ |
6. Security & authorization
| Check | Admin | Super Agent | Agent |
|---|---|---|---|
| Auth middleware | ✅ admin | ✅ superAgent | ✅ agent |
| Create: ownership | Admin can fund any SA | SA can only fund managed agents | Agent can only fund own shops |
| List: scope enforced | All (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 UUID | Admin OK by design | Mitigated by seller check | Mitigated 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.
7. Data model & links
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'
| Flow | debit_wallet_tx | credit_wallet_tx | payment_tx |
|---|---|---|---|
| Admin → SA | null | SA credit | ✅ |
| SA → Agent | SA debit | Agent credit | ✅ |
| Agent → Shop | Agent debit | Shop credit | ✅ |
USD wallet: all 3 flows use walletFlow: 'none' on PaymentTransaction — shop/agent USD wallet unaffected. ✅
8. Test coverage gaps
| Scenario | Admin→SA | SA→Agent | Agent→Shop |
|---|---|---|---|
| Happy path + cash tx links | ✅ | ✅ | ✅ |
| Inactive/terminated buyer | partial | ✅ terminated | ❌ |
| Zero cost rate | ✅ validator | ❌ | ✅ |
| Wrong ownership | N/A | partial | ✅ |
| Insufficient seller balance | ❌ | ❌ | ✅ |
| paymentMethod passthrough | ✅ | ❌ | ❌ (assert) |
| Legacy distribute creates cash tx | N/A | ❌ | ❌ |
| Controller scope 403 | ❌ integration | ❌ integration | ❌ integration |
| Invalid received_at | ❌ | ❌ | ❌ |
9. Recommendations by priority
P1 — Fix before wide production (1 PR, ~1–2 days)
- Sync
paymentMethodintoPaymentTransactionfor SA→Agent and Agent→Shop (copy Admin→SA pattern). - Block distribute for inactive buyer/seller:
- SA→Agent: add
suspended - Agent→Shop: add
terminated+suspendedfor shop; optional agent suspended check
- SA→Agent: add
getCashTransactionDetail— resolveperformedByby type:// performedByType → lookup Admin | SuperAgent | Agent
P2 — Shared cash settlement refactor (1 PR, ~2–3 days)
- Extract
validateCashSettlementPayload(payload, buyer, options)shared across 3 flows (credits, payment_method, received_at, external_reference, V1 lock cash_received). - Deprecate legacy distribute endpoints (
410or internal redirect to cash-credits with defaults). - Fix
listCashTransactionssummary — separate aggregate query, not reduce on page items. - Migrate Agent→Shop to
paymentTransactionCoreService+ inject dependencies (removeglobal.db).
P3 — Post-V1 hardening
- Idempotency key (
Idempotency-Keyheader or clientrequest_id) dedupe double-submit. generateTransactionCode— use DB sequence or advisory lock instead of count+1.- Require
created_aton detail API for partitioned table performance. - Standardize route params (
:shop_ideverywhere). - Admin cash withdrawal → create
cash_transactionif 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