Tích hợp MQ-Pay
MQ-Pay (Multi-provider Quick Payment System) là thành phần tích hợp thanh toán cung cấp API thống nhất để kết nối nhiều nhà cung cấp thanh toán.
Tổng quan
| Thuộc tính | Giá trị |
|---|---|
| Gói | @nx/mq-pay |
| Trạng thái | Production |
| Nhà cung cấp | VNPAY QR MMS, VNPAY Phone POS, VNPAY Smart POS, Hệ thống (Tiền mặt/Chuyển khoản) |
Chế độ Triển khai
MQ-Pay hỗ trợ triển khai theo chế độ qua APP_ENV_MQ_PAY_MODE để mở rộng API và worker độc lập:
| Chế độ | Controllers | Queue Producers | BullMQ Workers | Event Handler | Trường hợp sử dụng |
|---|---|---|---|---|---|
full | Có | Có | Có | Có | Mặc định — một instance xử lý tất cả |
api | Có | Có | Không | Không | Chỉ REST API — đẩy job vào hàng đợi, không xử lý |
worker | Không | Có | Có | Có | Chỉ BullMQ workers — xử lý job, không có HTTP |
Mô hình triển khai production: 1 API instance (mode=api, Snowflake ID=8) + N worker instances (mode=worker, Snowflake IDs duy nhất 91, 92, ...).
Tính năng Chính
- Đa nhà cung cấp - Hỗ trợ nhiều cổng thanh toán tại một nơi
- Tích hợp nhanh - Thiết lập đơn giản, bắt đầu chấp nhận thanh toán trong vài phút
- API Thống nhất - Cùng một giao diện cho tất cả các nhà cung cấp
- Xử lý nền - Hàng đợi BullMQ để xử lý thanh toán tin cậy
- Thông báo sự kiện - Hệ thống sự kiện dựa trên callback cho vòng đời thanh toán
Bắt đầu Nhanh
1. Cấu hình ứng dụng của bạn
// application.ts
import { MQPayComponent, MQPayBindingKeys, IMQPayOptions } from '@nx/mq-pay';
class MyApplication extends BaseApplication {
preConfigure() {
// Cấu hình nhà cung cấp thanh toán
this.bind<IMQPayOptions>({ key: MQPayBindingKeys.MQ_PAY_CLIENT_OPTIONS })
.toValue({
mode: process.env.APP_ENV_MQ_PAY_MODE ?? 'full', // 'full' | 'api' | 'worker'
vnpayQrMMS: {
enable: true,
isDefault: true,
enableController: true,
appId: process.env.VNPAY_APP_ID,
secretKey: process.env.VNPAY_SECRET_KEY,
masterMerchantCode: process.env.VNPAY_MERCHANT_CODE,
isProduction: process.env.NODE_ENV === 'production',
},
});
// Tải thành phần MQ-Pay
this.component(MQPayComponent);
}
}2. Thiết lập biến môi trường
# Chế độ Triển khai (full | api | worker)
APP_ENV_MQ_PAY_MODE=full
# Cơ sở dữ liệu (PostgreSQL)
APP_ENV_MQ_PAY_POSTGRES_HOST=localhost
APP_ENV_MQ_PAY_POSTGRES_PORT=5432
APP_ENV_MQ_PAY_POSTGRES_DATABASE=mq_pay
APP_ENV_MQ_PAY_POSTGRES_USERNAME=postgres
APP_ENV_MQ_PAY_POSTGRES_PASSWORD=secret
# Redis (cho queues)
APP_ENV_MQ_PAY_REDIS_QUEUE_HOST=localhost
APP_ENV_MQ_PAY_REDIS_QUEUE_PORT=63793. Hoàn tất!
MQ-Pay sẽ tự động:
- Kết nối với cơ sở dữ liệu và Redis của bạn
- Đăng ký các nhà cung cấp thanh toán bạn đã kích hoạt
- Thiết lập hàng đợi nền để xử lý thanh toán
- Đăng ký các điểm cuối IPN/webhook (nếu
enableController: true)
Các Nhà cung cấp được Hỗ trợ
| Nhà cung cấp | Trạng thái | Mô tả |
|---|---|---|
| SYSTEM | Có sẵn | Tiền mặt, Chuyển khoản Thủ công |
| VNPAY_QR_MMS | Có sẵn | Thanh toán Mã QR Động VNPAY |
| VNPAY_PHONE_POS | Có sẵn | Tích hợp VNPAY Phone POS |
| VNPAY_SMART_POS | Có sẵn | Terminal VNPAY Smart POS |
| MOMO | Sắp ra mắt | - |
| ZaloPay | Sắp ra mắt | - |
Kiến trúc
Luồng Khởi tạo Thành phần
Khi MQPayComponent.binding() được gọi, trình tự khởi tạo sau xảy ra:
Mô hình Miền
Transaction - Một đơn hàng → một giao dịch
- Theo dõi tổng số tiền (
net,tax,fee,discount,total,paid) và trạng thái uidlà mã định danh duy nhất đọc được (không phảinumber)- Ràng buộc duy nhất ngăn chặn giao dịch hoạt động trùng lặp theo source
TransactionItem - Snapshot mặt hàng tại thời điểm thanh toán
- Ghi lại giá mặt hàng tại thời điểm tạo giao dịch
PaymentAttempt - Mỗi phương thức thanh toán tạo một lần thử
- Liên kết với giao dịch, theo dõi nhà cung cấp và phương thức sử dụng
remainingđược PostgreSQL tự động tính (amount - tendered)parentIdliên kết lần thử hủy/hoàn tiền với lần thử thanh toán gốc
PaymentResult - Dữ liệu phản hồi/callback từ nhà cung cấp
- Lưu trữ payload IPN/webhook gốc với loại trùng qua
payloadHash - Index duy nhất trên
(paymentAttemptId, type, payloadHash)ngăn xử lý trùng lặp
Mẫu AppRegistry
MQ-Pay sử dụng singleton registry (AppRegistry) tách biệt khỏi IGNIS DI container để quản lý tài nguyên runtime. Điều này cần thiết vì BullMQ workers và kết nối Redis có vòng đời không phù hợp với mô hình DI theo request-scope.
| Registry | Tiền tố | Mục đích |
|---|---|---|
| Redis | redis | Pool kết nối cho hàng đợi BullMQ |
| BullMQ | bullmq | Instance queue và worker |
| Payment Providers | payment-provider | VNPAY, System, v.v. |
| Event Handler | (singleton) | IPaymentEventHandler để phát sự kiện |
IMPORTANT
AppRegistry không thể sử dụng decorator @inject(). Luôn truy cập qua AppRegistry.getInstance().
Hệ thống Hàng đợi
MQ-Pay sử dụng BullMQ để xử lý công việc nền tin cậy. Các công việc được phân phối qua các partition để cân bằng tải.
| Loại Hàng đợi | Partitions | Loại Job | Mục đích |
|---|---|---|---|
scheduler | P01, P02, P03 | PAYMENT_EXPIRATION | Hết hạn mã QR chưa thanh toán sau timeout |
confirmation | P01, P02, P03 | PROCESS_IPN | Xử lý callback IPN/webhook từ nhà cung cấp |
Tổng cộng mỗi instance: chế độ full/worker: 6 queues + 6 workers. Chế độ api: chỉ có 6 queue producers (không có workers). 3 partitions mỗi loại, 10 công việc đồng thời mỗi worker theo mặc định.
Lựa chọn Partition: Các công việc được gán vào partition sử dụng hash của transactionId, đảm bảo tất cả công việc cho cùng một giao dịch đi vào cùng partition.
Vòng đời Trạng thái
Vòng đời Trạng thái Giao dịch
NOTE
SETTLED là trạng thái cuối cùng — khi giao dịch đã được thanh toán đầy đủ, không thể thay đổi. Hoàn tiền tạo các REFUND_PAYMENT attempt mới nhưng không thay đổi trạng thái giao dịch.
Vòng đời Trạng thái Lần thử Thanh toán
Luồng Thanh toán
Luồng Thanh toán Mã QR
Luồng Thanh toán Tiền mặt
Luồng Thanh toán Chia nhỏ
Luồng Xử lý IPN
Luồng Hủy Thanh toán
Luồng Hoàn tiền
Tài liệu tham khảo API
Tạo Thanh toán
POST /api/payments/checkout
Content-Type: application/json
{
"source": {
"type": "Order",
"id": "order-uuid-123",
"uid": "ORD-2024-001"
},
"payment": {
"provider": "VNPAY_QR_MMS",
"method": "100_QR_CODE",
"total": 100000,
"currency": "VND"
},
"expiration": {
"mode": "duration",
"milliseconds": 300000
}
}Phản hồi:
{
"transaction": {
"id": "txn-uuid-001",
"number": "TXN001",
"status": "100_NEW",
"total": "100000.0000",
"paid": "0.0000"
},
"attempt": {
"id": "att-uuid-001",
"code": "ATT001",
"status": "204_SENT",
"provider": "VNPAY_QR_MMS",
"method": "100_QR_CODE"
},
"payment": {
"qrCode": "00020101021238...",
"qrUrl": "https://...",
"expiresAt": "2024-01-15T10:35:00Z"
}
}Xác nhận Thanh toán Tiền mặt
POST /api/payments/system/ipn
Content-Type: application/json
{
"attempt": { "id": "att-uuid-001" },
"tendered": 100000,
"note": "Đã nhận thanh toán tiền mặt"
}Phản hồi:
{
"transaction": {
"id": "txn-uuid-001",
"status": "304_SETTLED",
"paid": "100000.0000",
"settledAt": "2024-01-15T10:30:00Z"
},
"attempt": {
"id": "att-uuid-001",
"status": "302_SUCCESS",
"tendered": "100000.0000",
"remaining": "0.0000"
}
}Tiếp tục Thanh toán Một phần
POST /api/payments/checkout
Content-Type: application/json
{
"transactionId": "txn-uuid-001",
"source": {
"type": "Order",
"id": "order-uuid-123"
},
"payment": {
"provider": "SYSTEM",
"method": "000_CASH",
"total": 40000,
"currency": "VND"
}
}Cấu hình Nhà cung cấp
VNPAY QR MMS
vnpayQrMMS: {
enable: true,
isDefault: true,
enableController: true,
appId: 'your-app-id',
secretKey: 'your-secret-key',
masterMerchantCode: 'your-merchant-code',
isProduction: false,
// Callback tùy chọn
onPaymentNotification: async (ipnData) => {
console.log('Payment received:', ipnData);
// Kích hoạt logic nghiệp vụ của bạn
},
}VNPAY Phone POS
vnpayPhonePOS: {
enable: true,
enableController: true,
appId: 'your-phone-pos-app-id',
secretKey: 'your-secret-key',
merchantCode: 'your-merchant-code',
terminalId: 'your-terminal-id',
isProduction: false,
}Hệ thống (Tiền mặt / Chuyển khoản)
Không cần cấu hình - luôn có sẵn.
Tham khảo Trạng thái
Trạng thái Giao dịch
| Trạng thái | Mã | Mô tả |
|---|---|---|
NEW | 100_NEW | Đã tạo, chưa thanh toán |
PARTIAL | 300_PARTIAL | Thanh toán một phần |
SETTLED | 304_SETTLED | Đã thanh toán đầy đủ (cuối cùng, không thay đổi) |
CANCELLED | 505_CANCELLED | Tự động hủy khi tất cả attempts bị hủy |
BLOCKED | 403_BLOCKED | Bị chặn tạm thời bởi admin |
CLOSED | 404_CLOSED | Đã đóng vĩnh viễn bởi admin |
Trạng thái Lần thử Thanh toán
| Trạng thái | Mã | Mô tả |
|---|---|---|
NEW | 100_NEW | Đã tạo, chưa gửi đến nhà cung cấp |
SENT | 204_SENT | Đã gửi đến nhà cung cấp, QR có sẵn |
SUCCESS | 302_SUCCESS | Thanh toán đã xác nhận qua IPN |
FAIL | 500_FAIL | Gọi nhà cung cấp thất bại |
EXPIRED | 501_EXPIRED | QR hết hạn, không nhận được thanh toán |
Loại Lần thử Thanh toán
| Loại | Mã | Mô tả |
|---|---|---|
MAKE_PAYMENT | 100_MAKE_PAYMENT | Lần thử thanh toán |
CANCEL_PAYMENT | 200_CANCEL_PAYMENT | Lần thử hủy (liên kết với gốc qua parentId) |
REFUND_PAYMENT | 300_REFUND_PAYMENT | Lần thử hoàn tiền (liên kết với gốc qua parentId) |
Phương thức Thanh toán
| Phương thức | Mã | Mô tả |
|---|---|---|
CASH | 000_CASH | Thanh toán tiền mặt |
QR_CODE | 100_QR_CODE | Thanh toán mã QR |
BANK_TRANSFER | 200_BANK_TRANSFER | Chuyển khoản ngân hàng |
E_WALLET | 300_E_WALLET | Thanh toán ví điện tử |
CARD | 400_CARD | Thanh toán thẻ |
Dịch vụ
PaymentExecutionService
Source:
services/payment/execution.service.ts| Extends:BasePaymentService
DI Dependencies: TransactionRepository, TransactionItemRepository, PaymentAttemptRepository, QueueProcessorService
| Phương thức | Chữ ký | Mô tả |
|---|---|---|
makePayment | (opts: { payload: TMakePaymentRequest }) → Promise<TMakePaymentResponse> | Tạo transaction + attempt, gọi nhà cung cấp hoặc chuẩn bị thanh toán thủ công |
Xử lý cả giao dịch mới và thêm attempt vào giao dịch hiện có (thanh toán chia nhỏ). Xác minh tổng mặt hàng, tính thời gian hết hạn, và thêm scheduler jobs cho nhà cung cấp bên thứ ba.
PaymentCancellationService
Source:
services/payment/cancellation.service.ts| Extends:BasePaymentService
DI Dependencies: TransactionRepository, PaymentAttemptRepository, QueueProcessorService
| Phương thức | Chữ ký | Mô tả |
|---|---|---|
cancelPayment | (opts: { payload: TCancelPaymentRequest }) → Promise<TCancelPaymentResponse> | Hủy lần thử cụ thể hoặc tất cả lần thử cho giao dịch |
Tạo CANCEL_PAYMENT attempt con liên kết qua parentId. Với nhà cung cấp bên thứ ba, gọi provider.cancelPayment(). Xóa scheduler expiration jobs.
PaymentConfirmationService
Source:
services/payment/confirmation.service.ts| Extends:BasePaymentService
DI Dependencies: TransactionRepository, PaymentAttemptRepository, PaymentResultRepository
| Phương thức | Chữ ký | Mô tả |
|---|---|---|
confirmPayment | (opts: TConfirmPaymentRequest) → Promise<TConfirmPaymentResponse> | Xác nhận thanh toán thủ công (CASH/BANK_TRANSFER) đã nhận |
Chỉ hoạt động với nhà cung cấp SYSTEM với phương thức CASH hoặc BANK_TRANSFER. Cập nhật attempt thành SUCCESS, tính toán giao dịch là SETTLED hay PARTIAL.
PaymentRefundService
Source:
services/payment/refund.service.ts| Extends:BasePaymentService
DI Dependencies: PaymentAttemptRepository
| Phương thức | Chữ ký | Mô tả |
|---|---|---|
refund | (opts: TRefundRequest) → Promise<TRefundResponse> | Hoàn tiền cho MAKE_PAYMENT attempt đã thành công |
Xác minh attempt gốc là SUCCESS, kiểm tra tổng đã hoàn tiền qua getTotalRefundedAmount(), đảm bảo số tiền hoàn không vượt quá còn lại.
PaymentVerificationService
Source:
services/payment/verification.service.ts| Extends:BasePaymentService
DI Dependencies: TransactionRepository, PaymentAttemptRepository, PaymentResultRepository
| Phương thức | Chữ ký | Mô tả |
|---|---|---|
verifyAttempt | (opts: TCheckTransactionRequest) → Promise<TCheckTransactionResponse> | Xác minh trạng thái lần thử thanh toán với nhà cung cấp |
Với nhà cung cấp bên thứ ba, gọi provider.checkTransaction(). Với thanh toán thủ công, trả về trạng thái local.
QueueProcessorService
Source:
services/queue-processor.service.ts| Extends:BaseService
DI Dependencies: PaymentAttemptRepository, TransactionRepository, PaymentResultRepository
| Phương thức | Chữ ký | Mô tả |
|---|---|---|
initialize | () → void | Tạo tất cả queues và workers — gọi initializeQueues() + initializeWorkers() |
initializeQueues | () → void | Tạo riêng 6 queue producer instances (dùng bởi chế độ api) |
initializeWorkers | () → void | Tạo riêng 6 worker instances (dùng nội bộ bởi initialize()) |
addSchedulerJob | (opts: { data, jobOptions? }) → Promise<{ queue, job } | null> | Thêm job hết hạn thanh toán |
removeSchedulerJob | (opts: { transactionId, jobId? }) → Promise<void> | Xóa job hết hạn |
addConfirmationJob | (opts: { data, jobOptions? }) → Promise<Job> | Thêm job xử lý IPN |
createSchedulerWorkers | (opts?: { handler?, concurrency? }) → BullMQHelper[] | Tạo scheduler partition workers |
createConfirmationWorkers | (opts?: { handler?, concurrency? }) → BullMQHelper[] | Tạo confirmation partition workers |
getQueue | (opts: { type, partition }) → BullMQHelper | Lấy queue theo loại và partition |
getQueueByKey | (opts: { type, partitionKey }) → BullMQHelper | Lấy queue theo partition key (tự động phân giải) |
Repositories
| Repository | Entity | Base Class | Phương thức Tùy chỉnh |
|---|---|---|---|
TransactionRepository | Transaction | DefaultCRUDRepository | getTotalPaidAmount({ transactionId }) — Tổng tendered từ các SUCCESS MAKE_PAYMENT attempts |
PaymentAttemptRepository | PaymentAttempt | DefaultCRUDRepository | isAttemptCancelled({ attemptId }) — Kiểm tra đã hủy qua child attempt |
findActiveAttempts({ transactionId }) — Tìm NEW/SENT attempts chưa bị hủy (CTE query) | |||
getTotalRefundedAmount({ parentAttemptId }) — Tổng số tiền SUCCESS refund | |||
resolveAttemptForIPN({ attemptId }) — Lấy attempt + transaction + trạng thái hủy trong một truy vấn | |||
PaymentResultRepository | PaymentResult | DefaultCRUDRepository | (Chỉ CRUD) |
TransactionItemRepository | TransactionItem | DefaultCRUDRepository | (Chỉ CRUD) |
NOTE
Tất cả repositories sử dụng PostgreSQL schema mqpay (cấu hình qua APP_ENV_MQ_PAY_SCHEMA).
Xử lý Lỗi
Các Lỗi Thường gặp
| Lỗi | Nguyên nhân | Giải pháp |
|---|---|---|
Invalid transaction status | Thêm thanh toán vào giao dịch đã hoàn tất | Kiểm tra trạng thái giao dịch trước |
Only CASH/BANK_TRANSFER can be confirmed | Xác nhận thanh toán QR qua endpoint hệ thống | Sử dụng IPN của nhà cung cấp |
PaymentAttempt is not in NEW status | Xác nhận kép | Kiểm tra trạng thái lần thử trước |
Missing binding key | Thành phần chưa được cấu hình | Bind tùy chọn trước khi tải thành phần |
Refund amount exceeds remaining | Hoàn tiền > (gốc - đã hoàn) | Kiểm tra getTotalRefundedAmount() trước |
Tham khảo Biến Môi trường
Cơ sở Dữ liệu (Bắt buộc)
| Biến | Mô tả | Mặc định |
|---|---|---|
APP_ENV_MQ_PAY_POSTGRES_HOST | Host cơ sở dữ liệu | - |
APP_ENV_MQ_PAY_POSTGRES_PORT | Cổng cơ sở dữ liệu | 5432 |
APP_ENV_MQ_PAY_POSTGRES_DATABASE | Tên cơ sở dữ liệu | - |
APP_ENV_MQ_PAY_POSTGRES_USERNAME | Người dùng cơ sở dữ liệu | - |
APP_ENV_MQ_PAY_POSTGRES_PASSWORD | Mật khẩu cơ sở dữ liệu | - |
APP_ENV_MQ_PAY_SCHEMA | Tên PostgreSQL schema | mqpay |
Redis Queue (Bắt buộc)
| Biến | Mô tả | Mặc định |
|---|---|---|
APP_ENV_MQ_PAY_REDIS_QUEUE_HOST | Host Redis (chế độ single) | localhost |
APP_ENV_MQ_PAY_REDIS_QUEUE_PORT | Cổng Redis | 6379 |
APP_ENV_MQ_PAY_REDIS_QUEUE_PASSWORD | Mật khẩu Redis | - |
APP_ENV_MQ_PAY_REDIS_QUEUE_MODE | single hoặc cluster | single |
APP_ENV_MQ_PAY_REDIS_QUEUE_MAX_RETRY | Số lần thử lại kết nối tối đa | - |
APP_ENV_MQ_PAY_REDIS_QUEUE_CLUSTER_NODES | Các cặp host:port phân cách bằng dấu phẩy (chế độ cluster) | - |
Snowflake ID
| Biến | Mô tả | Mặc định |
|---|---|---|
APP_ENV_MQ_PAY_WORKER_ID | Snowflake worker ID | 0 |
APP_ENV_MQ_PAY_EPOCH_CHECKPOINT | Snowflake epoch timestamp | 1735689600000 |
Chế độ Triển khai
| Biến | Mô tả | Mặc định |
|---|---|---|
APP_ENV_MQ_PAY_MODE | Chế độ triển khai: full, api, hoặc worker | full |
full— Đăng ký controllers, tạo queue producers và BullMQ workers, bind event handler. Mặc định cho phát triển và triển khai đơn giản.api— Chỉ đăng ký controllers và queue producers. Không có workers, không có event handler. Dùng để mở rộng API độc lập.worker— Tạo queue producers, BullMQ workers, và bind event handler. Không có controllers. Dùng để mở rộng xử lý job độc lập.
Thống kê Mã nguồn
| Chỉ số | Số lượng |
|---|---|
| Source Files | 25+ TypeScript files |
| Entities | 4 (Transaction, TransactionItem, PaymentAttempt, PaymentResult) |
| Repositories | 4 (tất cả với DefaultCRUDRepository) |
| Services | 7 (6 payment + QueueProcessor) |
| Controllers | 5 (PaymentController + 4 CRUD) |
| REST Endpoints | 5 tùy chỉnh + 4×CRUD |
| Payment Providers | 4 (System, VNPAY QR MMS, Phone POS, Smart POS) |
| BullMQ Queues | 6 (2 loại × 3 partitions) |
| Redis Connections | 1 (queue) |
| PostgreSQL Schema | mqpay |
Liên quan
- Tài liệu tham khảo API - Tài liệu điểm cuối đầy đủ
- Webhooks & IPN - Thông báo sự kiện và xử lý IPN
- Nhà cung cấp Thanh toán - Cấu hình từng nhà cung cấp
- Gói Payment - Lớp điều phối thanh toán