Hướng dẫn lập trình cổng thanh toán miễn phí

1. Tổng quan

Bài viết này hướng dẫn lập trình cổng thanh toán một cách cơ bản. Bạn có thể hoàn thành bài hướng dẫn này chỉ trong 30 phút.

Xem Demo Online

SePay payment gateway overview

Mô hình tổng quan của cổng thanh toán sử dụng API Ngân hàng

Giải thích mô hình

Bước (1): Sau khi hoàn tất đặt hàng, website của bạn sẽ hiện lên thông tin thanh toán.

Bước (2): Khách hàng mở app ngân hàng quét mã chuyển khoản.

Bước (3): Sau khi hoàn tất chuyển khoản, tiền sẽ được chuyển đến tài khoản ngân hàng của bạn, ngân hàng sẽ push thông tin giao dịch đến hệ thống của SePay

Bước (4): SePay sẽ bắn webhook đến website của bạn, webhook chứa toàn bộ thông tin giao dịch.

Bước (5): Website của bạn nhận thông tin và tìm kiếm đơn hàng liên quan đến giao dịch, sau đó cập nhật trạng thái thanh toán của đơn hàng.

Bước (6): Website của bạn hiển thị thông tin đã nhận thanh toán thành công cho người dùng.

Như vậy, chúng ta cần lập trình bước (1) GIAO DIỆN CHECKOUT, (5) WEBSITE CỦA BẠN (Xử lý thanh toán) và (6) HIỂN THỊ KẾT QUẢ THANH TOÁN THÀNH CÔNG.

Yên tâm vì việc lập trình này rất đơn giản, chúng tôi cung cấp toàn bộ code mẫu để minh hoạ bên dưới.

Lưu ý: Ví dụ này cung cấp các dòng code đơn giản và không có tính năng lọc, ngăn chặn các hình thức tấn công như SQL Injection, XSS... Nếu bạn dùng cho môi trường production, hãy đảm bảo các quy tắc bảo mật được thực thi.

2. Chuẩn bị website đơn giản

Trước khi lập trình giao diện checkout, chúng ta cần có một website đơn giản với tính năng lưu đơn hàng, giao dịch ngân hàng. Ví dụ này dùng PHP & MySQL. Bạn có thể dùng ngôn ngữ khác nếu muốn.

2.1. Cơ sở dữ liệu

2.1.1. Tạo database và phân quyền

Tạo database tên host_sepay, user mysql host_sepay tên và mật khẩu YOUR_PASSWORD

Vào giao diện MySQL Command line, thực hiện các lệnh:

create database host_sepay;
create user 'host_sepay'@'localhost' identified by 'YOUR_PASSWORD';
grant all privileges on host_sepay.* to  'host_sepay'@'localhost' identified by 'YOUR_PASSWORD';

2.1.2. Tạo các bảng cần thiết (table)

Tạo table tb_transactions để lưu thông tin giao dịch ngân hàng:

use host_sepay;
CREATE TABLE `tb_transactions` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `gateway` varchar(100) NOT NULL,
    `transaction_date` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `account_number` varchar(100) DEFAULT NULL,
    `sub_account` varchar(250) DEFAULT NULL,
    `amount_in` decimal(20,2) NOT NULL DEFAULT 0.00,
    `amount_out` decimal(20,2) NOT NULL DEFAULT 0.00,
    `accumulated` decimal(20,2) NOT NULL DEFAULT 0.00,
    `code` varchar(250) DEFAULT NULL,
    `transaction_content` text DEFAULT NULL,
    `reference_number` varchar(255) DEFAULT NULL,
    `body` text DEFAULT NULL,
    `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
    PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3;

Tạo table tb_orders để lưu thông tin đơn hàng:

CREATE TABLE `tb_orders` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `total` decimal(20,2) NOT NULL DEFAULT 0.00,
    `payment_status` enum('Unpaid','Paid','Cancelled','Refunded') NOT NULL DEFAULT 'Unpaid',
    `name` varchar(250),
    `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
    PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3;

2.2. Tạo trang web đặt hàng

Trang web này cung cấp giao diện đặt hàng đơn giản. Sau khi điền số tiền, bấm đặt hàng, hệ thống sẽ tạo đơn hàng và chuyển hướng đến giao diện thanh toán.

Danh sách các file trong bộ mã nguồn demo:

files

Tải toàn bộ code mẫu

Demo trang web này tại https://payment-gateway-demo.sepay.dev/order.php

2.2.1. File db_connect.php

File này dùng để cho các file khác cần thao tác với CSDL include vào. Mục đích để kết nối CSDL mà không cần phải code lặp lại. Hãy điều chỉnh giá trị các biến khai báo cho phù hợp với thông tin kết nối CSDL của bạn.

<?php

    /*
    File db_connect.php
    File này dùng cho các file khác include vào. Mục đích để khởi tạo kết nối CSDL
    */
    
    // Khai báo cấu hình kết nối CSDL. Tuỳ chỉnh ở đây nếu tham số kết nối CSDL của bạn khác
    $servername = "localhost";
    $username = "host_sepay";
    $password = "YOUR_PASSWORD";
    $dbname = "host_sepay";

    // Kết nối CSDL sử dụng MySQLi.
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Kiểm tra kết nối
    if ($conn->connect_error) {
        echo json_encode(['success'=>FALSE, 'message' => 'MySQL connection failed: '. $conn->connect_error]);
        die();
    }
 
?>

2.2.2. File order.php

File order.php dùng để tạo đơn hàng, đồng thời chuyển hướng người dùng đến giao diện cổng thanh toán sau khi đơn hàng được tạo.

Xem demo online file này tại đây

2.2.3. File check_payment_status.php

File dùng cho Ajax ở giao diện thanh toán gọi đến kiểm tra trạng thái đơn hàng. Payload POST lên sẽ là {order_id: ORDER_ID}. Dữ liệu trả về dạng JSON {"payment_status":"PAYMENT_STATUS"}. Ví dụ dữ liệu trả về: {"payment_status":"Unpaid"}

<?php
/*
File check_payment_status.php
File phục vụ cho Ajax POST lấy kết quả trạng thái đơn hàng
URL ajax post sẽ là https://yourwebsite.tld/check_payment_status.php
*/

// Include file db_connect.php, file chứa toàn bộ kết nối CSDL
require('db_connect.php');

// Chỉ cho phép POST và POST có ID đơn hàng
if(!$_POST || !isset($_POST['order_id']) || !is_numeric($_POST['order_id']))
   die('access denied');

$order_id = $_POST['order_id'];

// Kiểm tra đơn hàng có tồn tại không
$result = $conn->query("SELECT payment_status FROM tb_orders where id={$order_id}");

if($result) {
    // Lấy thông tin đơn hàng
   $order_details = $result->fetch_object();
   
   // Trả về kết quả trạng thái đơn hàng dạng JSON. Ví dụ: {"payment_status":"Unpaid"}
   echo json_encode(['payment_status' => $order_details->payment_status]);
} else {
    
    // Trả về kết quả không tìm thấy đơn hàng
   echo json_encode(['payment_status' => 'order_not_found']);

}
?>

2.3. Nhận webhook từ SePay với file sepay_webhook.php

Mỗi khi phát sinh giao dịch thanh toán. SePay sẽ bắn webhook về website của bạn. Vì vậy chúng ta phải tạo một endpoint nhận thông tin thanh toán.

Dữ liệu gửi từ SePay đến endpoint của bạn sẽ ở dạng POST, với định dạng như sau:

{
    "id": 92704,                              // ID giao dịch trên SePay
    "gateway":"Vietcombank",                  // Brand name của ngân hàng
    "transactionDate":"2024-07-25 14:02:37",  // Thời gian xảy ra giao dịch phía ngân hàng
    "accountNumber":"0123499999",              // Số tài khoản ngân hàng
    "code":null,                               // Mã code thanh toán (sepay tự nhận diện dựa vào cấu hình tại Công ty -> Cấu hình chung)
    "content":"chuyen tien mua iphone",        // Nội dung chuyển khoản
    "transferType":"in",                       // Loại giao dịch. in là tiền vào, out là tiền ra
    "transferAmount":2277000,                  // Số tiền giao dịch
    "accumulated":19077000,                    // Số dư tài khoản (lũy kế)
    "subAccount":null,                         // Tài khoản ngân hàng phụ (tài khoản định danh),
    "referenceCode":"MBVCB.3278907687",         // Mã tham chiếu
    "description":""                           // Toàn bộ nội dung tin notify ngân hàng
}

Bạn có thể xem thêm về webhook của SePay tại đây

Endpoint trong ví dụ này là https://payment-gateway-demo.sepay.dev/sepay_webhook.php với file sepay_webhook.php. Dưới đây là nội dung file sepay_webhook.php

<?php

/* 
File sepay_webhook.php
File này dùng làm endpoint nhận webhook từ SePay. Mỗi khi có giao dịch SePay sẽ bắn webhook về và chúng ta sẽ lưu thông tin giao dịch vào CSDL. Đồng thời bóc tách ID đơn hàng từ nội dung thanh toán. Sau khi tìm được ID đơn hàng thì cập nhật trạng thái thanh toán của đơn hàng thành đã thanh toán (payment_status=Paid).
 Xem hướng dẫn tạo tích hợp Webhook phía SePay tại https://docs.sepay.vn/tich-hop-webhooks.html
 Endpoint nhận webhook sẽ là https://yourwebsite.tld/sepay_webhook.php
*/

 // Include file db_connect.php, file chứa toàn bộ kết nối CSDL
require('db_connect.php');

// Lay du lieu tu webhooks, xem cac truong du lieu tai https://docs.sepay.vn/tich-hop-webhooks.html#du-lieu
$data = json_decode(file_get_contents('php://input'));
if(!is_object($data)) {
    echo json_encode(['success'=>FALSE, 'message' => 'No data']);
    die('No data found!');
}

// Khoi tao cac bien
$gateway = $data->gateway;
$transaction_date = $data->transactionDate;
$account_number = $data->accountNumber;
$sub_account = $data->subAccount;

$transfer_type = $data->transferType;
$transfer_amount = $data->transferAmount;
$accumulated = $data->accumulated;

$code = $data->code;
$transaction_content = $data->content;
$reference_number = $data->referenceCode;
$body = $data->description;

$amount_in = 0;
$amount_out = 0;

// Kiem tra giao dich tien vao hay tien ra
if($transfer_type == "in")
    $amount_in = $transfer_amount;
else if($transfer_type == "out")
    $amount_out = $transfer_amount;

// Tao query SQL
$sql = "INSERT INTO tb_transactions (gateway, transaction_date, account_number, sub_account, amount_in, amount_out, accumulated, code, transaction_content, reference_number, body) VALUES ('{$gateway}', '{$transaction_date}', '{$account_number}', '{$sub_account}', '{$amount_in}', '{$amount_out}', '{$accumulated}', '{$code}', '{$transaction_content}', '{$reference_number}', '{$body}')";

// Chay query de luu giao dich vao CSDL
if ($conn->query($sql) === TRUE) {
   // echo json_encode(['success'=>TRUE]);
} else {
    echo json_encode(['success'=>FALSE, 'message' => 'Can not insert record to mysql: ' . $conn->error]);
}

// Tách mã đơn hàng

// Biểu thức regex để khớp với mã đơn hàng
$regex = '/DH(\d+)/';

// Sử dụng preg_match để khớp regex với chuỗi nội dung chuyển tiền
preg_match($regex, $transaction_content, $matches);

// Lấy mã đơn hàng từ kết quả khớp
$pay_order_id = $matches[1];

// Nếu không tìm thấy mã đơn hàng từ nội dung thanh toán thì trả về kết quả lỗi
if(!is_numeric($pay_order_id)) {
    echo json_encode(['success' => false, 'message' => 'Order not found. Order_id ' . $pay_order_id]);
    die();
}

// Tìm đơn hàng với mã đơn hàng và số tiền tương ứng với giao dịch thanh toán trên. Điều kiện là id đơn hàng, số tiền, trạng thái đơn hàng phải là 'Unpaid'
$result = $conn->query("SELECT * FROM tb_orders where id={$pay_order_id} AND total={$amount_in} AND payment_status='Unpaid'");

// Nếu không tìm thấy đơn hàng
if(!$result) {
    echo json_encode(['success' => false, 'message' => 'Order not found. Order_id ' . $pay_order_id]);
    die();
} else {
    // Tìm thấy đơn hàng, update trạng thái 
    $conn->query("UPDATE tb_orders SET payment_status='Paid' WHERE id='{$pay_order_id}'");
    echo json_encode(['success'=>TRUE]);

}
 
?>

Nhiệm vụ của file sepay_webhook.php

  • Nhận thông tin giao dịch từ SePay
  • Lưu giao dịch vào CSDL (bảng tb_transactions)
  • Bóc tách nội dung giao dịch để tìm mã đơn hàng (dùng regex)
  • Kiểm tra đơn hàng có tồn tại không
  • Nếu đơn hàng tồn tại (khớp với ID, Số tiền, Trạng thái) thì cập nhật trạng thái đơn hàng sang đã thanh toán (Paid)

3. Tạo tích hợp Webhook phía SePay

Mục đích bước này là khai báo endpoint nhận Webhook ở phía SePay. Sau bước này, mỗi khi có giao dịch thanh toán, SePay sẽ bắn webhook về endpoint của bạn.

Truy vập vào my.sepay.vn. Chọn Đăng ký nếu chưa có tài khoản, bạn có thể chọn gói Miễn phí của SePay để trải nghiệm và tích hợp lập trình.

Chọn menu Tích hợp Webhooks => Thêm Webhooks

webhook form in sepay

Tại đây, chúng ta cần để ý những trường như sau:

- Khi tài khoản ngân hàng là: Chọn tài khoản ngân hàng dùng để nhận thanh toán, tương ứng với tài khoản mà bạn hiển thị thanh toán cho người dùng

- Gọi đến URL: Điền endpoint của bạn, trong ví dụ này là https://payment-gateway-demo.sepay.dev/sepay_webhook.php

- Kiểu chứng thực:(Nâng cao). Để an toàn, bạn có thể chọn kiểu chứng thực là API Key, sau đó lập trình kiểm tra chứng thực tại file sepay_webhook.php. Tại ví dụ này để cho đơn giản, chúng tôi bỏ qua phần chứng thực này.

4. Testing và debug

4.1. Đặt hàng và thanh toán

Truy cập vào https://yourdomain.tld/order.php. Chọn đặt hàng và thử thanh toán. Nếu sau vài giây, giao diện checkout hiển thị "Thanh toán thành công" Nghĩa là code của bạn hoạt động tốt. Lúc này kiểm tra tại bảng tb_transactions sẽ thấy giao dịch ngân hàng. Đồng thời dữ liệu đơn hàng tại tb_orders cũng có payment_statusPaid.

Bước 1:

order page

Bước 2:

checkout form

Bước 3:

Scan qr code

Bước 4:

order paid

4.2. Xem webhook logs tại SePay

SePay đồng bộ giao dịch một cách tức thì. Bạn có thể xem lại danh sách giao dịch thanh toán tại my.sepay.vn => Giao dịch

 sepay transaction list

Để xem danh sách các webhook SePay đã gửi đi, bạn có thể vào Xem chi tiết giao dịch => Xem Webhooks đã bắn.

sepay transaction details

Để xem chi tiết thông tin webhook bao gồm request lẫn response. Bạn click vào Chi tiết ở mỗi dòng log

Tại đây bạn có thể chọn vào Gọi lại nếu muốn SePay bắn lại webhook. SePay sẽ bắn lại webhook tức thì và trả kết quả ngay trên giao diện.

sepay webhook logs

Như vậy, giao diện xem webhook của SePay sẽ giúp bạn debug được webhook của mình code có chạy đúng hay không (dựa vào response trả về, HTTP Status code).

Tính năng gọi lại Webhook giúp bạn có thể gọi lại webhook mà không cần thực hiện giao dịch mới.

5. Nâng cao

Retry webhooks tự động

SePay sẽ gọi lại webhooks nếu trạng thái kết nối mạng đến webhook url thất bại. Ngoài ra, bạn có thể tùy chọn các điều kiện SePay hỗ trợ sẵn để có thể gọi lại webhooks. Xem thêm phần retry webhook của SePay tại đây

Chống trùng lặp giao dịch

Để tránh trùng lặp giao dịch với cơ chế retry. SePay khuyến nghị người dùng xử lý chống trùng lặp giao dịch bằng cách sử dụng trường id trong nội dung webhook của SePay. Đây là trường ID giao dịch thuộc phía SePay, nếu webhook của 1 giao dịch bắn nhiều lần, trường này sẽ cùng một giá trị.

Như vậy mỗi lần nhận webhook, hãy lưu trường id lại. Đồng thời kiểm tra xem đã có dữ liệu với ID trên chưa, nếu đã có nghĩa là webhook bắn nhiều lần.

Công nghệ sử dụng

  • API ngân hàng: SePay kết nối trực tiếp API đến ngân hàng. Vì vậy tốc độ đồng bộ giao dịch gần như là tức thì. Công nghệ này giúp đồng bộ giao dịch từ ngân hàng đến SePay.
  • Webhook: Giúp bắn thông tin giao dịch từ SePay sang website của bạn.
  • VietQR: Công nghệ thanh toán chuyển khoản bằng mã QR Code. Được phát triển bởi NAPAS. Trang chủ của VietQR là https://vietqr.net. Bạn cũng có thể tạo ảnh QR động nhanh chóng tại qr.sepay.vn

Tổng kết

Hướng dẫn trên giúp bạn lập trình một cổng thanh toán đơn giản sử dụng webhook của SePay. Chúng tôi hy vọng hướng dẫn sẽ giúp ích cho bạn. Đồng thời nếu bạn cần hỗ trợ, hãy liên hệ SePay nhé