Skip to content

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

VNPAY Phone POS

POST /api/mq-pay/vnpay/phone-pos/ipn

VNPAY Smart POS

POST /api/mq-pay/vnpay/smart-pos/ipn

Hệ thống (Thủ công)

POST /api/mq-pay/system/ipn

Dữ liệu IPN VNPAY

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

typescript
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

typescript
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

  1. Các payment service (subclass của BasePaymentService) và QueueProcessorService gọi this.emitEvent(eventType, payload)
  2. BasePaymentService.emitEvent() ủy quyền cho AppRegistry.emit({ payload }) — gắn thêm trường event, source, và timestamp
  3. AppRegistry.emit() gọi IPaymentEventHandler.handle({ payload }) trên handler đã đăng ký
  4. 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ệnHằng sốDanh mụcMô tả
mq-pay:transaction.createdTRANSACTION_CREATEDTransactionTransaction mới + attempt ban đầu được tạo
mq-pay:transaction.settledTRANSACTION_SETTLEDTransactionTransaction đã thanh toán đầy đủ
mq-pay:transaction.cancelledTRANSACTION_CANCELLEDTransactionTất cả attempt bị hủy trên transaction NEW
mq-pay:attempt.createdATTEMPT_CREATEDAttemptAttempt thanh toán mới được tạo
mq-pay:attempt.sentATTEMPT_SENTAttemptAttempt đã gửi đến provider (ví dụ: mã QR khả dụng)
mq-pay:attempt.successATTEMPT_SUCCESSAttemptThanh toán được xác nhận qua IPN
mq-pay:attempt.failedATTEMPT_FAILEDAttemptLời gọi provider thất bại
mq-pay:attempt.expiredATTEMPT_EXPIREDAttemptQR hết hạn, không nhận được thanh toán
mq-pay:attempt.cancelledATTEMPT_CANCELLEDAttemptAttempt 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).

typescript
// 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ả transactionattempt.

Nguồn Phát Sự kiện

NguồnSự kiện Được Phát
PaymentExecutionServiceTRANSACTION_CREATED, ATTEMPT_CREATED, ATTEMPT_SENT, ATTEMPT_FAILED
PaymentConfirmationServiceATTEMPT_SUCCESS, TRANSACTION_SETTLED
PaymentCancellationServiceATTEMPT_CANCELLED, TRANSACTION_CANCELLED
QueueProcessorServiceATTEMPT_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

typescript
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ấpSố lần thử lạiKhoảng thời gian
VNPAY QR MMS55 phút
VNPAY Phone POS32 phút
VNPAY Smart POS32 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ộ:

bash
ngrok http 3000

Cậ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

bash
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"
  }'

Liên quan

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