Thực hành lập trình xử lý sự kiện webhook để xác nhận thanh toán
Giới thiệu
Hiện tại Casso đã hỗ trợ nhiều hình thức tích hợp xác nhận thanh toán thông qua các APICasso đã public. Để phần tích hợp thanh toán của bạn xịn hơn thì có thể dùng VietQR để tạo QR-Code cho phần thanh toán. VietQR là tiêu chuẩn quốc gia về mã QR ngân hàng. Mã này được chấp nhận bởi 50 ngân hàng Việt Nam. Có thể xem chi tiết tại đây
Hướng dẫn tích hợp
Để có thể sử dụng và hiểu được các API này thì dưới đây Casso demo một server basic được viết bằng NodeJS + Express basic về tích hợp thanh toán. Chi tiết source tại Github.
Dưới đây là demo các bước về việc tạo webhook để lắng nghe có các giao dịch mới của Casso gửi qua và yêu cầu đồng bộ giao dịch tức thì từ phía app. Quá trình code có thể chỉ mất vài giờ nếu bạn đã quen với việc viết API. Bạn có thể làm theo kịch bản sau:
Cấu trúc file sever
Bước 1: Tạo file index.js
Đầu tiên chúng ta sẽ tạo file index.js để xây dựng server lắng nghe các request. Server mình sẽ thiết lập với cổng 4300
require('dotenv').config({ path:'.env' });let app =require('./app');asyncfunctionmain() { appconsole.log(`Server on port ${process.env.PORT||4300}`);};main();
let express =require("express");var cors =require('cors');let app =express();// Tạo corsvar corsOption = { origin:true, methods:'GET,HEAD,PUT,PATCH,POST,DELETE', credentials:true, exposedHeaders: ['x-auth-token'] };app.use(cors(corsOption));app.use(express.json());app.use(express.urlencoded({ extended:true }));app.use('/',require('./routes'));// Endpoint not foundapp.use(function (req, res, next) {res.status(404).json({ code:404, message:'Endpoint not found' });})// Xử lí khi lỗi ở phía serverapp.use(function (err, req, res, next) {res.status(500).json({ code:500, error:'Something went wrong, please try again!' })})app.listen(process.env.PORT||4300);module.exports= app;
Bước 3: Tạo các routes và test hello world
Ở đây mình sẽ tạo 3 route chính:
/webhook/handler-bank-transfer Webhook để nhận thông tin giao dịch từ Casso
/register-webhook Thực hiện đăng kí webhook và lấy token từ Casso
/users-paid Thực hiện tính năng đồng bộ giao dịch tức thì qua Casso
var express =require('express');var router =express.Router();//Router này sẽ là webhook nhận thông tin giao dịch từ casso gọi qua được bảo mật bằng secure_token trong headerrouter.route('/webhook/handler-bank-transfer').post(async (req, res, next) => {res.status(200).json({message:"Hello world"}) })// Router này sẽ thực hiện tính năng đồng bộ giao dịch tức thì.// Ví dụ: Khi người dùng chuyển khoản cho bạn và họ ấn nút tôi đã thanh toán thì nên xử lí gọi qua casso đề đồng bộ giao dịch vừa chuyển khoản
router.route('/users-paid').post(async (req, res, next) => {res.status(200).json({message:"Hello world"}) })// Route này sẽ thực hiện đăng kí webhook dựa vào API KEY và lấy thông tin về business và banksrouter.route('/register-webhook').post(async (req, res, next) => {res.status(200).json({message:"Hello world"}) })module.exports= router;
Kiểm tra với postman
Bước 4: Xây dựng các hàm hỗ trợ
Để có thể giao tiếp với server Casso sẽ dùng 1 HTTP Client để gọi qua. Ở Demo này sẽ sử dụng Axios và Query-string.
Các hàm thêm, xóa, sửa và xóa webhook Mô tả chi tiết tại đây
/*webhook.util.js*/ create:async (data, accessToken) => {api.defaults.headers.Authorization = accessToken;let res =awaitapi.post('/webhooks', data);return res; }, getDetailWebhookById:async (webhookId, accessToken) => {api.defaults.headers.Authorization = accessToken;let res =awaitapi.get(`/webhooks/${webhookId}`);return res; }, updateWebhookById:async (webhookId, accessToken, data) => {api.defaults.headers.Authorization = accessToken;let res =awaitapi.put(`/webhooks/${webhookId}`, data);return res; }, deleteWebhookById:async (webhookId, accessToken) => {api.defaults.headers.Authorization = accessToken;let res =awaitapi.delete(`/webhooks/${webhookId}`);return res; }, deleteWebhookByUrl:async (urlWebhook, accessToken) => { // Thêm url vào query để delete https://oauth.casso.vn/v1/webhooks?webhook=https://website-cua-ban.com/api/webhook
let query = { params: { webhook: urlWebhook } };api.defaults.headers.Authorization = accessToken;let res =awaitapi.delete(`/webhooks`, query);return res; },
Parser orderId từ nội dung giao dịch và tiền tố giao dịch (DH1231=> 1231) và đồng thời cũng kiểm tra có phân biệt chữ hoa với thường trong nội dung giao dịch hay không?
/*webhook.util.js*/ parseOrderId: (caseInsensitive, transactionPrefix, description) => {// Ở đây mình ở sử dụng regex để parse nội dung chuyển khoản có chứa orderId// CASSO101 => orderId = 101let re =newRegExp(transactionPrefix);if (!caseInsensitive) re =newRegExp(transactionPrefix,'i');let matchPrefix =description.match(re);// Không tồn tại tiền tố giao dịchif (!matchPrefix) returnnull;let orderId =parseInt(description.substring(transactionPrefix.length,description.length));return orderId; }
Bước 5: Xây dựng các Route
Mình cần định nghĩa một vài biến cần trong quá trình dựng
//routes/index.js//Tiền tố giao dịchconsttransaction_prefix='CASSO';// Phân biệt chữ hoa/thường trong tiền tố giao dịchconstcase_insensitive=false;//Hạn của đơn hàng là 3 ngày. Quá 3 ngày thì không xử lýconstexpiration_date=3;// API KEY lấy từ cassoconstapi_key='45e40320-e0b7-11eb-a12c-35cc867f21a0';// secure_token đăng kí khi tạo webhookconstsecure_token='R5G4cbnN7uSAwfTd'
Route tạo webhook bằng API_KEY và lấy thông tin user bao gồm Business và banks
//routes/index.jsrouter.route('/register-webhook').post(async (req, res, next) => {try {// Token có hạn 6h nên bạn có thể lưu lại khi nào hết thì gọi hàm lấy token lạilet resToken =awaitgetTokenUtil.getTokenByAPIKey(api_key);let accessToken =resToken.access_token;//Delete Toàn bộ webhook đã đăng kí trước đó với https://ten-mien-cua-ban.com/webhook/handler-bank-transfer await webhookUtil.deleteWebhookByUrl('https://ten-mien-cua-ban.com/webhook/handler-bank-transfer', accessToken);
//Tiến hành tạo webhooklet data = { webhook:'https://ten-mien-cua-ban.com/webhook/handler-bank-transfer', secure_token: secure_token, income_only:true }let newWebhook =awaitwebhookUtil.create(data, accessToken);// Lấy thông tin về userInfolet userInfo =awaituserUtil.getDetailUser(accessToken);returnres.status(200).json({ code:200, message:'success', data: { webhook:newWebhook.data, userInfo:userInfo.data } }) } catch (error) {next(error) } })
2. Route này sẽ thực hiện tính năng đồng bộ giao dịch qua Casso.
Ví dụ: Khi người dùng chuyển khoản cho bạn và họ ấn nút tôi đã thanh toán thì nên xử lí gọi qua Casso để đồng bộ giao dịch vừa được chuyển khoản. Có thể sử dụng cho tính năng Tôi đã thanh toán để xác nhận thanh toán ngay.
//routes/index.jsrouter.route('/users-paid').post(async (req, res, next) => {try {// Để thực hiện tính năng đồng bộ cần có Số tài khoản, Bạn có thể validate bằng schema ở middlewares// Hoặc có thể kiểm tra trong đây luônif (!req.body.accountNumber) {returnres.status(404).json({ code:404, message:'Not foung Account number' }) }let resToken =awaitgetTokenUtil.getTokenByAPIKey(api_key);let accessToken =resToken.access_token;// Tiến hành gọi hàm đồng bộ qua cassoawaitsyncUtil.syncTransaction(req.body.accountNumber, accessToken);returnres.status(200).json({ code:200, message:'success', data:null }) } catch (error) {next(error) } })
3. Tạo một webhook để Casso có thể gửi giao dịch qua khi có giao dịch mới (quan trọng):
//routes/index.jsrouter.route('/webhook/handler-bank-transfer').post(async (req, res, next) => {try {// B1: Ở đây mình sẽ thực hiện check secure-token. Bình thường phần này sẽ nằm trong middlewares// Mình sẽ code trực tiếp tại đây cho dễ hình dung luồng. Nếu không có secure-token hoặc sai đều trả về lỗiif (!req.header('secure-token') ||req.header('secure-token') != secure_token) {returnres.status(401).json({ code:401, message:'Missing secure-token or wrong secure-token' }) }// B2: Thực hiện lấy thông tin giao dịch for (let item ofreq.body.data) {// Lấy thông orderId từ nội dung giao dịchlet orderId =webhookUtil.parseOrderId(case_insensitive, transaction_prefix,item.description);// Nếu không có orderId phù hợp từ nội dung ra next giao dịch tiếp theoif (!orderId) continue;// Kiểm tra giao dịch còn hạn hay không? Nếu không qua giao dịch tiếp theo if ((((new Date()).getTime() - (new Date(item.when)).getTime()) / 86400000) >= expiration_date) continue;
// Bước quan trọng đây.// Sau khi có orderId Thì thực hiện thay đổi các trang thái giao dịch// Ví dụ như kiểm tra orderId có tồn tại trong danh sách các đơn hàng của bạn?// Sau đó cập nhật trạng thái theo orderId và amount nhận được: đủ hay thiếu tiền...// Và một số chức năng khác có thể tùy biến }returnres.status(200).json({ code:200, message:'success', data:null }) } catch (error) {next(error) } })