MQ-Pay Integration
MQ-Pay (Multi-provider Quick Payment System) is a payment integration component that provides a unified API for connecting multiple payment providers.
Overview
| Attribute | Value |
|---|---|
| Package | @nx/mq-pay |
| Status | Production |
| Providers | VNPAY QR MMS, VNPAY Phone POS, VNPAY Smart POS, System (Cash/Bank Transfer) |
Deployment Modes
MQ-Pay supports mode-based deployment via APP_ENV_MQ_PAY_MODE for independent scaling of API and worker components:
| Mode | Controllers | Queue Producers | BullMQ Workers | Event Handler | Use case |
|---|---|---|---|---|---|
full | Yes | Yes | Yes | Yes | Default — single instance handles everything |
api | Yes | Yes | No | No | REST API only — enqueues jobs, no processing |
worker | No | Yes | Yes | Yes | BullMQ workers only — processes jobs, no HTTP |
Production deployment pattern: 1 API instance (mode=api, Snowflake ID=8) + N worker instances (mode=worker, unique Snowflake IDs 91, 92, ...).
Key Features
- Multi-provider - Support multiple payment gateways in one place
- Quick integration - Simple setup, start accepting payments in minutes
- Unified API - Same interface for all providers
- Background processing - BullMQ queues for reliable payment processing
- Event notifications - Callback-based event system for payment lifecycle events
Quick Start
1. Configure your application
// application.ts
import { MQPayComponent, MQPayBindingKeys, IMQPayOptions } from '@nx/mq-pay';
class MyApplication extends BaseApplication {
preConfigure() {
// Configure payment providers
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',
},
});
// Load MQ-Pay component
this.component(MQPayComponent);
}
}2. Set environment variables
# Deployment Mode (full | api | worker)
APP_ENV_MQ_PAY_MODE=full
# Database (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 (for queues)
APP_ENV_MQ_PAY_REDIS_QUEUE_HOST=localhost
APP_ENV_MQ_PAY_REDIS_QUEUE_PORT=63793. Done!
MQ-Pay will automatically:
- Connect to your database and Redis
- Register payment providers you enabled
- Set up background job queues for payment processing
- Register IPN/webhook endpoints (if
enableController: true)
Supported Providers
| Provider | Status | Description |
|---|---|---|
| SYSTEM | Available | Cash, Manual Bank Transfer |
| VNPAY_QR_MMS | Available | VNPAY Dynamic QR Code Payment |
| VNPAY_PHONE_POS | Available | VNPAY Phone POS Integration |
| VNPAY_SMART_POS | Available | VNPAY Smart POS Terminal |
| MOMO | Coming Soon | - |
| ZaloPay | Coming Soon | - |
Architecture
Component Initialization Flow
When MQPayComponent.binding() is called, the following initialization sequence occurs:
Domain Model
Transaction - One order → one transaction
- Tracks total amount (
net,tax,fee,discount,total,paid) and status uidis the human-readable unique identifier (notnumber)- Unique constraint prevents duplicate active transactions per source
TransactionItem - Line items snapshot at payment time
- Captures item pricing at the moment of transaction creation
PaymentAttempt - Each payment method creates an attempt
- Links to transaction, tracks provider and method used
remainingis auto-calculated by PostgreSQL (amount - tendered)parentIdlinks cancel/refund attempts to the original make-payment attempt
PaymentResult - Provider response/callback data
- Stores raw IPN/webhook payloads with deduplication via
payloadHash - Unique index on
(paymentAttemptId, type, payloadHash)prevents duplicate processing
AppRegistry Pattern
MQ-Pay uses a singleton registry (AppRegistry) separate from the IGNIS DI container to manage runtime resources. This is necessary because BullMQ workers and Redis connections have lifecycles that don't fit the request-scoped DI model.
| Registry | Prefix | Purpose |
|---|---|---|
| Redis | redis | Connection pool for BullMQ queues |
| BullMQ | bullmq | Queue and worker instances |
| Payment Providers | payment-provider | VNPAY, System, etc. |
| Event Handler | (singleton) | IPaymentEventHandler for event dispatch |
IMPORTANT
AppRegistry cannot use @inject() decorators. Always access via AppRegistry.getInstance().
Queue System
MQ-Pay uses BullMQ for reliable background job processing. Jobs are distributed across partitions for load balancing.
| Queue Type | Partitions | Job Type | Purpose |
|---|---|---|---|
scheduler | P01, P02, P03 | PAYMENT_EXPIRATION | Expire unpaid QR codes after timeout |
confirmation | P01, P02, P03 | PROCESS_IPN | Process IPN/webhook callbacks from providers |
Total per instance: full/worker mode: 6 queues + 6 workers. api mode: 6 queue producers only (no workers). 3 partitions per type, 10 concurrent jobs per worker by default.
Partition Selection: Jobs are assigned to partitions using a hash of the transactionId, ensuring all jobs for the same transaction go to the same partition.
Status Lifecycles
Transaction Status Lifecycle
NOTE
SETTLED is a terminal state — once a transaction is fully paid, it cannot be modified. Refunds create new REFUND_PAYMENT attempts but do not change the transaction status.
PaymentAttempt Status Lifecycle
Payment Flows
QR Code Payment Flow
Cash Payment Flow
Split Payment Flow
IPN Processing Flow
Cancel Payment Flow
Refund Payment Flow
API Reference
Create Payment
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
}
}Response:
{
"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"
}
}Confirm Cash Payment
POST /api/payments/system/ipn
Content-Type: application/json
{
"attempt": { "id": "att-uuid-001" },
"tendered": 100000,
"note": "Cash payment received"
}Response:
{
"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"
}
}Continue Partial Payment
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"
}
}Provider Configuration
VNPAY QR MMS
vnpayQrMMS: {
enable: true,
isDefault: true,
enableController: true,
appId: 'your-app-id',
secretKey: 'your-secret-key',
masterMerchantCode: 'your-merchant-code',
isProduction: false,
// Optional callback
onPaymentNotification: async (ipnData) => {
console.log('Payment received:', ipnData);
// Trigger your business logic
},
}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,
}System (Cash / Bank Transfer)
No configuration needed - always available.
Status Reference
Transaction Statuses
| Status | Code | Description |
|---|---|---|
NEW | 100_NEW | Created, no payment yet |
PARTIAL | 300_PARTIAL | Partially paid |
SETTLED | 304_SETTLED | Fully paid (terminal, immutable) |
CANCELLED | 505_CANCELLED | Auto-cancelled when all attempts cancelled |
BLOCKED | 403_BLOCKED | Temporarily blocked by admin |
CLOSED | 404_CLOSED | Permanently closed by admin |
PaymentAttempt Statuses
| Status | Code | Description |
|---|---|---|
NEW | 100_NEW | Created, not sent to provider |
SENT | 204_SENT | Sent to provider, QR available |
SUCCESS | 302_SUCCESS | Payment confirmed via IPN |
FAIL | 500_FAIL | Provider call failed |
EXPIRED | 501_EXPIRED | QR expired, no payment received |
PaymentAttempt Types
| Type | Code | Description |
|---|---|---|
MAKE_PAYMENT | 100_MAKE_PAYMENT | Payment attempt |
CANCEL_PAYMENT | 200_CANCEL_PAYMENT | Cancellation attempt (links to original via parentId) |
REFUND_PAYMENT | 300_REFUND_PAYMENT | Refund attempt (links to original via parentId) |
Payment Methods
| Method | Code | Description |
|---|---|---|
CASH | 000_CASH | Cash payment |
QR_CODE | 100_QR_CODE | QR code payment |
BANK_TRANSFER | 200_BANK_TRANSFER | Bank transfer |
E_WALLET | 300_E_WALLET | E-wallet payment |
CARD | 400_CARD | Card payment |
Services
PaymentExecutionService
Source:
services/payment/execution.service.ts| Extends:BasePaymentService
DI Dependencies: TransactionRepository, TransactionItemRepository, PaymentAttemptRepository, QueueProcessorService
| Method | Signature | Description |
|---|---|---|
makePayment | (opts: { payload: TMakePaymentRequest }) → Promise<TMakePaymentResponse> | Create transaction + attempt, call provider or prepare manual payment |
Handles both new transactions and adding attempts to existing transactions (split payment). Validates item totals, calculates expiration, and adds scheduler jobs for third-party providers.
PaymentCancellationService
Source:
services/payment/cancellation.service.ts| Extends:BasePaymentService
DI Dependencies: TransactionRepository, PaymentAttemptRepository, QueueProcessorService
| Method | Signature | Description |
|---|---|---|
cancelPayment | (opts: { payload: TCancelPaymentRequest }) → Promise<TCancelPaymentResponse> | Cancel specific attempt or all attempts for a transaction |
Creates CANCEL_PAYMENT child attempts linked via parentId. For third-party providers, calls provider.cancelPayment(). Removes scheduler expiration jobs.
PaymentConfirmationService
Source:
services/payment/confirmation.service.ts| Extends:BasePaymentService
DI Dependencies: TransactionRepository, PaymentAttemptRepository, PaymentResultRepository
| Method | Signature | Description |
|---|---|---|
confirmPayment | (opts: TConfirmPaymentRequest) → Promise<TConfirmPaymentResponse> | Confirm manual payment (CASH/BANK_TRANSFER) received |
Only works for SYSTEM provider with CASH or BANK_TRANSFER methods. Updates attempt to SUCCESS, calculates if transaction is SETTLED or PARTIAL.
PaymentRefundService
Source:
services/payment/refund.service.ts| Extends:BasePaymentService
DI Dependencies: PaymentAttemptRepository
| Method | Signature | Description |
|---|---|---|
refund | (opts: TRefundRequest) → Promise<TRefundResponse> | Refund a successful MAKE_PAYMENT attempt |
Validates the original attempt is SUCCESS, checks total already refunded via getTotalRefundedAmount(), ensures refund amount does not exceed remaining.
PaymentVerificationService
Source:
services/payment/verification.service.ts| Extends:BasePaymentService
DI Dependencies: TransactionRepository, PaymentAttemptRepository, PaymentResultRepository
| Method | Signature | Description |
|---|---|---|
verifyAttempt | (opts: TCheckTransactionRequest) → Promise<TCheckTransactionResponse> | Verify payment attempt status against provider |
For third-party providers, calls provider.checkTransaction(). For manual payments, returns local status.
QueueProcessorService
Source:
services/queue-processor.service.ts| Extends:BaseService
DI Dependencies: PaymentAttemptRepository, TransactionRepository, PaymentResultRepository
| Method | Signature | Description |
|---|---|---|
initialize | () → void | Create all queues and workers — calls initializeQueues() + initializeWorkers() |
initializeQueues | () → void | Create 6 queue producer instances only (used by api mode) |
initializeWorkers | () → void | Create 6 worker instances only (used internally by initialize()) |
addSchedulerJob | (opts: { data, jobOptions? }) → Promise<{ queue, job } | null> | Add payment expiration job |
removeSchedulerJob | (opts: { transactionId, jobId? }) → Promise<void> | Remove expiration job |
addConfirmationJob | (opts: { data, jobOptions? }) → Promise<Job> | Add IPN processing job |
createSchedulerWorkers | (opts?: { handler?, concurrency? }) → BullMQHelper[] | Create scheduler partition workers |
createConfirmationWorkers | (opts?: { handler?, concurrency? }) → BullMQHelper[] | Create confirmation partition workers |
getQueue | (opts: { type, partition }) → BullMQHelper | Get queue by type and partition |
getQueueByKey | (opts: { type, partitionKey }) → BullMQHelper | Get queue by partition key (auto-resolved) |
Repositories
| Repository | Entity | Base Class | Custom Methods |
|---|---|---|---|
TransactionRepository | Transaction | DefaultCRUDRepository | getTotalPaidAmount({ transactionId }) — Sum of tendered from SUCCESS MAKE_PAYMENT attempts |
PaymentAttemptRepository | PaymentAttempt | DefaultCRUDRepository | isAttemptCancelled({ attemptId }) — Check if cancelled via child attempt |
findActiveAttempts({ transactionId }) — Find NEW/SENT attempts not cancelled (CTE query) | |||
getTotalRefundedAmount({ parentAttemptId }) — Sum of SUCCESS refund amounts | |||
resolveAttemptForIPN({ attemptId }) — Get attempt + transaction + cancellation status in one query | |||
PaymentResultRepository | PaymentResult | DefaultCRUDRepository | (CRUD only) |
TransactionItemRepository | TransactionItem | DefaultCRUDRepository | (CRUD only) |
NOTE
All repositories use the mqpay PostgreSQL schema (configurable via APP_ENV_MQ_PAY_SCHEMA).
Error Handling
Common Errors
| Error | Cause | Solution |
|---|---|---|
Invalid transaction status | Adding payment to completed transaction | Check transaction status first |
Only CASH/BANK_TRANSFER can be confirmed | Confirming QR payment via system endpoint | Use provider's IPN |
PaymentAttempt is not in NEW status | Double confirmation | Check attempt status first |
Missing binding key | Component not configured | Bind options before loading component |
Refund amount exceeds remaining | Refund > (original - already refunded) | Check getTotalRefundedAmount() first |
Environment Variables Reference
Database (Required)
| Variable | Description | Default |
|---|---|---|
APP_ENV_MQ_PAY_POSTGRES_HOST | Database host | - |
APP_ENV_MQ_PAY_POSTGRES_PORT | Database port | 5432 |
APP_ENV_MQ_PAY_POSTGRES_DATABASE | Database name | - |
APP_ENV_MQ_PAY_POSTGRES_USERNAME | Database user | - |
APP_ENV_MQ_PAY_POSTGRES_PASSWORD | Database password | - |
APP_ENV_MQ_PAY_SCHEMA | PostgreSQL schema name | mqpay |
Redis Queue (Required)
| Variable | Description | Default |
|---|---|---|
APP_ENV_MQ_PAY_REDIS_QUEUE_HOST | Redis host (single mode) | localhost |
APP_ENV_MQ_PAY_REDIS_QUEUE_PORT | Redis port | 6379 |
APP_ENV_MQ_PAY_REDIS_QUEUE_PASSWORD | Redis password | - |
APP_ENV_MQ_PAY_REDIS_QUEUE_MODE | single or cluster | single |
APP_ENV_MQ_PAY_REDIS_QUEUE_MAX_RETRY | Max connection retries | - |
APP_ENV_MQ_PAY_REDIS_QUEUE_CLUSTER_NODES | Comma-separated host:port (cluster mode) | - |
Snowflake ID
| Variable | Description | Default |
|---|---|---|
APP_ENV_MQ_PAY_WORKER_ID | Snowflake worker ID | 0 |
APP_ENV_MQ_PAY_EPOCH_CHECKPOINT | Snowflake epoch timestamp | 1735689600000 |
Deployment Mode
| Variable | Description | Default |
|---|---|---|
APP_ENV_MQ_PAY_MODE | Deployment mode: full, api, or worker | full |
full— Registers controllers, creates queue producers and BullMQ workers, binds event handler. Default for development and simple deployments.api— Registers controllers and queue producers only. No workers, no event handler. Use for scaling API independently.worker— Creates queue producers, BullMQ workers, and binds event handler. No controllers. Use for scaling job processing independently.
Code Statistics
| Metric | Count |
|---|---|
| Source Files | 25+ TypeScript files |
| Entities | 4 (Transaction, TransactionItem, PaymentAttempt, PaymentResult) |
| Repositories | 4 (all with DefaultCRUDRepository) |
| Services | 7 (6 payment + QueueProcessor) |
| Controllers | 5 (PaymentController + 4 CRUD) |
| REST Endpoints | 5 custom + 4×CRUD |
| Payment Providers | 4 (System, VNPAY QR MMS, Phone POS, Smart POS) |
| BullMQ Queues | 6 (2 types × 3 partitions) |
| Redis Connections | 1 (queue) |
| PostgreSQL Schema | mqpay |
Related
- API Reference - Complete endpoint documentation
- Webhooks & IPN - Event notifications and IPN handling
- Payment Providers - Provider-specific configuration
- Payment Package - Payment orchestration layer