Skip to content

Type Deep Dive — Wallet Balance / Hold Semantics

1. Ba lớp số dư cần phân biệt

LớpNguồnÝ nghĩa
wallet.amountbảng walletSố dư base đang lưu trên ví
hold_amountDB functionSố tiền sender đang bị giữ theo rule hiện tại
balance khả dụngwallet_balance()amount - hold_amount - min_balance_capacity

Nếu không tách ba lớp này, rất dễ viết sai validation hoặc doc business.

2. wallet_balance() đang làm gì?

Function này trả về:

  • amount,
  • hold_amount,
  • balance,
  • metadata của wallet type.

Rule chính:

text
balance = wallet.amount - wallet_get_hold_amount(user_id, wallet_type_id) - min_balance_capacity

min_balance_capacity đến từ wallet_type, nghĩa là:

  • không phải mọi ví đều rút/chuyển toàn bộ amount,
  • một phần có thể bị giữ cố định theo cấu hình loại ví.

3. wallet_get_hold_amount() đang hold cái gì?

Theo code/migration hiện tại, hold chỉ cộng các sender legs:

  • thuộc request type T hoặc W,
  • status là P.

Đây là điểm cực quan trọng vì nhiều màn hình hoặc business wording hay hiểu “request đang chờ duyệt” là bị giữ tiền, nhưng implementation hiện tại không hoàn toàn như vậy.

4. walletBalances action khác gì wallet_balance()?

walletBalances là Hasura action/facade, không chỉ là function query:

  • ép user_id về caller nếu không phải admin,
  • lọc wallet type theo flags disabled, promotion, default,
  • tự tạo wallet rows còn thiếu bằng insert on_conflict do nothing.

Mental model đúng:

Read pathBản chất
wallet_balance()Read model / SQL function
wallet_stats()Aggregate read model
walletBalancesAction facade có side effect cấp phát wallet

5. FE đang đọc balance như thế nào?

Các surface chính:

  • WithdrawRequestCreate.tsx
  • WithdrawRequestDetail.tsx
  • WithdrawRequestForm.tsx
  • WithdrawAffiliateForm.tsx
  • Collaborators.tsx

Nhưng có drift:

  • một số nơi đọc wallet_balance,
  • một số nơi đọc wallet_stats,
  • một số nơi dựa vào thứ tự phần tử response thay vì lookup theo wallet_type_id.

6. Risk lớn nhất của semantics hiện tại

6.1 Hold của status R

Family refund/withdraw thường chạy ở R -> S/C/Reject, trong khi hold_amount chỉ cộng status P.

Inference mạnh:

  • request đang chờ reviewer nhưng còn ở R có thể chưa reserve số dư,
  • business expectation và implementation đang có khoảng lệch.

6.2 CheckWalletBalanceEnough() có dấu hiệu đọc sai lớp dữ liệu

Backend helper hiện query wallet_balance nhưng đọc amount, không dùng balance hoặc hold_amount.

Hệ quả:

  • check khả dụng có thể bỏ qua tiền đang hold,
  • min_balance_capacity có thể không được tôn trọng ở mọi path.

6.3 Auto-create wallet làm mờ ranh giới read/write

Một màn hình “chỉ mở để xem balance” vẫn có thể tạo wallet row mới.

Hệ quả:

  • audit/test seed data phải tính tới side effect này,
  • không nên giả định “wallet row chỉ sinh khi user dùng ví lần đầu để giao dịch”.

7. Rủi ro / Findings kỹ thuật

IDMứcFinding
BH-F01P1hold_amount hiện có dấu hiệu không cover request status R, trong khi nhiều flow approval thực tế dùng R.
BH-F02P1CheckWalletBalanceEnough() đọc amount thay vì balance, có nguy cơ bỏ qua hold/min-balance.
BH-F03P1Một số FE component phụ thuộc order của response wallet_balance, không lookup theo wallet_type_id.
BH-F04P2walletBalances là read facade có side effect auto-create row, dễ làm sai assumption của QA/reporting seed.