0

[NodeJS] Tích hợp VNPay "Thực Chiến": Từ khởi tạo đến khi "Ting Ting" tài khoản Production

Chào anh em, mình là Hoàng. Sau một thời gian lăn lộn với các hệ thống E-commerce, mình nhận thấy việc tích hợp thanh toán luôn là khâu "đau đầu" nhất, không phải vì code khó, mà vì làm sao để an toànkhông mất tiền oan.

Hôm nay mình sẽ hướng dẫn anh em triển khai VNPay bằng NodeJS một cách bài bản nhất cho môi trường Production.

1. Cấu trúc thư mục (Project Structure)

Đừng viết tất cả vào 1 file index.js. Một dự án "thật" cần sự tách biệt:

vnpay-integration/
├── src/
│   ├── config/
│   │   └── vnpay.config.js  # Lưu tham số Terminal ID, Secret Key
│   ├── utils/
│   │   └── vnpay.util.js    # Hàm băm SHA512, sắp xếp params
│   ├── services/
│   │   └── payment.service.js # Logic tạo URL & xử lý kết quả
│   ├── routes/
│   │   └── payment.route.js
│   └── app.js
├── .env
└── package.json

2. Thiết lập Utility - "Trái tim" của bảo mật

VNPay yêu cầu tham số truyền lên phải được sắp xếp theo bảng chữ cái và băm bằng thuật toán HmacSHA512

File: src/utils/vnpay.util.js

const crypto = require("crypto");
const qs = require("qs");

/**
 * Sắp xếp các key của object theo thứ tự alphabet
 */
const sortObject = (obj) => {
    let sorted = {};
    let str = [];
    let key;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) {
            str.push(encodeURIComponent(key));
        }
    }
    str.sort();
    for (key = 0; key < str.length; key++) {
        sorted[str[key]] = encodeURIComponent(obj[str[key]]).replace(/%20/g, "+");
    }
    return sorted;
};

/**
 * Tạo chữ ký Secure Hash
 */
const generateSignature = (params, secretKey) => {
    const signData = qs.stringify(params, { encode: false });
    const hmac = crypto.createHmac("sha512", secretKey);
    return hmac.update(Buffer.from(signData, 'utf-8')).digest("hex");
};

module.exports = { sortObject, generateSignature };

3. Khởi tạo thanh toán (Create Payment URL)

Tại bước này, lưu ý quan trọng nhất là vnp_Amount. VNPay quy định số tiền gửi lên phải nhân với 100.

File: src/services/payment.service.js (Trích đoạn)

const createPaymentUrl = (order, ipAddr) => {
    const vnpParams = {
        vnp_Version: '2.1.0',
        vnp_Command: 'pay',
        vnp_TmnCode: process.env.VNP_TMN_CODE,
        vnp_Locale: 'vn',
        vnp_CurrCode: 'VND',
        vnp_TxnRef: order.id, // ID duy nhất của bạn
        vnp_OrderInfo: `Thanh toan don hang ${order.id}`,
        vnp_OrderType: 'other',
        vnp_Amount: order.amount * 100, // Quan trọng: Nhân 100
        vnp_ReturnUrl: process.env.VNP_RETURN_URL,
        vnp_IpAddr: ipAddr,
        vnp_CreateDate: moment().format('YYYYMMDDHHmmss'),
    };

    const sortedParams = sortObject(vnpParams);
    const signed = generateSignature(sortedParams, process.env.VNP_HASH_SECRET);
    
    sortedParams['vnp_SecureHash'] = signed;
    return `${process.env.VNP_URL}?${qs.stringify(sortedParams, { encode: false })}`;
};

4. Xử lý IPN - "Chốt hạ" giao dịch (Cực kỳ quan trọng)

Có 2 URL trả về:

  1. Return URL: Để hiển thị giao diện cho khách hàng (Có thể bị can thiệp).
  2. IPN URL: Server-to-Server. Đây mới là nơi bạn cập nhật DB là khách đã trả tiền hay chưa.

Lưu ý Production: Bạn phải kiểm tra 4 điều kiện trước khi xác nhận thành công:

  1. Checksum hợp lệ
  2. Đơn hàng tồn tại trong DB
  3. Số tiền khớp với DB
  4. Trạng thái đơn hàng hiện tại là "Chờ thanh toán".

5. Quy trình Test "Mất tiền thật"

Khi bạn đã chạy ổn trên Sandbox, đây là các bước để Go-Live:

  1. Ký hợp đồng: Liên hệ VNPay để lấy thông tin Production (TmnCode, HashSecret).
  2. Cập nhật Config: Thay URL Sanbox bằng URL Production (https://pay.vnpayment.vn/vpcpay.html).
  3. Thanh toán thật:
  • Tạo một sản phẩm giá rẻ (VD: 10,000 VNĐ).
  • Dùng thẻ ngân hàng thật của bạn để thanh toán.
  • Kiểm tra DB: Xem trạng thái có nhảy từ Pending sang Success không.
  • Đối soát: Vào trang quản trị Merchant của VNPay xem tiền đã vào chưa.

Kinh nghiệm xương máu:

  • Log mọi thứ: Hãy lưu toàn bộ log từ VNpay gửi về một bảng PaymentLogs. Khi có tranh chấp hoặc lỗi, đây là bằng chứng duy nhất.
  • Idempotency: Đảm bảo một mã giao dịch vnp_TxnRef không được xử lý 2 lần thành công nếu VNPay gọi IPN nhiều lần.

Kết bài

Hy vọng bài viết giúp anh em tự tin hơn khi "đụng độ" với VNPay. Nếu thấy hay thì cho mình một Upvote và Bookmark nhé!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.