Skip to content

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ínhGiá trị
Gói@nx/mq-pay
Trạng tháiProduction
Nhà cung cấpVNPAY 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ế độControllersQueue ProducersBullMQ WorkersEvent HandlerTrường hợp sử dụng
fullMặc định — một instance xử lý tất cả
apiKhôngKhôngChỉ REST API — đẩy job vào hàng đợi, không xử lý
workerKhôngChỉ 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

typescript
// 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

bash
# 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=6379

3. 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ấpTrạng tháiMô tả
SYSTEMCó sẵnTiền mặt, Chuyển khoản Thủ công
VNPAY_QR_MMSCó sẵnThanh toán Mã QR Động VNPAY
VNPAY_PHONE_POSCó sẵnTích hợp VNPAY Phone POS
VNPAY_SMART_POSCó sẵnTerminal VNPAY Smart POS
MOMOSắp ra mắt-
ZaloPaySắ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
  • uid là mã định danh duy nhất đọc được (không phải number)
  • 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)
  • parentId liê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.

RegistryTiền tốMục đích
RedisredisPool kết nối cho hàng đợi BullMQ
BullMQbullmqInstance queue và worker
Payment Providerspayment-providerVNPAY, 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 đợiPartitionsLoại JobMục đích
schedulerP01, P02, P03PAYMENT_EXPIRATIONHết hạn mã QR chưa thanh toán sau timeout
confirmationP01, P02, P03PROCESS_IPNXử 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

SETTLEDtrạ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

http
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:

json
{
  "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

http
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:

json
{
  "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

http
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

typescript
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

typescript
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áiMô tả
NEW100_NEWĐã tạo, chưa thanh toán
PARTIAL300_PARTIALThanh toán một phần
SETTLED304_SETTLEDĐã thanh toán đầy đủ (cuối cùng, không thay đổi)
CANCELLED505_CANCELLEDTự động hủy khi tất cả attempts bị hủy
BLOCKED403_BLOCKEDBị chặn tạm thời bởi admin
CLOSED404_CLOSEDĐã đóng vĩnh viễn bởi admin

Trạng thái Lần thử Thanh toán

Trạng tháiMô tả
NEW100_NEWĐã tạo, chưa gửi đến nhà cung cấp
SENT204_SENTĐã gửi đến nhà cung cấp, QR có sẵn
SUCCESS302_SUCCESSThanh toán đã xác nhận qua IPN
FAIL500_FAILGọi nhà cung cấp thất bại
EXPIRED501_EXPIREDQR hết hạn, không nhận được thanh toán

Loại Lần thử Thanh toán

LoạiMô tả
MAKE_PAYMENT100_MAKE_PAYMENTLần thử thanh toán
CANCEL_PAYMENT200_CANCEL_PAYMENTLần thử hủy (liên kết với gốc qua parentId)
REFUND_PAYMENT300_REFUND_PAYMENTLầ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ứcMô tả
CASH000_CASHThanh toán tiền mặt
QR_CODE100_QR_CODEThanh toán mã QR
BANK_TRANSFER200_BANK_TRANSFERChuyển khoản ngân hàng
E_WALLET300_E_WALLETThanh toán ví điện tử
CARD400_CARDThanh toán thẻ

Dịch vụ

PaymentExecutionService

Source: services/payment/execution.service.ts | Extends: BasePaymentService

DI Dependencies: TransactionRepository, TransactionItemRepository, PaymentAttemptRepository, QueueProcessorService

Phương thứcChữ 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ứcChữ 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ứcChữ 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ứcChữ 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ứcChữ 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ứcChữ kýMô tả
initialize() → voidTạo tất cả queues và workers — gọi initializeQueues() + initializeWorkers()
initializeQueues() → voidTạo riêng 6 queue producer instances (dùng bởi chế độ api)
initializeWorkers() → voidTạ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 }) → BullMQHelperLấy queue theo loại và partition
getQueueByKey(opts: { type, partitionKey }) → BullMQHelperLấy queue theo partition key (tự động phân giải)

Repositories

RepositoryEntityBase ClassPhương thức Tùy chỉnh
TransactionRepositoryTransactionDefaultCRUDRepositorygetTotalPaidAmount({ transactionId }) — Tổng tendered từ các SUCCESS MAKE_PAYMENT attempts
PaymentAttemptRepositoryPaymentAttemptDefaultCRUDRepositoryisAttemptCancelled({ 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
PaymentResultRepositoryPaymentResultDefaultCRUDRepository(Chỉ CRUD)
TransactionItemRepositoryTransactionItemDefaultCRUDRepository(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ỗiNguyên nhânGiải pháp
Invalid transaction statusThêm thanh toán vào giao dịch đã hoàn tấtKiểm tra trạng thái giao dịch trước
Only CASH/BANK_TRANSFER can be confirmedXác nhận thanh toán QR qua endpoint hệ thốngSử dụng IPN của nhà cung cấp
PaymentAttempt is not in NEW statusXác nhận képKiểm tra trạng thái lần thử trước
Missing binding keyThành phần chưa được cấu hìnhBind tùy chọn trước khi tải thành phần
Refund amount exceeds remainingHoà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ếnMô tảMặc định
APP_ENV_MQ_PAY_POSTGRES_HOSTHost cơ sở dữ liệu-
APP_ENV_MQ_PAY_POSTGRES_PORTCổng cơ sở dữ liệu5432
APP_ENV_MQ_PAY_POSTGRES_DATABASETên cơ sở dữ liệu-
APP_ENV_MQ_PAY_POSTGRES_USERNAMENgười dùng cơ sở dữ liệu-
APP_ENV_MQ_PAY_POSTGRES_PASSWORDMật khẩu cơ sở dữ liệu-
APP_ENV_MQ_PAY_SCHEMATên PostgreSQL schemamqpay

Redis Queue (Bắt buộc)

BiếnMô tảMặc định
APP_ENV_MQ_PAY_REDIS_QUEUE_HOSTHost Redis (chế độ single)localhost
APP_ENV_MQ_PAY_REDIS_QUEUE_PORTCổng Redis6379
APP_ENV_MQ_PAY_REDIS_QUEUE_PASSWORDMật khẩu Redis-
APP_ENV_MQ_PAY_REDIS_QUEUE_MODEsingle hoặc clustersingle
APP_ENV_MQ_PAY_REDIS_QUEUE_MAX_RETRYSố lần thử lại kết nối tối đa-
APP_ENV_MQ_PAY_REDIS_QUEUE_CLUSTER_NODESCác cặp host:port phân cách bằng dấu phẩy (chế độ cluster)-

Snowflake ID

BiếnMô tảMặc định
APP_ENV_MQ_PAY_WORKER_IDSnowflake worker ID0
APP_ENV_MQ_PAY_EPOCH_CHECKPOINTSnowflake epoch timestamp1735689600000

Chế độ Triển khai

BiếnMô tảMặc định
APP_ENV_MQ_PAY_MODEChế độ triển khai: full, api, hoặc workerfull
  • 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 Files25+ TypeScript files
Entities4 (Transaction, TransactionItem, PaymentAttempt, PaymentResult)
Repositories4 (tất cả với DefaultCRUDRepository)
Services7 (6 payment + QueueProcessor)
Controllers5 (PaymentController + 4 CRUD)
REST Endpoints5 tùy chỉnh + 4×CRUD
Payment Providers4 (System, VNPAY QR MMS, Phone POS, Smart POS)
BullMQ Queues6 (2 loại × 3 partitions)
Redis Connections1 (queue)
PostgreSQL Schemamqpay

Liên quan

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.