Code Review — Backend Cash Settlement V1
Reviewer perspective: Senior Engineer / Architect
Phạm vi: Backend kioskgaming_backend — 3 luồng cash settlement V1
Ngày review: 2026-06-20
Trạng thái: V1 đã triển khai, ship được staging/internal; cần hardening trước production scale
1. Tóm tắt điều hành
Ba luồng cash settlement V1 đã được triển khai theo cùng một mô hình:
- Validate buyer + cost rate snapshot
- Wallet transfer (credit CDN wallet của buyer)
PaymentTransaction(audit legacy)cash_transactions+cash_transaction_events(ledger mới)- API list/detail/export theo scope portal
Verdict tổng thể: Approve có điều kiện — nghiệp vụ V1 đúng, atomicity tốt, tái sử dụng cashTransactionBizService hợp lý. Tuy nhiên 3 luồng chưa đồng đều về chất lượng code (Admin→SA tốt nhất; Agent→Shop kế thừa nhiều technical debt cũ). Có 5 issue cross-cutting ảnh hưởng cả 3 luồng cần gom fix trong 1 PR refactor.
| Luồng | Flow enum | Maturity | Ship V1? |
|---|---|---|---|
| Admin → Super Agent | admin_super_agent | ★★★★☆ | ✅ |
| Super Agent → Agent | super_agent_agent | ★★★☆☆ | ✅ |
| Agent → Shop | agent_shop | ★★★☆☆ | ✅ (có caveats) |
2. Kiến trúc tổng quan
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 vẫn tồn tại:
POST /api/super-agent/agents/:id/credits/distribute→ gọidistribute()(sau refactor cũng tạocash_transaction)POST /api/agent/credits/distribute→ gọidistribute()(tương tự)
3. Review từng luồng
3.1 Admin → Super Agent (admin_super_agent)
File chính: superAgentCreditPurchaseBizService.js → applyAdminCashFunding
Điểm mạnh
- Validator tập trung:
validateAdminSuperAgentPayload()— validate credits, rate, payment method, received_at một chỗ. - PaymentTransaction sync paymentMethod: dùng biến
paymentMethodđã validate, không hardcode. - Dependency injection:
paymentTransactionCore+superAgentCorequasetDependencies— dễ test. - Wallet flow đúng: credit từ platform (
null→ super_agent wallet),debitWalletTransactionId = nullhợp lý (platform không có wallet debit). - Buyer balance after: refresh qua
walletCoreService.findByOwner— chính xác hơn 2 luồng kia. - Super agent status check:
status !== 'active'→ reject. - Backward compat payload: hỗ trợ legacy
(superAgentId, amount, context)và object payload mới.
Điểm yếu
| # | Issue | Mức |
|---|---|---|
| A1 | V1 UI khóa cash_received = expected, nhưng validator vẫn cho phép cash_received ≠ expected nếu client gửi khác — chưa enforce server-side lock V1 | P2 |
| A2 | sellerType = platform, sellerId = null — list/filter theo seller khó; admin list OK vì không scope seller | P3 |
| A3 | Withdraw (applyAdminCashWithdrawal) không tạo cash_transaction — asymmetry nếu sau này cần full audit trail | P3 |
Tests
superAgentAdminCashFunding.test.js— happy path + integration mockcashTransactionBizService.test.js— validator + formatRowForApi
Coverage: tốt cho happy path; thiếu test inactive SA, insufficient balance platform credit.
3.2 Super Agent → Agent (super_agent_agent)
File chính: superAgentCreditDistributionBizService.js → distribute / distributeCashCredits
Điểm mạnh
- Ownership check:
assertManagedAgent(superAgentId, agentId)— agent phải thuộc SA. - Terminated agent blocked:
status === 'terminated'→ reject. - Cost rate snapshot:
buyerCostRate = agent.costRate(buyer = agent). - V1 locked:
cashReceived = expectedCash. - Links đầy đủ: payment + debit SA wallet + credit agent wallet.
- Controller scope: list/detail ép
seller_type=super_agent,seller_id=req.superAgent.id.
Điểm yếu
| # | Issue | Mức |
|---|---|---|
| S1 | PaymentTransaction.paymentMethod hardcode 'cash' trong khi cash_transactions.paymentMethod lấy từ payload — audit mismatch | P1 |
| S2 | Không check agent suspended — chỉ terminated | P2 |
| S3 | Không check SA balance trước transfer — rely on walletTransferService.transfer throw (message có thể không user-friendly) | P2 |
| S4 | receivedAt: new Date(String(...)) không validate Invalid Date | P2 |
| S5 | Legacy POST /credits/distribute vẫn active, tạo cash tx thiếu metadata (payment_method, external_ref) | P2 |
| S6 | performedByType: 'super_agent' dùng string literal thay vì HIERARCHY_OWNER_TYPES | P3 |
| S7 | buyerBalanceAfter lấy từ wt.transaction?.balanceAfter — không refresh wallet như Admin flow | P3 |
Tests
superAgentCreditDistributionBizService.test.js— happy path + terminated agent
Coverage: thiếu wrong agent, zero cost rate, insufficient balance, paymentMethod sync.
3.3 Agent → Shop (agent_shop)
File chính: creditDistributionBizService.js → distribute / distributeCashCredits
Điểm mạnh
- Shop ownership:
shop.agentId !== agentId→ reject. - Cost rate snapshot:
buyerCostRate = shop.costRate. - V1 locked:
cashReceived = expectedCash. - Pattern mirror SA→Agent: cùng structure create cash tx + event.
- Controller scope: tương tự SA portal.
Điểm yếu
| # | Issue | Mức |
|---|---|---|
| G1 | PaymentTransaction.paymentMethod hardcode 'cash' — cùng bug S1 | P1 |
| G2 | Không validate shop status (terminated / suspended) — API vẫn distribute được | P1 |
| G3 | global.db trực tiếp cho Agent + PaymentTransaction — lệch kiến trúc, khó test/migrate | P2 |
| G4 | PaymentTransaction.create trực tiếp thay vì paymentTransactionCoreService | P2 |
| G5 | Legacy POST /api/agent/credits/distribute + Dashboard/ShopsPage vẫn dùng → cash tx chất lượng thấp | P2 |
| G6 | Route param inconsistency: :id (cash-credits) vs :shop_id (withdraw, detail) | P3 |
| G7 | Không có shared validateAgentShopPayload — validation duplicate inline | P2 |
Tests
creditDistributionCashTx.test.js— 5 cases (happy, wrong agent, zero rate, wrapper, insufficient balance)
Coverage: tốt hơn SA flow về edge cases; thiếu shop status, legacy distribute path.
4. Hạ tầng dùng chung — cashTransactionBizService
File: cashTransactionBizService.js
Điểm mạnh
- Single entry
createCashTransactionRecord+ auto eventcreated generateTransactionCodeformatCTX-YYYYMMDD-NNNNbuildListWherehỗ trợ filter flow, seller, buyer, date range, searchformatRowForApinormalize snake_case/camelCase- Export CSV cap 10k rows
Issue cross-cutting (ảnh hưởng cả 3 luồng)
| # | Issue | Mức | Chi tiết |
|---|---|---|---|
| X1 | getCashTransactionDetail — performedBy chỉ lookup Admin | P1 | SA/Agent detail page không hiện tên người thực hiện |
| X2 | generateTransactionCode race condition | P2 | count + 1 không lock — concurrent request có thể trùng code |
| X3 | List summary tính trên page hiện tại | P2 | totalExpected/totalReceived trong summary chỉ aggregate items của page, không phải full filter |
| X4 | Partitioned table — created_at optional ở detail | P2 | Không bắt buộc created_at → scan chậm khi data lớn |
| X5 | Chỉ có validateAdminSuperAgentPayload — thiếu validator cho SA→Agent và Agent→Shop | P2 | Validation logic rải rác trong từng biz service |
| X6 | global.db trong getModels() | P2 | Coupling, khó unit test integration thật |
5. Ma trận so sánh 3 luồng
| Tiêu chí | 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 cho phép 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. Bảo mật & phân quyền
| 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 |
Ghi chú: Admin detail không cần seller scope — đúng thiết kế. SA/Agent detail check seller là pattern tốt, nên giữ.
7. Data model & links
Mỗi cash transaction V1 nên có:
cash_transactions
├── payment_transaction_id → PaymentTransaction (audit)
├── credit_wallet_transaction_id → WalletTransaction (buyer credit)
├── debit_wallet_transaction_id → WalletTransaction (seller debit, null cho 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: cả 3 luồng đều walletFlow: 'none' trên PaymentTransaction — shop/agent USD wallet không bị ảnh hưởng. ✅
8. Test coverage gap
| 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. Khuyến nghị theo priority
P1 — Fix trước khi production rộng (1 PR, ~1–2 ngày)
- Sync
paymentMethodvàoPaymentTransactioncho SA→Agent và Agent→Shop (copy pattern Admin→SA). - Block distribute cho buyer/seller không active:
- SA→Agent: thêm
suspended - Agent→Shop: thêm
terminated+suspendedcho shop; optional check agent suspended
- SA→Agent: thêm
getCashTransactionDetail— resolveperformedBytheo type:// performedByType → lookup Admin | SuperAgent | Agent
P2 — Refactor cash settlement chung (1 PR, ~2–3 ngày)
- Extract
validateCashSettlementPayload(payload, buyer, options)dùng chung 3 luồng (credits, payment_method, received_at, external_reference, V1 lock cash_received). - Deprecate legacy distribute endpoints (
410hoặc redirect nội bộ sang cash-credits với defaults). - Fix
listCashTransactionssummary — aggregate query riêng, không reduce trên page items. - Migrate Agent→Shop sang
paymentTransactionCoreService+ inject dependencies (bỏglobal.db).
P3 — Hardening sau V1
- Idempotency key (
Idempotency-Keyheader hoặc clientrequest_id) dedupe double-submit. generateTransactionCode— dùng DB sequence hoặc advisory lock thay vì count+1.- Require
created_attrên detail API cho partitioned table performance. - Chuẩn hóa route params (
:shop_ideverywhere). - Admin cash withdrawal → tạo
cash_transactionnếu cần full ledger.
10. Kết luận
Ba luồng cash settlement V1 đạt mục tiêu nghiệp vụ:
- Ghi nhận bán credit bằng cash với cost rate snapshot
- Transfer CDN wallet đúng chiều hierarchy
- Ledger
cash_transactions+ event audit - Portal list/detail/export theo scope
Điểm nổi bật: Admin→SA flow là reference implementation tốt nhất — nên dùng làm template refactor 2 luồng còn lại.
Rủi ro chính nếu không fix P1:
- Payment audit (
PaymentTransaction) và cash ledger (cash_transactions) lệch payment method - API distribute cho shop/agent inactive vẫn chạy được
- Detail page không hiện performer cho SA/Agent flows
Recommended next step: Một PR "Cash Settlement Hardening P1" gom 3 fix cross-cutting, sau đó PR "Cash Settlement Refactor P2" unify validator + deprecate legacy.
Phụ lục — 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