Tại sao cần test kỹ trước khi go-live?
Thanh toán là nghiệp vụ nhạy cảm nhất trong bất kỳ ứng dụng nào. Một lỗi nhỏ — IPN handler bị crash, số tiền tính sai, đơn hàng không đóng — trực tiếp ảnh hưởng đến doanh thu và uy tín.
TConnect Sandbox cho phép test toàn bộ luồng thực (bao gồm IPN callback, mã hóa AES, tạo VA) mà không cần:
- KYB (Know Your Business) doanh nghiệp
- Tài khoản ngân hàng thật
- Tiền thật
Thiết lập môi trường
Endpoint
| Môi trường | Base URL |
|---|---|
| Sandbox | https://sme-open-api-sandbox.tconnect.vn |
| Production | https://sme-open-api.tconnect.vn |
Chỉ thay đổi base_url khi chuyển từ sandbox lên production — không cần sửa code logic.
Credential Sandbox
Đăng ký tại tconnect.vn/developers để nhận:
username/passwordclient_id/client_secretpartner_codeaes_key(hex string)
Cấu hình bằng Environment Variables
# .env.sandbox
TCONNECT_BASE_URL=https://sme-open-api-sandbox.tconnect.vn
TCONNECT_PARTNER_CODE=SANDBOX_PARTNER_001
TCONNECT_USERNAME=[email protected]
TCONNECT_PASSWORD=your_sandbox_password
TCONNECT_CLIENT_ID=sandbox_client_id
TCONNECT_CLIENT_SECRET=sandbox_client_secret
TCONNECT_AES_KEY=dummyhexstring1234567890abcdef
# .env.production
TCONNECT_BASE_URL=https://sme-open-api.tconnect.vn
# ... production credentials// Load đúng env file theo môi trường
env = read_env_var("APP_ENV", default="sandbox")
load_env_file(".env.{env}")
TCONNECT_BASE_URL = read_env_var("TCONNECT_BASE_URL")
TCONNECT_AES_KEY = hex_decode(read_env_var("TCONNECT_AES_KEY"))
TCONNECT_PARTNER = read_env_var("TCONNECT_PARTNER_CODE")Test Checklist: Luồng QR thanh toán
1. Login & lấy token
function test_login():
token, refresh = tconnect.login(USERNAME, PASSWORD, CLIENT_ID, CLIENT_SECRET)
assert token is not null
assert length(token) > 10
print "Login OK — token length: {length(token)}"2. Tạo Virtual Account
function test_create_va():
va = tconnect.create_va(
bank_code: "970454",
account_name: "TEST MERCHANT CO LTD"
)
assert va.va_number starts with "VA"
assert va.status == "active"
print "VA created: {va.va_number}"
return va.va_number3. Tạo QR động
function test_create_qr(va_number):
order_id = "TEST-ORDER-" + current_unix_timestamp()
qr = tconnect.create_qr(
order_id: order_id,
va: va_number,
bincode: "970454",
service_code: "bidv-qr",
amount: 50000 // 50,000 VND test
)
assert "image_png_base64" in qr
assert "qr_content" in qr
assert length(qr.image_png_base64) > 100
print "QR created for order: {order_id}"
print "QR content preview: {first 50 chars of qr.qr_content}..."
return order_id, qr4. Simulate IPN (Sandbox feature)
Sandbox cho phép trigger IPN thủ công:
// Trigger IPN giả lập để test handler của bạn
function test_simulate_ipn(order_id):
payload = {
order_id: order_id,
status: "SUCCESS",
amount: 50000
}
POST {SANDBOX_BASE_URL}/openapi/v1/test/simulate-ipn
body: { data: encrypt_payload(payload, AES_KEY) }
headers: AUTH_HEADERS
result = decrypt_response(response.data, AES_KEY)
print "IPN simulated: {result}"5. Test IPN Handler của bạn
function test_ipn_handler():
// Simulate IPN payload từ TConnect
ipn_payload = {
data: hex_encode(encrypt_payload({
request_id: "req_test_001",
order_id: "TEST-ORDER-12345",
status: "SUCCESS",
amount: 50000,
retrieval_ref_no: "TXN001"
}, AES_KEY))
}
response = POST /webhook/tconnect/ipn with body ipn_payload
assert response.status_code == 200
assert response.body.status == "ok"
// Verify đơn hàng đã được mark paid
order = db.get_order("TEST-ORDER-12345")
assert order.status == "paid"
function test_ipn_idempotency():
// Test rằng IPN trùng lặp không được xử lý 2 lần
ipn_payload = { data: "..." } // Same request_id
response1 = POST /webhook/tconnect/ipn with body ipn_payload
response2 = POST /webhook/tconnect/ipn with body ipn_payload
assert response1.status_code == 200
assert response2.status_code == 200
// Verify chỉ xử lý 1 lần
payments = db.get_payments_for_order("TEST-ORDER-12345")
assert length(payments) == 1Test các edge cases quan trọng
| Test case | Mô tả | Kết quả mong đợi |
|---|---|---|
| IPN SUCCESS | Thanh toán thành công | Order marked paid |
| IPN FAILED | Thanh toán thất bại | Order marked failed |
| IPN duplicate | Cùng request_id gửi 2 lần | Chỉ xử lý 1 lần |
| Amount mismatch | IPN amount ≠ order amount | Flag for review |
| Invalid decrypt | Payload bị corrupted | Return 200, log error |
| Unknown order_id | Order không tồn tại | Return 200, log warning |
| Token expired | 401 khi gọi API | Auto-refresh và retry |
Expose local server cho IPN (ngrok)
Sandbox TConnect cần gọi được vào ipn_url của bạn. Trong môi trường local, dùng ngrok:
# Cài ngrok
brew install ngrok # macOS
# hoặc: https://ngrok.com/download
# Expose port 8000
ngrok http 8000
# Output:
# Forwarding https://abc123.ngrok.io → http://localhost:8000Dùng https://abc123.ngrok.io/webhook/tconnect/ipn làm ipn_url khi test.
// Đọc ngrok URL tự động từ ngrok local API
function get_ngrok_url():
GET http://localhost:4040/api/tunnels
tunnels = response.tunnels
https_tunnel = find first tunnel where tunnel.proto == "https"
return https_tunnel.public_url
IPN_URL = get_ngrok_url() + "/webhook/tconnect/ipn"Production readiness checklist
Trước khi chuyển sang production, đảm bảo:
Authentication
✓ Token caching (không login mỗi request)
✓ Token refresh khi gần hết hạn
✓ Retry sau 401 với token mới
Encryption
✓ AES key lưu trong secret manager (không hardcode)
✓ Test decrypt mọi response
IPN Handler
✓ Idempotency với request_id
✓ Luôn return 200
✓ Log đầy đủ: request_id, order_id, status, amount
✓ Reconciliation cronjob
Error handling
✓ Retry với exponential backoff cho transient errors
✓ Alert khi error rate tăng đột biến
✓ Fallback nếu IPN không đến sau 30 phút
Security
✓ IPN endpoint không expose trong docs/swagger
✓ Rate limiting
✓ Validate source IP (nếu có whitelist từ TConnect)
Kết luận
Sandbox là môi trường an toàn để thử nghiệm mọi scenario trước khi money thật vào cuộc. Đừng bỏ qua test các edge cases — đặc biệt là idempotency và amount mismatch. Một hệ thống thanh toán tốt là hệ thống xử lý được cả các tình huống bất thường mà không cần can thiệp thủ công.