+17

Ronin Engineer Tích Hợp với VNPay Như Thế Nào?

Hello mọi người, mình là một Ronin Engineer. Hôm nay mình sẽ trình bày website roninhub.com bên mình tích hợp với VNPay như nào thế?
Let’s go!

1. Yêu Cầu

Đầu tiên, chúng ta sẽ bắt đầu bằng yêu cầu của nghiệp vụ.

  • (Automation) Một giải pháp thanh toán tự động: Nó không chỉ tạo ra sự thuận tiện và tăng trải nghiệm của người dùng mà còn giảm chi phí vận hành “bằng cơm".
  • (Security) Tính bảo mật cao: Liên quan tới tiền, tài chính thì yếu tố này đặc biệt quan trọng, nó có thể ảnh hưởng trực tiếp tới business.
  • (Pricing) Phí giao dịch thấp: Khi số lượng giao dịch lớn hoặc tỷ suất lợi nhuận của bạn thấp, nếu phí trên mỗi giao dịch cao thì tổng phí giao dịch là một khoản đáng kể.
  • (Scalability) Hỗ trợ nhiều phương thức thanh toán: Do người dùng của hệ thống có thể là sinh viên, người đi làm hoặc người có thói quen sử dụng thẻ thanh toán quốc tế. Ngoài ra, về sau nghiệp vụ có thể mở rộng hỗ trợ cả thanh toán trả góp. Do đó, việc hỗ trợ nhiều phương thức thanh toán là cần thiết.
  • Ngoài ra, chúng ta có thể đánh giá thêm các yếu tố như độ ổn định, hỗ trợ khách hàng, khuyến mãi, giao diện, độ khó tích hợp, quản lý và các tính năng khác, …

Như vậy, chúng ta sẽ cần tích hợp một cổng thanh toán để giải quyết yêu cầu trên của nghiệp vụ. Bên cạnh đó, cổng thanh toán cần đáp ứng được ít nhất là 4 yếu tố chính trên. qr-payment.png

2. Tại Sao là VNPay QR?

Đầu tiên, tại sao lại là VNPay QR? Thứ nhất, bên mình không quảng cáo cho VNPay. Và bên mình cũng chưa trải nghiệm hết tất cả các bên cung cấp giải pháp thanh toán nên việc so sánh giữa các bên là không khách quan. Mỗi bên đều có ưu nhược điểm, việc lựa chọn sẽ phụ thuộc vào yêu cầu cụ thể của bạn.

Dưới đây mình đưa một số thông tin để bạn tham khảo nhé.

2.1. Bảo Mật

Quan điểm của mình là nên chọn những nhà cung cấp lớn và uy tín thì khả năng có tính bảo mật cao. Tuy chưa có một thống kê cụ thể về thị phần nhưng theo mình có thể kể đến các ông lớn sau:

  • Napas
  • Momo
  • VNPay
  • Viettel Paygate

Mình đoán những năm gần đây VNPay đang dần vươn lên trong thị trường ví điện tử, giải pháp thanh toán. Chúng ta dễ dàng nhìn thấy nhiều chiến dịch marketing rầm rộ từ trên TV, biển quảng cáo trên những trục đường to, cho đến những quán cơm nhỏ có những chiếc loa nhỏ nhỏ xinh xinh thông báo biến động số dư. Kéo theo mảng về cổng thanh toán VNPay QR cũng phát triển.

Xét về thông số bảo mật, hầu hết các bên cung cấp đều tuân thủ tiêu chuẩn bảo mật PCI DSS. Nhưng ta cần đánh giá thêm về version và level của PCI DSS. Nôm na, version cao, level thấp là tốt. Ví dụ version 4.0, level 1 là “xịn".

pci.png

Theo thông tin trên, VNPay QR có độ bảo mật đứng thứ 2 và Momo có độ bảo mật cao nhất.

2.2. Phí

Dưới đây là bảng giá của một số cổng thanh toán với phương thức thanh toán là QR mobile banking (nội địa). pricing.png

Chúng ta có thể thấy VNPay QR có phí giao dịch không phải rẻ so với các bên khác.

2.3. Yếu Tố Khác

Bên cạnh đó, VNPay QR có:

  • Tốc độ xử lý giao dịch nhanh và ổn định.
  • Sale và hỗ trợ kỹ thuật đều nhiệt tình hỗ trợ.
  • Giao diện không quá đẹp nhưng thân thiện.
  • Liên kết với nhiều ngân hàng.
  • Thỉnh thoảng có khuyến mãi hấp dẫn.
  • Tính năng quản lý đầy đủ.
  • Hỗ trợ phương thức trả góp, một tính năng hay và business sẽ sử dụng trong tương lai.

Tuy có nhiều điểm mạnh nhưng VNPay QR cũng có những điểm cần cải thiện, mình sẽ đề cập ở các phần sau.

3. Thiết Kế

Về luồng tích hợp, mọi người có thể xem thêm tại trang docs của VNPay QR giúp mình nhé. https://sandbox.vnpayment.vn/apis/docs/thanh-toan-pay/pay.html

Trong docs đang chỉ vẽ và mô tả luồng thanh toán bằng thẻ. Tuy nhiên phương thức thanh toán mà khách hàng hay sử dụng lại là QR mobile banking nên mình sẽ vẽ thêm cho luồng phương thức thanh toán QR theo kiểu khác. Ngoài ra, mình sẽ vẽ thêm luồng tiền để mọi người dễ hiểu và có bức tranh toàn cảnh hơn.

3.1. Luồng Thanh Toán QR

payment-flow.png

Chú thích:

  • Ronin FE: frontend của RoninHub
  • Ronin BE: backend của RoninHub
  • PGW FE: frontend của cổng thanh toán VNPay QR
  • PGW BE: backend của cổng thanh toán VNPay QR
  • Bank: ngân hàng mà khách hàng sử dụng, cũng là ngân hàng mà cổng thanh toán có kết nối.
  • Luồng trên là happy case.

Luồng:

  1. User thao tác tạo yêu cầu thanh toán trên Ronin FE.
  2. Ronin FE gửi yêu cầu thanh toán lên Ronin BE.
  3. Ronin BE xử lý yêu cầu thanh toán, tạo transaction lưu xuống DB
  4. Ronin BE tạo ra 1 URL thanh toán có dạng sau và gửi về cho Ronin FE. Trong URL thanh toán chứa thông tin thanh toán, Return URL (được sử dụng ở bước 9.2) và các thông tin khác.
https://sandbox.vnpayment.vn/paymentv2/vpcpay.html?vnp_Amount=1806000&vnp_Command=pay&vnp_CreateDate=20210801153333&vnp_CurrCode=VND&vnp_IpAddr=127.0.0.1&vnp_Locale=vn&vnp_OrderInfo=Thanh+toan+don+hang+%3A5&vnp_OrderType=other&vnp_ReturnUrl=https%3A%2F%2Fdomainmerchant.vn%2FReturnUrl&vnp_TmnCode=DEMOV210&vnp_TxnRef=5&vnp_Version=2.1.0&vnp_SecureHash=3e0d61a0c0534b2e36680b3f7277743e8784cc4e1d68fa7d276e79c23be7d6318d338b477910a27992f5057bb1582bd44bd82ae8009ffaf6d141219218625c42
  1. Ronin FE (browser) sẽ điều hướng sang PGW FE theo URL thanh toán trên.
  2. Client chọn phương thức thanh toán QR. Mã QR sẽ hiện trên web của cổng. Client sử dụng app ngân hàng quét QR và thực hiện xác thực để thanh toán.
  3. Ứng dụng ngân hàng của Client sẽ gửi lệnh chuyển khoản lên Bank.
  4. Khi tài khoản của VNPay tại Bank có biến động số dư, Bank sẽ gửi thông báo giao dịch về cho VNPay.
  5. Sau khi VNPay xử lý giao dịch thành công, hệ thống sẽ thực hiện đồng thời 2 bước (9.1 và 9.2).
    9.1.1. VNPay gửi thông báo kết quả giao dịch về cho Ronin BE bằng cách gọi API IPN (Instant Payment Notification) của Ronin BE.
    9.1.2. Ronin BE thực hiện cập nhận trạng thái giao dịch của hệ thống.
    9.2.1. VNPay BE gửi yêu cầu điều hướng tới VNPay FE.
    9.2.2. VNPay FE sẽ được điều hướng về lại Ronin FE theo Return URL được tạo ở bước 4.
    9.2.3. Ronin FE sẽ định kỳ kiểm tra trạng thái của giao dịch bằng cách gọi API của Ronin BE. Vì bước 9.1 và 9.2 được thực hiện đồng thời nên có thể 9.2 được thực hiện trước khi giao dịch được cập nhật trạng thái ở bước 9.1.

3.2. Luồng Tiền

Cổng thanh toán VNPay QR liên kết với nhiều ngân hàng, lúc đó ở mỗi ngân hàng sẽ tồn tại (ít nhất) 2 tài khoản của VNPay QR.

  • 1 tài khoản PGW Merchant Account dùng để tiếp nhận lệnh thanh toán từ phía tài khoản khách hàng Client Account.
  • 1 tài khoản PGW Settlement Account dùng để quyết toán (settlment) cho tài khoản Merchant Account (tài khoản ngân hàng của Ronin Engineer)
  • Đối với ngân hàng, VNPay là 1 merchant kết nối với ngân hàng. Còn đối với VNPay, Ronin Engineer là 1 merchant kết nối với cổng thanh toán.

Như vậy, mọi người có thể hình dung luồng tiền sẽ đi như sau: fund-flow.png

  1. Client thao tác thanh toán QR trên ứng dụng ngân hàng.
  2. Ví dụ ngân hàng khách hàng sử dụng là Techcombank. Sau khi xác thực thành công, tài khoản khách hàng (Client Account) sẽ chuyển khoản tới PGW Merchant Account tại Techcombank.
  3. VNPay có kết nối với Techcombank nên khi tài khoản PGW Merchant Account có biến động số dư thì ngân hàng sẽ gửi thông báo, thông tin giao dịch về cho VNPay để VNPay xử lý.
  4. VNPay xử lý xong sẽ gửi thông báo về Ronin BE tại IPN API.
  5. Sau đó, VNPay gom các thanh toán vào ngày T và thực hiện quyết toán cho Merchant vào ngày T + 1.
  6. Ví dụ merchant Ronin sử dụng ngân hàng BIDV, VNPay sẽ gửi lệnh quyết toán tới tài khoản PGW Settlement Account tại BIDV và chuyển khoản cho Merchant Account (toàn khoản của Ronin).
  7. Định kỳ VNPay thực hiện đối soát với ngân hàng. Kiểm tra, so sánh giao dịch giữa cổng thanh toán và ngân hàng để đảm bảo tất cả giao dịch đã được ghi chép chính xác và đầy đủ.

Lưu ý:

  • Bước 2 và bước 6 là phản ánh luồng tiền.
  • Từ bước 1 đến 4 sẽ được thực hiện tại thời điểm giao dịch và trong ngày T.
  • Bước 5 và 6 thực hiện tại ngày T + 1.
  • Cùng trong 1 thanh toán, nhưng 3 hệ thống khác nhau (bank, payment gateway, merchant), mỗi hệ thống sẽ ghi nhận 1 mã giao dịch khác nhau.

4. Code Mẫu

Đối với luồng thanh toán có 2 bước quan trọng đó là bước 4 (tạo giao dịch và URL thanh toán) và bước 9.1 (API IPN nhận thông báo kết quả giao dịch). Mình sẽ cùng mọi người review code mẫu nodejs trên trang docs của VNPay nha.

Trước khi review, mọi người vui lòng đọc trước tài liệu kỹ thuật của VNPay để nắm được ý nghĩa của các tham số nhé.
https://sandbox.vnpayment.vn/apis/docs/thanh-toan-pay/pay.html

4.1. API Tạo URL Thanh Toán

router.post('/create_payment_url', function (req, res, next) {
  
   process.env.TZ = 'Asia/Ho_Chi_Minh';
  
   let date = new Date();
   let createDate = moment(date).format('YYYYMMDDHHmmss');
  
   let ipAddr = req.headers['x-forwarded-for'] ||
       req.connection.remoteAddress ||
       req.socket.remoteAddress ||
       req.connection.socket.remoteAddress;


   let config = require('config');
  
   let tmnCode = config.get('vnp_TmnCode');
   let secretKey = config.get('vnp_HashSecret');
   let vnpUrl = config.get('vnp_Url');
   let returnUrl = config.get('vnp_ReturnUrl');
   let orderId = moment(date).format('DDHHmmss');
   let amount = req.body.amount;
   let bankCode = req.body.bankCode;
  
   let locale = req.body.language;
   if(locale === null || locale === ''){
       locale = 'vn';
   }
   let currCode = 'VND';
   let vnp_Params = {};
   vnp_Params['vnp_Version'] = '2.1.0';
   vnp_Params['vnp_Command'] = 'pay';
   vnp_Params['vnp_TmnCode'] = tmnCode;
   vnp_Params['vnp_Locale'] = locale;
   vnp_Params['vnp_CurrCode'] = currCode;
   vnp_Params['vnp_TxnRef'] = orderId;
   vnp_Params['vnp_OrderInfo'] = 'Thanh toan cho ma GD:' + orderId;
   vnp_Params['vnp_OrderType'] = 'other';
   vnp_Params['vnp_Amount'] = amount * 100;
   vnp_Params['vnp_ReturnUrl'] = returnUrl;
   vnp_Params['vnp_IpAddr'] = ipAddr;
   vnp_Params['vnp_CreateDate'] = createDate;
   if(bankCode !== null && bankCode !== ''){
       vnp_Params['vnp_BankCode'] = bankCode;
   }


   vnp_Params = sortObject(vnp_Params);


   let querystring = require('qs');
   let signData = querystring.stringify(vnp_Params, { encode: false });
   let crypto = require("crypto");    
   let hmac = crypto.createHmac("sha512", secretKey);
   let signed = hmac.update(new Buffer(signData, 'utf-8')).digest("hex");
   vnp_Params['vnp_SecureHash'] = signed;
   vnpUrl += '?' + querystring.stringify(vnp_Params, { encode: false });


   res.redirect(vnpUrl)
});
  • Đầu tiên, chúng ta nên làm theo những hướng dẫn của VNPay. Cung cấp đầy đủ các tham số và đúng định dạng. Ví dụ một số tham số mọi người cần lưu ý sau.
  • vnp_Amount số tiền thanh toán, không có dấu thập phân và phải nhân thêm 100 trước khi gửi sang VNPay.
  • Những tham số dạng date có timezone là +7 và format là yyyyMMddHHmmss. Mình chưa hiểu lý do vì sao VNPay không để format ISO 🤔
  • vnp_IpAddr là string. Nếu lấy IP address theo code mẫu thì giá trị có thể là nhiều địa chỉ IP nối nhau như này 13.160.92.202, 12.161.56.21. Như vậy, không đúng định dạng trong docs. Mọi người nên chỉ lấy 1 địa chỉ IP, có thể là IP đầu tiên hoặc IP cố định của server. Để tránh lỗi có thể xảy ra ở đầu VNPay.
  • vnp_ExpireDate mọi người nên đặt thời gian hết hạn. Tham số này trong code mẫu khai báo. Mọi người không nên đặt thời gian quá dài, không nên quá 30p, mình đang để 15p.
  • vnp_Version nên sử dụng version mới. Lý do tại sao mình sẽ đề cập tại ở phần bảo mật bên dưới.
  • vnp_TxnRef là mã giao dịch tại Ronin BE. Mọi người lưu ý đối với trường hợp mua hàng trên web, lúc đó entity Order sẽ được sinh ra trước, entity Transaction sẽ được sinh ra sau. Ngoài ra, có trường hợp không order nhưng vẫn có transaction. Ví dụ như hoàn tiền (refund). Vậy nên, mọi người lưu ý cần tách biệt 2 entity này ra và vnp_TxnRef nên để là transaction_id.
  • Để code clean hơn, mọi người có thể sắp xếp thứ tự tầm quan trọng của các tham số theo chiều giảm dần và nhóm các tham số có quan hệ với nhau thành một nhóm. Ví dụ như sau:
// thông tin về cách thức thanh toán
vnp_Version
vnp_Command
vnp_TmnCode
vnp_BankCode

// thông tin về payment
vnp_Amount
vnp_TxnRef
vnp_OrderType
vnp_OrderInfo

// control flow
vnp_ReturnUrl

// session
vnp_CreateDate
vnp_ExpireDate

// meta
vnp_IpAddr
vnp_CurrCode
vnp_Locale

// security
vnp_SecureHash

4.2. API IPN

router.get('/vnpay_ipn', function (req, res, next) {
   let vnp_Params = req.query;
   let secureHash = vnp_Params['vnp_SecureHash'];
  
   let orderId = vnp_Params['vnp_TxnRef'];
   let rspCode = vnp_Params['vnp_ResponseCode'];


   delete vnp_Params['vnp_SecureHash'];
   delete vnp_Params['vnp_SecureHashType'];


   vnp_Params = sortObject(vnp_Params);
   let config = require('config');
   let secretKey = config.get('vnp_HashSecret');
   let querystring = require('qs');
   let signData = querystring.stringify(vnp_Params, { encode: false });
   let crypto = require("crypto");    
   let hmac = crypto.createHmac("sha512", secretKey);
   let signed = hmac.update(new Buffer(signData, 'utf-8')).digest("hex");    
  
   let paymentStatus = '0'; // Giả sử '0' là trạng thái khởi tạo giao dịch, chưa có IPN. Trạng thái này được lưu khi yêu cầu thanh toán chuyển hướng sang Cổng thanh toán VNPAY tại đầu khởi tạo đơn hàng.
   //let paymentStatus = '1'; // Giả sử '1' là trạng thái thành công bạn cập nhật sau IPN được gọi và trả kết quả về nó
   //let paymentStatus = '2'; // Giả sử '2' là trạng thái thất bại bạn cập nhật sau IPN được gọi và trả kết quả về nó
  
   let checkOrderId = true; // Mã đơn hàng "giá trị của vnp_TxnRef" VNPAY phản hồi tồn tại trong CSDL của bạn
   let checkAmount = true; // Kiểm tra số tiền "giá trị của vnp_Amout/100" trùng khớp với số tiền của đơn hàng trong CSDL của bạn
   if(secureHash === signed){ //kiểm tra checksum
       if(checkOrderId){
           if(checkAmount){
               if(paymentStatus=="0"){ //kiểm tra tình trạng giao dịch trước khi cập nhật tình trạng thanh toán
                   if(rspCode=="00"){
                       //thanh cong
                       //paymentStatus = '1'
                       // Ở đây cập nhật trạng thái giao dịch thanh toán thành công vào CSDL của bạn
                       res.status(200).json({RspCode: '00', Message: 'Success'})
                   }
                   else {
                       //that bai
                       //paymentStatus = '2'
                       // Ở đây cập nhật trạng thái giao dịch thanh toán thất bại vào CSDL của bạn
                       res.status(200).json({RspCode: '00', Message: 'Success'})
                   }
               }
               else{
                   res.status(200).json({RspCode: '02', Message: 'This order has been updated to the payment status'})
               }
           }
           else{
               res.status(200).json({RspCode: '04', Message: 'Amount invalid'})
           }
       }      
       else {
           res.status(200).json({RspCode: '01', Message: 'Order not found'})
       }
   }
   else {
       res.status(200).json({RspCode: '97', Message: 'Checksum failed'})
   }
});
  • Nhiệm vụ của API IPN này là nhận thông báo kết quả giao dịch và cập nhật lại trạng thái của transaction, order.
  • Đầu tiên, chúng ta cần validate những tham số sau:
    • secureHash mình sẽ nói sâu hơn ở phần sau.
    • Kiểm tra order có tồn tại hay không
    • Kiểm tra số tiền có bằng số tiền của order gốc không
    • Kiểm tra trạng thái giao dịch và xử lý giao dịch theo từng trạng thái.
  • Trong trường hợp, bạn để vnp_TxnRef là transaction id thì bạn cần kiểm tra cả transaction và order tương ứng với transaction. Sau đó, cập nhật trạng của cả transaction và order tương ứng.
  • Trong code mẫu, sử dụng nested condition rất nhiều, như vậy code sẽ khó đọc. Trường hợp này, mình khuyến kích mọi người sử dụng Early Return pattern để code tường minh hơn. 🧼 https://gomakethings.com/the-early-return-pattern-in-javascript/
  • Những biến như secretKey, querystring, crypto chúng ta có thể khởi tạo lúc start app, trước khi chạy function này.
  • Mình không chuyên về javascript, nếu mọi người có thêm nhận xét thì comment ở dưới giúp mình nhé. 🙏

5. Bảo Mật

5.1. Tính đúng đắn dữ liệu và Xác thực

Trong luồng payment này, chúng ta cần giải quyết được 2 vấn đề bảo mật sau:

  • Data integrity: Làm sao đảm bảo tính đúng đắn của dữ liệu? Do payment request được tạo ra ở BE và được gửi về cho FE để client thực hiện thanh toán. Tuy nhiên, phía client là phía dễ bị can thiệp nên chúng ta cần đảm bảo thông tin của payment request không bị sửa đổi trong quá trình vận chuyển từ BE → FE → VNPay.
  • Authentity: Làm sao để xác thực rằng đầu tạo payment request là Ronin merchant và đầu gửi IPN là VNPay?
  • Liệu có thể sử dụng 1 cơ chế để giải quyết được đồng thời 2 bài toán trên?

Đầu tiên, HTTPS không đáng tin cậy 100% do một số lỗ hổng của giao thức SSL/TLS và các version cũ. Ngoài ra, HTTPS chỉ mã hoá (che giấu) dữ liệu khi truyền tải (BE → FE), trong trường hợp này ta cần đảm bảo dữ liệu không bị sửa đổi ở đầu FE. Do đó, ta cần kết hợp thêm một cơ chế nữa để tăng cường bảo mật.

Một cơ chế mà giải quyết được đồng thời 2 bài toán trên thì chúng ta có thể nghĩ tới Message Authentication Code (key-based hash function) và chữ ký số (digital signature). Mỗi cơ chế đều có ưu nhược điểm. Thông thường, digital signature sử dùng asymmetric key, còn key-based hash sử dụng symmetric key. Key-based hash có hiệu suất tốt hơn nhưng độ bảo mật thường kém hơn so với digital signature. Trong trường hợp này, chúng ta cần đáng giá thêm yếu tố dễ dàng tích hợp. So với digital signature, key-based hash có ưu thế hơn ở điểm này. Do đó, VNPay sử dụng key-based hash, cụ thể là HMACSHA512.

Đầu merchant và đầu cổng thanh toán cần thống nhất với nhau một hash secret trước khi tích hợp. VNPay sẽ generate ra hash secret rồi gửi email cho merchant. Lúc này, mỗi khi tạo payment request, đầu merchant (Ronin BE) cần kết hợp thông tin thanh toán với secret, rồi hash (HMACSHA512) kết quả kết hợp. Cuối cùng, được giá trị vnp_SecureHash gửi về cho Ronin FE. Secure Hash đảm bảo nội dung của payment request không bị can thiệp, sửa đổi ở đầu FE. Khi gửi payment request lên VNPay, hệ thống sẽ sử dụng hash secret để xác thực chỉ có merchant Ronin mới có thể tạo ra payment request này.

5.2. Cập Nhật Trạng Thái Thanh Toán

Lưu ý, bước ReturnURL được thực hiện ở phía client mà phía client là phía dễ bị can thiệp hơn. Do đó, bước cập nhật trạng thái transaction/order phải được thực hiện ở bước IPN (bước 9.1), không được thực hiện ở bước ReturnURL (bước 9.2).

5.3. Whitelist

Để tăng cường bảo mật, chúng ta nên whitelist, chỉ cho phép một số địa chỉ IP có thể gọi vào API IPN. Đội kỹ thuật bên VNPay sẽ cung cấp danh sách địa chỉ IP này cho bạn.

5.4. Version

Best practice: nên sử dụng version mới nhất.
VNPay khuyến cáo sử dụng API version mới. Các thuật toán mã hóa phiên bản cũ đã kém an toàn và bảo mật. Merchant cần chuyển đổi sang phiên bản version 2.1.0 và thuật toán mã hóa HMACSHA512.
Nếu bạn có sử dụng các thư viện ngoài liên quan tới luồng payment thì nên kiểm tra lỗ hổng và update version của thư viện. Tránh sử dụng các method đã bị đánh deprecated.

Và sau khi update version thì nên test kỹ, ít nhất đảm bảo tất cả function đã chạy đúng.

6. Xử Lý Lỗi

Việc xử lý lỗi ở tất cả các bước đều quan trọng, nó giúp dễ debug và hạn chế được ảnh hưởng khi xảy ra lỗi. Và xử lý lỗi ở bước IPN đặc biệt quan trọng. Do đầu VNPay gọi vào API IPN, nếu ở đầu merchant xử lý lỗi hiệu quả thì sẽ hạn chế ảnh hưởng tới đầu VNPay.

6.1. Response Code

Xử lý lỗi thế nào là hiệu quả? Ít nhất chúng ta cần phải có try catch trong quá trình IPN. Ngoài ra, cần trả đúng mã lỗi theo hướng dẫn của VNPay. Ví dụ:

  • Xử lý giao dịch thành công thì trả về RspCode: ‘00’
  • Nếu không tìm được order thì trả về RspCode: ‘01’
  • ...

Mọi người lưu ý:

  • vnp_ResponseCodeRspCode là 2 mã khác nhau.
  • vnp_ResponseCode (request: VNPay → Merchant): mô tả chuyện gì đã xảy ra bên VNPay.
  • RspCode (response: Merchant → VNPay): mô tả chuyện gì đã xảy ra trong quá trình xử lý IPN.

Tuy nhiên, tài liệu của VNPay đoạn này không rõ ràng. Và bên mình phải làm theo test cases trong môi trường test sandbox. Hy vọng đội kỹ thuật VNPay cải thiện lại tài liệu.

6.2. Retry

Tại sao cần trả đúng mã lỗi theo hướng dẫn của VNPay? Như vậy thì đầu VNPay sẽ biết xử lý lỗi gọi IPN như nào. Ví dụ:

  • Xử lý giao dịch thành công (RspCode: ‘00’), VNPay sẽ kết thúc luồng thanh toán đó.
  • Mã secure hash không hợp lệ (RspCode: ‘97’), VNPay sẽ thực hiện retry, gọi lại IPN.

Trong trường hợp RspCode: 01, 04, 97, 99 hoặc IPN timeout là những lỗi có thể retry thì VNPAY sẽ bật cơ chế retry IPN. Thực hiện retry tối đa 10, trong vòng 5 phút.

Như vậy, việc giao tiếp giữa Merchant và VNPay sẽ đáng tin cậy hơn.

6.3. Idempotent Handler

Tuy nhiên, việc retry có thể dẫn đến vấn đề lặp (duplicate) message. Để giải pháp cho vấn đề này phụ thuộc vào logic của IPN handler. Chúng ta cần kiểm tra trạng thái giao dịch trước khi thực hiện câp nhật trạng thái. Ví dụ, nếu giao dịch ở trạng thái PROCESSING thì cho phép thực thi tiếp, còn ở trạng thái SUCCESSFUL, FAILED thì sẽ reject request.

7. Tracing

Trong giai đoạn đầu tích hợp, tỉ lệ xảy ra lỗi ở điểm tích hợp là khá cao. Do đó, chúng ta cần thu thập được nhiều thông tin để debug. Cụ thể, chúng ta nên đặt log ở đầu vào, đầu ra từng bước trong luồng thanh toán, cả case thành công, lẫn case thất bại, exception.

Mọi người lưu ý, cần đảm bảo có log ở bước 9.2.2 (Return URL). Bước này PGW FE sẽ redirect về Ronin FE để kiểm tra trạng thái giao dịch và hoàn tất luồng thanh toán.

172.71.218.67 - - [10/Apr/2024:11:35:52 +0700] "GET /orders/35/status?vnp_Amount=300000&vnp_BankCode=TECHCOMBANK&vnp_BankTranNo=342736554&vnp_CardType=QRCODE&vnp_OrderInfo=Thanh+toan+don+hang+35&vnp_PayDate=20240410113552&vnp_ResponseCode=00&vnp_TmnCode=RONINHUB&vnp_TransactionNo=127554884&vnp_TransactionStatus=00&vnp_TxnRef=35&vnp_SecureHash=e2cf04610bef254575c383e68e154cce48a969e89567d12ca6bffd06aadb9006b8d505001f1de379fe8ab331aadf6b3cf6a6ed4d286bab5591daed209b3485bc HTTP/1.1" 200 5110 "https://pay.vnpay.vn/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"

Tại sao lại cần có log ở bước này? bên mình đã gặp trường hợp client chuyển khoản thành công, Ronin BE nhận được IPN và xử lý thành công nhưng màn hình thanh toán QR (PWG FE) vẫn đứng hình, không redirect về Ronin FE. Mình có báo đội kỹ thuật bên VNPay check nhưng họ check không ra và bảo lỗi bên mình. Mình phải show log ở đầu nginx (bước 9.2.2) là để chứng minh lỗi bên VNPay, PWG FE chưa redirect sang Ronin FE. Sau đó, 2 bên cũng chưa tìm ra nguyên nhân gốc. Tuy nhiên, mình chỉ gặp 2 case như này, còn lại đều ổn định.

Để dễ dàng cho việc search log, biết được 1 payment request diễn ra như nào thì ở tất cả log cần chứa thông tin của correlation id (có thể là transaction_id, order_id).

8. Kiểm Thử

VNPay cung cấp môi trường test sandbox và hoạt động ổn định. Đội kỹ thuật sẽ gửi cho bạn đường dẫn tới trang cấu hình môi trường sandbox để bạn chủ động test.

sandbox.png

Trang này cho phép mình cấu hình IPN URL. Trong trường hợp, bạn chưa có môi trường deploy (chưa có một public IPN URL) và muốn test ở local. Bạn có thể tham khảo giải pháp tạo static domain đơn giản, miễn phí của ngrok. https://ngrok.com/blog-post/free-static-domains-ngrok-users

ngrok http 8888 --domain=fox-funny-noticeably.ngrok-free.app

Sau khi test chức năng, tất cả test case thành công. Nếu bạn có yêu cầu tối thiểu về performance thì bạn có thể thực hiện theo bước load test. Tuy nhiên, do phụ thuộc vào môi trường sandbox của VNPay nên kết quả có thể không chính xác, không giống với môi trường production. Nhưng bạn nên thử load test qua để đảm bảo BE vẫn hoạt động tốt với lượng load lớn hơn.

9. Bước Tiếp Theo

Đối với phần thanh toán, team Ronin sẽ tiếp tục:

  • Phát triển thêm các loại giao dịch khác.
  • Hoàn thiện tài liệu.
  • Truy vấn những giao dịch bị treo.
  • Hoàn tiền (refund) tự động.
  • Tích hợp phương thức trả góp.

10. Tổng Kết

Khi tích hợp với cổng thanh toán, chúng ta cần:

  • Đánh giá nhiều yếu tố khi chọn giải pháp thanh toán như độ bảo mật, phí, độ ổn định, tính năng, tốc độ tích hợp, … VNPay có nhiều điểm đã làm tốt tuy nhiên phí giao dịch hơi cao và tài liệu chưa thực sự thân thiện.
  • Nắm được cả về nghiệp vụ để hiểu rõ hơn về luồng thanh toán được xử lý như nào.
  • Bảo mật là yếu tố quan trọng khi tích hợp và đặc biệt quan trọng đối với các hệ thống tài chính.
  • Xử lý lỗi là việc làm cần thiết giúp cho hệ thống tin cậy (reliable) và khoẻ mạnh (robust) hơn.

Cám ơn mọi người đã đọc đến hết bài.
Nếu mọi người thấy bài viết hữu ích thì nhờ mọi người share để nội dung của Ronin được nhiều người biết hơn nữa.
Cám ơn mọi người, chúc mọi người nghỉ lễ vui vẻ.


🧑‍💻 90+ Ronin Engineers: https://roninhub.com/
📚️ System Design VN: https://fb.com/groups/systemdesign.vn


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí