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.
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:
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
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_status
là Paid.
Bước 1:
Bước 2:
Bước 3:
Bước 4:
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
Để 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.
Để 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.
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é