1. Bối cảnh nghiệp vụ: ví điện tử là hệ thống tiền thật
Một ví điện tử hay ứng dụng fintech xử lý tiền thật, nên sai sót không chỉ gây khó chịu mà tạo thiệt hại tài chính và rủi ro pháp lý. Ở quy mô năm triệu người dùng, mỗi ngày có hàng trăm nghìn lệnh nạp tiền, và mỗi lệnh phải tuân thủ quy định chống rửa tiền, hạn mức theo mức độ định danh khách hàng, và các luật chống gian lận. Ba trục kiểm thử cốt lõi là: nạp tiền phải bảo toàn số dư và idempotent, hạn mức theo tầng KYC phải được thực thi ở backend, và các luật fraud dựa trên tốc độ giao dịch, vị trí địa lý và thiết bị phải chặn được hành vi bất thường mà không cản khách thật.
Bài viết này áp dụng nguyên tắc oracle-first cho fintech: thay vì khẳng định 'nạp thành công', ta khẳng định các bất biến tài chính. Chúng ta sẽ định nghĩa mô hình ledger kiểu ghi kép làm oracle bảo toàn tiền, dựng bảng hạn mức theo tầng KYC, viết test plan và ma trận ca, rồi đi sâu vào các ca thất bại: idempotency khi retry nạp tiền, vượt hạn mức, các luật fraud, và đối soát với đối tác thanh toán. Playwright kết hợp request context cho phép seed tầng KYC, bơm chuỗi sự kiện fraud và kiểm sổ cái cân bằng, tất cả một cách tất định.
2. Ledger ghi kép: bất biến bảo toàn tiền làm oracle
Trái tim của mọi hệ thống tiền tệ đáng tin là sổ cái ghi kép. Mỗi giao dịch tạo ít nhất hai bút toán cân bằng nhau: một bên ghi nợ, một bên ghi có, và tổng của mọi bút toán trong hệ thống luôn bằng không. Đây là bất biến bảo toàn tiền mạnh nhất và là oracle vàng cho kiểm thử fintech. Khi khách nạp một triệu đồng, tài khoản ví khách tăng một triệu và tài khoản quỹ đối tác giảm một triệu tương ứng; tiền không tự sinh ra hay biến mất. Test không kiểm 'số dư hiển thị đúng' mà kiểm 'tổng ghi nợ bằng tổng ghi có' và 'không có bút toán mồ côi'.
-- Sổ cái ghi kép: mỗi giao dịch phải cân bằng, không đâu tiền tự sinh
CREATE TABLE ledger_entry (
id UUID PRIMARY KEY,
txn_id UUID NOT NULL, -- gom các bút toán của cùng 1 giao dịch
account_id BIGINT NOT NULL,
direction TEXT NOT NULL CHECK (direction IN ('DEBIT','CREDIT')),
amount BIGINT NOT NULL CHECK (amount > 0), -- số nguyên, đơn vị nhỏ nhất
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ORACLE bảo toàn tiền: với MỌI txn_id, tổng DEBIT = tổng CREDIT
-- SELECT txn_id,
-- SUM(CASE WHEN direction='DEBIT' THEN amount ELSE 0 END) AS d,
-- SUM(CASE WHEN direction='CREDIT' THEN amount ELSE 0 END) AS c
-- FROM ledger_entry GROUP BY txn_id HAVING d <> c; -- phải trả về 0 hàng
CREATE TABLE top_up (
id UUID PRIMARY KEY,
user_id BIGINT NOT NULL,
amount BIGINT NOT NULL,
idempotency_key TEXT UNIQUE NOT NULL, -- BẤT BIẾN: 1 key → 1 lần cộng tiền
status TEXT NOT NULL -- PENDING | SUCCEEDED | FAILED
);Với tester, sổ cái ghi kép biến việc kiểm tiền thành một truy vấn đơn giản mà cực mạnh: nhóm các bút toán theo giao dịch, kiểm tổng ghi nợ bằng tổng ghi có ở mọi giao dịch. Nếu có một giao dịch lệch, đó là bằng chứng tiền bị tạo hoặc mất ở đâu đó. Một bất biến toàn cục nữa là tổng số dư của mọi tài khoản khách cộng lại phải bằng tổng tiền đối tác đã chuyển vào, trừ phí. Những truy vấn này chính là oracle: chúng không quan tâm giao diện hiển thị gì, chỉ quan tâm sổ cái có cân bằng hay không.
3. Hạn mức theo tầng KYC: đặc tả thành bảng quyết định
KYC là quy trình định danh khách hàng, và mức độ định danh quyết định hạn mức giao dịch. Khách chưa xác minh chỉ được nạp một khoản nhỏ; khách đã nộp giấy tờ tuỳ thân được hạn cao hơn; khách xác minh nâng cao gồm địa chỉ và nguồn tiền được hạn cao nhất. Đây là yêu cầu tuân thủ bắt buộc, không phải tuỳ chọn giao diện. Sai lầm phổ biến là chỉ ẩn nút trên giao diện khi vượt hạn, còn backend vẫn chấp nhận — một kẻ tấn công gọi thẳng API sẽ vượt qua. Vì thế oracle phải kiểm ở backend: tổng nạp trong hai mươi tư giờ không được vượt hạn mức của tầng hiện tại.
import { test, expect } from './fixtures';
// data-driven: mỗi tầng KYC có hạn mức riêng — kiểm ở BACKEND, không phải UI
const tiers = [
{ tier: 0, limit: 2_000_000, amount: 2_500_000, expect: 'DENIED' }, // vượt → từ chối
{ tier: 0, limit: 2_000_000, amount: 1_500_000, expect: 'ALLOWED' },
{ tier: 1, limit: 20_000_000, amount: 25_000_000, expect: 'DENIED' },
{ tier: 2, limit: 200_000_000, amount: 150_000_000, expect: 'ALLOWED' },
];
for (const t of tiers) {
test(`tier ${t.tier} nạp ${t.amount} phải ${t.expect}`, async ({ request }) => {
// seed người dùng với đúng tầng KYC qua test-only API
const user = await (await request.post('/api/test/users', { data: { kycTier: t.tier } })).json();
const res = await request.post('/api/topups', {
data: { userId: user.id, amount: t.amount, key: crypto.randomUUID() },
});
// ORACLE: vượt hạn mức tier → backend trả 403, KHÔNG cộng tiền
if (t.expect === 'DENIED') {
expect(res.status()).toBe(403);
const wallet = await (await request.get(`/api/test/wallets/${user.id}`)).json();
expect(wallet.balance).toBe(0); // số dư không đổi khi bị từ chối
} else {
expect(res.status()).toBe(201);
}
});
}Một fintech kiểm hạn mức chỉ ở phía giao diện: khi vượt, nút nạp bị mờ đi. Đội bảo mật thử gọi thẳng endpoint nạp tiền bằng công cụ API và nạp được gấp mười lần hạn mức, vì backend không kiểm lại. Đây là lỗ hổng tuân thủ nghiêm trọng có thể bị cơ quan quản lý phạt. Đội khắc phục bằng cách chuyển toàn bộ kiểm hạn mức xuống backend và thêm ca test gọi thẳng API vượt hạn phải nhận 403. Bài học: mọi ràng buộc tuân thủ phải thực thi ở backend, giao diện chỉ là lớp tiện lợi.
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!