Webhooks & IPN
MQ-Pay sử dụng Thông báo Thanh toán Tức thì (IPN) để nhận cập nhật trạng thái thanh toán thời gian thực từ các nhà cung cấp thanh toán.
Tổng quan
Khi một khách hàng hoàn tất thanh toán, nhà cung cấp thanh toán sẽ gửi một callback IPN đến máy chủ của bạn. MQ-Pay xử lý các thông báo này một cách tự động.
Điểm cuối IPN
VNPAY QR MMS
POST /api/mq-pay/vnpay/qr-mms/ipnVNPAY Phone POS
POST /api/mq-pay/vnpay/phone-pos/ipnVNPAY Smart POS
POST /api/mq-pay/vnpay/smart-pos/ipnHệ thống (Thủ công)
POST /api/mq-pay/system/ipnDữ liệu IPN VNPAY
interface VNPayIPNData {
vnp_TxnRef: string; // Mã attempt của bạn
vnp_TransactionNo: string; // Tham chiếu VNPAY
vnp_Amount: number; // Số tiền (x100)
vnp_ResponseCode: string; // '00' = thành công
vnp_BankCode: string; // Mã ngân hàng
vnp_PayDate: string; // YYYYMMDDHHMMSS
vnp_SecureHash: string; // Chữ ký
}Xác minh Chữ ký
MQ-Pay tự động xác minh chữ ký IPN:
function verifySignature(data: VNPayIPNData, secretKey: string): boolean {
const { vnp_SecureHash, ...params } = data;
const sortedParams = Object.keys(params)
.sort()
.map(key => `${key}=${params[key]}`)
.join('&');
const hash = crypto
.createHmac('sha512', secretKey)
.update(sortedParams)
.digest('hex')
.toUpperCase();
return hash === vnp_SecureHash;
}Trình xử lý IPN Tùy chỉnh
vnpayQrMMS: {
enable: true,
// ...
onPaymentNotification: async (ipnData, context) => {
const { attempt, transaction } = context;
if (attempt.status === '300_SUCCESS') {
// Cập nhật trạng thái đơn hàng
await saleOrderService.markAsPaid(transaction.sourceId);
// Gửi thông báo
await notificationService.send({
type: 'PAYMENT_RECEIVED',
orderId: transaction.sourceId,
amount: attempt.amount,
});
}
},
}Thông báo Sự kiện
MQ-Pay phát các sự kiện vòng đời thanh toán thông qua interface callback IPaymentEventHandler. Dịch vụ tiêu thụ triển khai interface này và truyền vào IMQPayOptions.eventHandler khi thiết lập component.
Cách Sự kiện Được Phát
- Các payment service (subclass của
BasePaymentService) vàQueueProcessorServicegọithis.emitEvent(eventType, payload) BasePaymentService.emitEvent()ủy quyền choAppRegistry.emit({ payload })— gắn thêm trườngevent,source, vàtimestampAppRegistry.emit()gọiIPaymentEventHandler.handle({ payload })trên handler đã đăng ký- Handler của dịch vụ tiêu thụ nhận
TMQPayEventPayloadđã được định kiểu và có thể dispatch theo nhu cầu (HTTP webhooks, WebSocket broadcast, v.v.)
Loại Sự kiện
Tất cả 9 loại sự kiện đều hoạt động. Các sự kiện refund được định nghĩa nhưng đang được comment trong source code.
| Sự kiện | Hằng số | Danh mục | Mô tả |
|---|---|---|---|
mq-pay:transaction.created | TRANSACTION_CREATED | Transaction | Transaction mới + attempt ban đầu được tạo |
mq-pay:transaction.settled | TRANSACTION_SETTLED | Transaction | Transaction đã thanh toán đầy đủ |
mq-pay:transaction.cancelled | TRANSACTION_CANCELLED | Transaction | Tất cả attempt bị hủy trên transaction NEW |
mq-pay:attempt.created | ATTEMPT_CREATED | Attempt | Attempt thanh toán mới được tạo |
mq-pay:attempt.sent | ATTEMPT_SENT | Attempt | Attempt đã gửi đến provider (ví dụ: mã QR khả dụng) |
mq-pay:attempt.success | ATTEMPT_SUCCESS | Attempt | Thanh toán được xác nhận qua IPN |
mq-pay:attempt.failed | ATTEMPT_FAILED | Attempt | Lời gọi provider thất bại |
mq-pay:attempt.expired | ATTEMPT_EXPIRED | Attempt | QR hết hạn, không nhận được thanh toán |
mq-pay:attempt.cancelled | ATTEMPT_CANCELLED | Attempt | Attempt bị hủy |
Kiểu Payload Sự kiện
Tất cả payload kế thừa interface cơ sở với timestamp (chuỗi ISO) và source (tên service phát sự kiện).
// Payload sự kiện Transaction (discriminated union trên trường `event`)
type TTransactionEventPayload =
| ITransactionCreatedPayload // { event, transaction, attempt }
| ITransactionSettledPayload // { event, transaction, attempt }
| ITransactionCancelledPayload // { event, transaction }
// Payload sự kiện Attempt (discriminated union trên trường `event`)
type TAttemptEventPayload =
| IAttemptCreatedPayload // { event, transaction, attempt }
| IAttemptSentPayload // { event, transaction, attempt }
| IAttemptSuccessPayload // { event, transaction, attempt }
| IAttemptFailedPayload // { event, attempt } (không có transaction)
| IAttemptExpiredPayload // { event, attempt } (không có transaction)
| IAttemptCancelledPayload // { event, attempt } (không có transaction)
// Union của tất cả payload sự kiện
type TMQPayEventPayload = TTransactionEventPayload | TAttemptEventPayload;NOTE
IAttemptFailedPayload, IAttemptExpiredPayload, và IAttemptCancelledPayload không bao gồm trường transaction. Chỉ payload creation/sent/success bao gồm cả transaction và attempt.
Nguồn Phát Sự kiện
| Nguồn | Sự kiện Được Phát |
|---|---|
PaymentExecutionService | TRANSACTION_CREATED, ATTEMPT_CREATED, ATTEMPT_SENT, ATTEMPT_FAILED |
PaymentConfirmationService | ATTEMPT_SUCCESS, TRANSACTION_SETTLED |
PaymentCancellationService | ATTEMPT_CANCELLED, TRANSACTION_CANCELLED |
QueueProcessorService | ATTEMPT_EXPIRED (scheduler), ATTEMPT_SUCCESS/ATTEMPT_FAILED (xác nhận IPN) |
Tích hợp với Dịch vụ Tiêu thụ
MQ-Pay chỉ phát sự kiện — nó không xử lý WebSocket broadcast, HTTP webhook dispatch, hay bất kỳ định tuyến downstream nào. Dịch vụ tiêu thụ (ví dụ: @nx/payment) triển khai IPaymentEventHandler để:
- Dispatch HTTP webhooks đến các endpoint đã đăng ký
- Broadcast sự kiện qua WebSocket rooms cho consumer thời gian thực (ví dụ: POS terminal)
- Kích hoạt business logic downstream (ví dụ: cập nhật đơn hàng, ghi nhận giao dịch tài chính)
Xem Payment — Webhook Dispatch để biết cách dịch vụ Payment xử lý các sự kiện này.
Ví dụ IPaymentEventHandler
import { IPaymentEventHandler, TMQPayEventPayload } from '@nx/mq-pay';
class MyEventHandler implements IPaymentEventHandler {
async handle(opts: { payload: TMQPayEventPayload }): Promise<void> {
const { payload } = opts;
console.log(`Event: ${payload.event}`);
// Dispatch đến webhook/WS/business logic của bạn
}
isReady(): boolean { return true; }
getName(): string { return 'MyEventHandler'; }
}
// Truyền vào MQ-Pay options
this.bind<IMQPayOptions>({ key: MQPayBindingKeys.MQ_PAY_CLIENT_OPTIONS })
.toValue({
eventHandler: new MyEventHandler(),
// ...provider configs
});Xử lý Thử lại
Nếu máy chủ của bạn không khả dụng, các nhà cung cấp sẽ thử lại:
| Nhà cung cấp | Số lần thử lại | Khoảng thời gian |
|---|---|---|
| VNPAY QR MMS | 5 | 5 phút |
| VNPAY Phone POS | 3 | 2 phút |
| VNPAY Smart POS | 3 | 2 phút |
Kiểm thử Webhooks
Phát triển Cục bộ
Sử dụng ngrok để công khai máy chủ cục bộ:
ngrok http 3000Cập nhật URL callback IPN trong cổng thương gia VNPAY của bạn để trỏ đến URL ngrok.
Mô phỏng IPN
curl -X POST http://localhost:3000/api/mq-pay/vnpay/qr-mms/ipn \
-H "Content-Type: application/json" \
-d '{
"vnp_TxnRef": "ATT001",
"vnp_Amount": 10000000,
"vnp_ResponseCode": "00"
}'