Appearance
Type Deep Dive — Wallet Balance / Hold Semantics
1. Ba lớp số dư cần phân biệt
| Lớp | Nguồn | Ý nghĩa |
|---|---|---|
wallet.amount | bảng wallet | Số dư base đang lưu trên ví |
hold_amount | DB function | Số tiền sender đang bị giữ theo rule hiện tại |
balance khả dụng | wallet_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_capacitymin_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
ThoặcW, - 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_idvề 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 path | Bản chất |
|---|---|
wallet_balance() | Read model / SQL function |
wallet_stats() | Aggregate read model |
walletBalances | Action facade có side effect cấp phát wallet |
5. FE đang đọc balance như thế nào?
Các surface chính:
WithdrawRequestCreate.tsxWithdrawRequestDetail.tsxWithdrawRequestForm.tsxWithdrawAffiliateForm.tsxCollaborators.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 ở
Rcó 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_capacitycó 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
| ID | Mức | Finding |
|---|---|---|
| BH-F01 | P1 | hold_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-F02 | P1 | CheckWalletBalanceEnough() đọc amount thay vì balance, có nguy cơ bỏ qua hold/min-balance. |
| BH-F03 | P1 | Một số FE component phụ thuộc order của response wallet_balance, không lookup theo wallet_type_id. |
| BH-F04 | P2 | walletBalances là read facade có side effect auto-create row, dễ làm sai assumption của QA/reporting seed. |