Mở đầu — vì sao bài này quan trọng
Nếu bạn làm BA cho bất kỳ sản phẩm nào có liên quan đến tiền — bán hàng online, đặt vé, gọi xe, ví điện tử, SaaS thu phí thuê bao — thì sớm hay muộn bạn sẽ phải viết spec cho một tích hợp cổng thanh toán (payment gateway). Và đây thường là tích hợp "đáng sợ" nhất trong cả dự án, vì một lý do rất đơn giản: nó dính trực tiếp đến tiền của khách hàng. Một bug ở màn hình sản phẩm thì cùng lắm người dùng khó chịu. Một bug ở luồng thanh toán có thể khiến công ty trừ tiền khách hai lần, ghi nhận đơn "đã thanh toán" trong khi tiền chưa về, hoặc tệ hơn — bị kẻ gian giả mạo callback để "mua hàng miễn phí".
Ở Việt Nam, bức tranh thanh toán còn đặc thù hơn nhiều so với thị trường quốc tế. Chúng ta không chỉ có thẻ Visa/Mastercard. Người dùng Việt thanh toán qua VNPAY (cổng tổng hợp ngân hàng nội địa), ví MoMo, ZaloPay, ShopeePay, chuyển khoản QR theo chuẩn VietQR của NAPAS, và đặc biệt là COD (Cash on Delivery — thu tiền khi giao hàng) vẫn chiếm tỷ trọng rất lớn. Mỗi phương thức có một cách tích hợp, một mô hình bảo mật, một luồng đối soát riêng.
Bài này không yêu cầu bạn code. Mục tiêu là giúp bạn — với tư cách BA — hiểu đủ sâu về luồng nghiệp vụ và kỹ thuật của tích hợp cổng thanh toán VN để: viết được spec chính xác, đặt đúng câu hỏi với đối tác và đội kỹ thuật, và lường trước những tình huống lỗi mà sản phẩm bắt buộc phải xử lý. Lưu ý: bài này dùng lại nhiều khái niệm bạn đã học (webhook, idempotency, status code, signature) nhưng đặt chúng vào bối cảnh thanh toán VN thực tế.
Khái niệm cốt lõi
Các thành phần trong một giao dịch thanh toán
Trước khi nói về luồng, hãy thống nhất các nhân vật:
- Merchant (đơn vị bán hàng): chính là sản phẩm/website của bạn. Ví dụ một sàn e-commerce.
- Payment Gateway / PSP (Payment Service Provider): bên trung gian xử lý thanh toán — VNPAY, MoMo, ZaloPay. Họ kết nối merchant với hệ thống ngân hàng/ví.
- Acquirer / Ngân hàng: nơi tiền thực sự được trừ và chuyển.
- NAPAS: tổ chức chuyển mạch quốc gia, đứng sau hầu hết giao dịch thẻ nội địa và QR liên ngân hàng (VietQR).
- Customer: người trả tiền.
Hai mô hình kết nối: redirect và server-to-server
Là BA, bạn cần phân biệt hai kiểu tích hợp vì chúng ảnh hưởng trực tiếp đến trải nghiệm và spec:
- Redirect / hosted payment page: hệ thống của bạn tạo một URL thanh toán rồi điều hướng (redirect) trình duyệt người dùng sang trang của cổng (hoặc app ví). Người dùng nhập OTP, xác nhận, rồi được đưa ngược về website của bạn. Đây là mô hình của VNPAY, của luvng "Thanh toán qua MoMo/ZaloPay" trên web. Ưu điểm: bạn không phải chạm vào dữ liệu thẻ nhạy cảm, giảm gánh nặng tuân thủ PCI DSS.
- Server-to-server (API trực tiếp): backend của bạn gọi API của cổng để tạo giao dịch, nhận về QR code hoặc deeplink, và sau đó nhận kết quả qua callback. Phổ biến khi tích hợp QR động hoặc thanh toán in-app.
Hai kênh nhận kết quả: return URL và IPN/webhook
Đây là điểm sống còn mà rất nhiều BA non kinh nghiệm hiểu sai. Sau khi khách thanh toán, kết quả về với bạn qua hai con đường khác nhau:
- Return URL (redirect về client): cổng đưa trình duyệt người dùng quay lại website kèm tham số kết quả. Vấn đề: kênh này không đáng tin cậy để cập nhật đơn hàng. Người dùng có thể tắt trình duyệt giữa chừng, mất mạng, hoặc kẻ gian có thể tự gọi URL này với tham số giả.
- IPN / Webhook (server-to-server): cổng gọi trực tiếp đến một endpoint backend của bạn để báo kết quả. VNPAY gọi đây là IPN URL (Instant Payment Notification). Đây mới là nguồn sự thật (source of truth) để cập nhật trạng thái "đã thanh toán". (Bạn đã học về Webhooks vs Polling ở Bài 26 — đây chính là ứng dụng thực tế của nó.)
Chữ ký (signature) — chống giả mạo
Mọi cổng thanh toán VN đều yêu cầu ký dữ liệu bằng một secret key mà chỉ merchant và cổng biết. VNPAY dùng HMAC-SHA512 trên chuỗi tham số đã sắp xếp; MoMo và ZaloPay dùng HMAC-SHA256. Khi tạo yêu cầu thanh toán, bạn ký; khi nhận callback, bạn phải verify lại chữ ký trước khi tin bất cứ điều gì. Một spec thiếu bước "verify signature" là một spec có lỗ hổng bảo mật nghiêm trọng.
Tình huống thực tế
Ví dụ 1 — Sàn e-commerce tích hợp MoMo và bài học về IPN
Một startup thương mại điện tử giả định, gọi là "ChợSạch", bán thực phẩm hữu cơ, ra mắt tính năng thanh toán MoMo. Luồng họ thiết kế ban đầu như sau:
- Khách chọn giỏ hàng 850.000đ, bấm "Thanh toán bằng MoMo".
- Backend ChợSạch gọi API
createcủa MoMo, gửiorderId,amount,redirectUrl,ipnUrlvàsignature. - MoMo trả về một
payUrl(trên desktop là QR code, trên mobile là deeplink mở app MoMo). - Khách quét QR / xác nhận trong app MoMo, nhập mã PIN.
- MoMo trừ tiền ví, rồi gọi
ipnUrlcủa ChợSạch báo kết quả, đồng thời redirect khách vềredirectUrl.
Bài học: BA viết spec phải tách bạch rõ — IPN là nguồn cập nhật trạng thái, redirect chỉ để hiển thị. Sau khi sửa lại để IPN làm chủ, kèm một cron job đối soát các đơn "pending quá 15 phút" bằng API truy vấn trạng thái của MoMo (query endpoint), vấn đề biến mất.
Ví dụ 2 — VNPAY và bẫy "double charge" do thiếu idempotency
Một công ty bán vé sự kiện giả định, "VéNhanh", tích hợp VNPAY. Người dùng hay sốt ruột bấm "Thanh toán" nhiều lần, hoặc refresh trang khi mạng chậm. Hệ quả: hệ thống tạo nhiều vnp_TxnRef (mã giao dịch) cho cùng một đơn, và trong vài trường hợp VNPAY gọi IPN nhiều lần cho cùng một giao dịch (VNPAY có cơ chế retry IPN nếu lần đầu không nhận được response RspCode=00).
Hậu quả tiềm tàng: cùng một thanh toán bị cộng doanh thu hai lần trong báo cáo, hoặc gửi hai vé cho khách. May mắn đội kỹ thuật phát hiện sớm khi đối soát cuối ngày thấy số đơn lệch số giao dịch.
Cách xử lý đúng (và BA cần ghi vào spec):
- Mỗi đơn hàng gắn duy nhất một
vnp_TxnRef, sinh ở backend, không phải ở client. - Endpoint IPN phải idempotent: nếu nhận lại IPN cho giao dịch đã xử lý "success" rồi, thì chỉ trả về
RspCode=02(đơn đã được xác nhận) và không xử lý nghiệp vụ lần nữa. (Đây chính là khái niệm idempotency ở Bài 25.) - Verify số tiền
vnp_Amountkhớp đúng với đơn (VNPAY tính theo đơn vị x100, tức 850.000đ gửi đi là85000000).
Ví dụ 3 — COD và bài toán đối soát với đơn vị vận chuyển
Đừng quên COD — phương thức vẫn chiếm tỷ trọng rất lớn ở thị trường tỉnh và với nhóm khách chưa quen thanh toán online. Một nhà bán hàng thời trang trên nền tảng giả định "MặcĐẹp" cho phép COD qua các hãng giao hàng GHN, GHTK, Viettel Post.
Điểm khác biệt nghiệp vụ: với COD, tiền không về qua payment gateway mà qua đơn vị vận chuyển. Khách trả tiền mặt cho shipper, shipper nộp về hãng, hãng đối soát rồi chuyển khoản (remittance) về cho merchant theo chu kỳ (thường vài ngày một lần). Trạng thái "đã thu tiền" đến từ webhook của hãng vận chuyển, không phải từ cổng thanh toán.
Tình huống đau đầu: khách từ chối nhận hàng (return), hoặc shipper giao thất bại nhiều lần. Lúc đó đơn ở trạng thái "đã giao một phần" hay "hoàn hàng" và tiền không bao giờ về. Spec của MặcĐẹp phải mô hình hóa được các trạng thái: cho_lay_hang → dang_giao → giao_thanh_cong (da_thu_tien) / giao_that_bai → hoan_hang, và đối soát tiền COD đã nhận remittance với tiền lẽ ra phải nhận.
Bài học: COD không đi qua payment gateway nhưng vẫn là một "tích hợp thanh toán" với luồng đối soát phức tạp. BA hay quên nhánh này vì nó "không có API cổng tiền", nhưng đây lại là nơi phát sinh tranh chấp tiền nong nhiều nhất.
Hướng dẫn từng bước
Đây là quy trình một BA nên đi qua khi nhận yêu cầu "tích hợp cổng thanh toán":
Bước 1 — Xác định phương thức và đối tác. Sản phẩm cần hỗ trợ gì: VNPAY, MoMo, ZaloPay, VietQR, thẻ quốc tế, COD? Mỗi cái là một tích hợp riêng. Liệt kê theo độ ưu tiên dựa trên hành vi khách hàng mục tiêu.
Bước 2 — Đọc tài liệu sandbox của đối tác. VNPAY, MoMo, ZaloPay đều có môi trường sandbox và bộ tài liệu kỹ thuật. BA nên đọc phần "luồng giao dịch", "tham số request/response", "danh sách response code", và "đối soát". Bạn không cần hiểu từng dòng code, nhưng phải nắm được các trường dữ liệu và các trạng thái.
Bước 3 — Vẽ sequence diagram cho luồng happy path. Mô tả rõ: client → backend → cổng → ngân hàng/ví → IPN về backend → return về client. Ghi rõ kênh nào cập nhật trạng thái (IPN), kênh nào chỉ hiển thị (return).
Bước 4 — Định nghĩa state machine của đơn hàng. Tối thiểu: khoi_tao → cho_thanh_toan → da_thanh_toan / that_bai / het_han / da_huy / hoan_tien. Mỗi chuyển trạng thái phải có điều kiện (trigger) rõ ràng — ví dụ chỉ chuyển sang da_thanh_toan khi IPN hợp lệ + verify chữ ký + verify số tiền.
Bước 5 — Đặc tả các nhánh lỗi. Khách hủy giữa chừng, OTP sai, hết hạn QR, mạng rớt sau khi trừ tiền, IPN đến trễ, IPN đến hai lần. Mỗi nhánh cần một hành vi sản phẩm cụ thể.
Bước 6 — Đặc tả đối soát (reconciliation). Cuối ngày, hệ thống đối chiếu giao dịch nội bộ với file/API sao kê của cổng. Số tiền lệch xử lý thế nào? Đơn nào "treo" pending cần truy vấn lại? Đây là phần BA hay bỏ sót nhưng kế toán và vận hành rất cần.
Bước 7 — Đặc tả hoàn tiền (refund). Quy trình refund toàn phần / một phần, ai duyệt, gọi API refund của cổng ra sao, trạng thái nào cho phép refund.
Lỗi thường gặp & mẹo
- Dựa vào return URL để xác nhận thanh toán. Lỗi kinh điển và nguy hiểm nhất. Luôn dùng IPN/webhook làm nguồn sự thật.
- Quên verify chữ ký trên callback. Nếu không verify, bất kỳ ai biết cấu trúc IPN đều có thể gửi request giả báo "đã thanh toán" để lấy hàng miễn phí. Spec phải bắt buộc verify signature trước mọi xử lý.
- Quên verify số tiền. Kẻ gian có thể đổi
amounttừ 1.000.000đ xuống 1.000đ. Backend phải so sánh số tiền callback với số tiền đơn gốc lưu trong DB của mình.
- Không xử lý IPN trùng lặp. Cổng sẽ retry. Endpoint phải idempotent, kiểm tra trạng thái đơn trước khi xử lý.
- Hiểu sai đơn vị tiền. VNPAY nhân 100 (đồng × 100). Nhầm đơn vị dẫn đến thu sai gấp 100 lần hoặc 1/100.
- Bỏ qua timeout / hết hạn. QR và phiên thanh toán có thời hạn. Spec phải định nghĩa đơn "chờ thanh toán" hết hạn sau bao lâu, và có cron đóng đơn treo.
- Mẹo về môi trường: Luôn yêu cầu đội kỹ thuật chạy đầy đủ trên sandbox trước, dùng thẻ/ví test do cổng cung cấp. Đừng test bằng tiền thật trên production.
- Mẹo về log và truy vết: Mỗi giao dịch nên log đủ
orderId,txnRef, response code, thời điểm. Khi khách khiếu nại "tôi mất tiền", bạn cần tra cứu được trong vài phút. (Liên hệ với observability ở Bài 49.)
- Mẹo về pháp lý/tuân thủ: Dùng mô hình redirect/hosted page để tránh chạm vào dữ liệu thẻ, giảm yêu cầu PCI DSS. Nếu sản phẩm lưu thông tin thẻ, đó là cả một câu chuyện tuân thủ lớn — hãy cảnh báo sớm cho stakeholder.
Bài tập thực hành
- Vẽ luồng end-to-end: Chọn VNPAY (mô hình redirect). Vẽ sequence diagram đầy đủ từ lúc khách bấm "Thanh toán" đến lúc đơn chuyển sang "đã thanh toán", chỉ rõ đâu là return URL, đâu là IPN, và bước verify chữ ký + verify số tiền nằm ở đâu.
- Thiết kế state machine: Cho một đơn hàng e-commerce có thể trả bằng MoMo hoặc COD. Liệt kê toàn bộ trạng thái và điều kiện chuyển trạng thái, đảm bảo cả hai phương thức cùng dùng được một mô hình (gợi ý: COD có thêm các trạng thái vận chuyển).
- Viết acceptance criteria cho nhánh lỗi: Soạn tiêu chí nghiệm thu (dạng Given/When/Then) cho ba tình huống: (a) IPN đến hai lần cho cùng giao dịch, (b) callback có chữ ký sai, (c) số tiền callback không khớp đơn gốc.
- Thiết kế báo cáo đối soát: Mô tả các cột cần có trong báo cáo đối soát cuối ngày giữa hệ thống của bạn và sao kê VNPAY, và quy tắc xác định một giao dịch "lệch" cần kế toán xử lý.
Tóm tắt
Tích hợp cổng thanh toán ở Việt Nam đòi hỏi BA hiểu cả bức tranh đặc thù nội địa: VNPAY, MoMo, ZaloPay, VietQR và COD — mỗi cái một luồng, một mô hình bảo mật, một cách đối soát. Ba nguyên tắc cốt lõi cần nhớ mãi: (1) IPN/webhook là nguồn sự thật để cập nhật trạng thái, return URL chỉ để hiển thị; (2) luôn verify chữ ký và verify số tiền trước khi tin bất kỳ callback nào; (3) thiết kế endpoint nhận callback idempotent vì cổng sẽ gửi lặp lại. Bên cạnh happy path, giá trị thật sự của một BA giỏi nằm ở việc đặc tả đầy đủ các nhánh lỗi, trạng thái treo, đối soát và hoàn tiền — đó mới là những nơi tiền của khách hàng thực sự được bảo vệ. Với COD, đừng quên rằng tiền đi qua đơn vị vận chuyển chứ không qua cổng, nên luồng đối soát remittance là một phần không thể thiếu của spec.