Tích hợp xác nhận thanh toán

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 API Casso đã 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');
async function main() {
    app
    console.log(`Server on port ${process.env.PORT || 4300}`);
};
main();

Bước 2: Tạo file app.js, config và xử lý lỗi

Các thứ cần thiết cho server như: cors, json, urlencodedExpress error handling

let express = require("express");
var cors = require('cors');
let app = express();
// Tạo cors
var 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 found
app.use(function (req, res, next) {
    res.status(404).json({
        code: 404,
        message: 'Endpoint not found'
    });
})
// Xử lí khi lỗi ở phía server
app.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 header
router.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à banks
router.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 AxiosQuery-string.

//utils/api.js
const axios = require("axios");
const queryString =  require("query-string");
const axiosClient = axios.create({
  baseURL: 'https://oauth.casso.vn/v1',
  headers: {
    "content-type": "application/json",
  },
  paramsSerializer: (params) => queryString.stringify(params),
});
axiosClient.interceptors.request.use(async (config) => {
  return config;
});
axiosClient.interceptors.response.use(
  (response) => {
    if (response && response.data) return response.data;
    return response;
  },
  (error) => {
    throw error;
  }
);
module.exports =  axiosClient;

Sau khi code HTTP Client thì tiến hành dựng từng hàm tương ứng với từng API.

  • Get token từ API key lấy từ Casso. Mô tả cụ thể về API tại đây

/*utils/get_token.util.js*/
const api = require('./api');
module.exports = {
    getTokenByAPIKey: async (code) => {
            let token = await api.post('/token', { code: code });
        return token;
    }
}
  • Get userInfo bao gồm thông tin về business và banks. Mô tả cụ thể về API tại đây

/*utils/get_user_info.util.js*/
    getDetailUser: async (accessToken) => {
        api.defaults.headers.Authorization = accessToken;
        let res = await api.get(`/userInfo`);
        return res;
    },
  • Đồng bộ dữ liệu mới nhất. Mô tả cụ thể về API tại đây

/*utils/sync.util.js*/
    syncTransaction: async (bankNumber, accessToken) => {
        api.defaults.headers.Authorization = accessToken;
        let res = await api.post('/sync', { bank_acc_id: bankNumber });
        return res;
    }
  • 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 = await api.post('/webhooks', data);
        return res;
    },
    getDetailWebhookById: async (webhookId, accessToken) => {
        api.defaults.headers.Authorization = accessToken;
        let res = await api.get(`/webhooks/${webhookId}`);
        return res;
    },
    updateWebhookById: async (webhookId, accessToken, data) => {
        api.defaults.headers.Authorization = accessToken;
        let res = await api.put(`/webhooks/${webhookId}`, data);
        return res;
    },
    deleteWebhookById: async (webhookId, accessToken) => {
        api.defaults.headers.Authorization = accessToken;
        let res = await api.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 = await api.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 = 101
        let re = new RegExp(transactionPrefix);
        if (!caseInsensitive)
            re = new RegExp(transactionPrefix, 'i');
        let matchPrefix = description.match(re);
        // Không tồn tại tiền tố giao dịch
        if (!matchPrefix) return null;
        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ịch
const transaction_prefix = 'CASSO';
// Phân biệt chữ hoa/thường trong tiền tố giao dịch
const case_insensitive = false;
//Hạn của đơn hàng là 3 ngày. Quá 3 ngày thì không xử lý
const expiration_date = 3;
// API KEY lấy từ casso
const api_key = '45e40320-e0b7-11eb-a12c-35cc867f21a0';
// secure_token đăng kí khi tạo webhook
const secure_token = 'R5G4cbnN7uSAwfTd'
  1. Route tạo webhook bằng API_KEY và lấy thông tin user bao gồm Business và banks

//routes/index.js
router.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ại
            let resToken = await getTokenUtil.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 webhook
            let data = {
                webhook: 'https://ten-mien-cua-ban.com/webhook/handler-bank-transfer',
                secure_token: secure_token,
                income_only: true
            }
            let newWebhook = await webhookUtil.create(data, accessToken);
            // Lấy thông tin về userInfo
            let userInfo = await userUtil.getDetailUser(accessToken);
            return res.status(200).json({
                code: 200,
                message: 'success',
                data: {
                    webhook: newWebhook.data,
                    userInfo: userInfo.data
                }
            })
        } catch (error) {
            next(error)
        }
    })
curl --location --request POST 'http://localhost:4300/register-webhook' \
--header 'Content-Type: application/json'
{
    "code": 200,
    "message": "success",
    "data": {
        "webhook": {
            "id": 415,
            "channel": "webhook",
            "param1": "https://ten-mien-cua-ban.com/webhook/handler-bank-transfer",
            "param2": "R5G4cbnN7uSAwfTd",
            "sendOnlyIncome": 1
        },
        "userInfo": {
            "user": {
                "id": 1553,
                "email": "haonh@magik.vn"
            },
            "business": {
                "id": 1540,
                "name": "Hữu Hảo"
            },
            "bankAccs": [
                {
                    "id": 619,
                    "bank": {
                        "bin": 970416,
                        "codeName": "acb_digi"
                    },
                    "bankAccountName": null,
                    "bankSubAccId": "17271687",
                    "connectStatus": 1,
                    "planStatus": 1
                },
                {
                    "id": 623,
                    "bank": {
                        "bin": 970454,
                        "codeName": "timoplus"
                    },
                    "bankAccountName": null,
                    "bankSubAccId": "8007041023848",
                    "connectStatus": 1,
                    "planStatus": 0
                }
            ]
        }
    }
}

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.js
router.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ôn
            if (!req.body.accountNumber) {
                return res.status(404).json({
                    code: 404,
                    message: 'Not foung Account number'
                })
            }
            let resToken = await getTokenUtil.getTokenByAPIKey(api_key);
            let accessToken = resToken.access_token;
            // Tiến hành gọi hàm đồng bộ qua casso
            await syncUtil.syncTransaction(req.body.accountNumber, accessToken);
            return res.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.js
router.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ỗi
            if (!req.header('secure-token') || req.header('secure-token') != secure_token) {
                return res.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 of req.body.data) {
                // Lấy thông orderId từ nội dung giao dịch
                let 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 theo
                if (!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
            }
            return res.status(200).json({
                code: 200,
                message: 'success',
                data: null
            })
        } catch (error) {
            next(error)
        }
    })

Cảm ơn đã theo dõi

Last updated