Bỏ qua nội dung

Nhận IPN/webhook SePay và đẩy realtime trạng thái thanh toán

Feature URL
Module
payment
Status
in-development
Priority
P0
Platforms
fe · be
AC progress
17 / 19
Last reviewed
2026-05-29

Mục tiêu

Tách riêng các public callback từ SePay thành một mục Payment độc lập: IPN checkout form và bank-transfer webhook của QR top-up. Khi callback xác nhận thanh toán thành công, backend cập nhật PaymentOrder, cộng V-Credits idempotent và đẩy realtime status tới owner qua WebSocket để màn chờ thanh toán cập nhật nhanh.

Phạm vi

Trong phạm vi (In scope):

  • Public IPN checkout endpoint POST /payments/sepay/ipn.
  • Public IPN alias POST /v1/payments/sepay/ipn.
  • Public bank-transfer webhook endpoint POST /payments/sepay/webhook.
  • Public webhook alias POST /v1/payments/sepay/webhook.
  • Verify IPN secret qua header X-Secret-Key.
  • Verify bank-transfer webhook bằng SEPAY_WEBHOOK_AUTH_MODE: none, api_key, hoặc hmac.
  • Match payment order bằng invoice/order id của IPN hoặc payload.code của webhook.
  • Validate provider status, transaction type và amount trước khi confirm order.
  • Publish PaymentOrderConfirmedEvent khi order chuyển SUCCESS.
  • Push PaymentOrderRealtimePayload tới owner qua /user/queue/payment-orders sau transaction commit.

Ngoài phạm vi (Out of scope):

  • Tạo checkout top-up, xem feature wallet-topup-create.
  • Tạo QR top-up và nội dung chuyển khoản, xem feature wallet-topup-sepay-qr-webhook.
  • API user chủ động sync detail, xem feature payment-order-detail-sync.
  • Hạ tầng STOMP/WebSocket nền tảng, xem system doc WebSocket / STOMP.
  • UI ví/nạp tiền hoàn chỉnh trên frontend.

User Stories

  • hệ thống payment, tôi muốn xử lý IPN/webhook từ SePay để xác nhận order đã thanh toán.
  • user đang chờ nạp tiền, tôi muốn màn hình nhận realtime status khi thanh toán thành công thay vì phải polling liên tục.
  • hệ thống ví, tôi muốn chỉ cộng V-Credits sau khi callback match đúng order và amount.
  • ops/support, tôi muốn callback lặp hoặc callback không match order không cộng ví sai và vẫn ACK đúng cho provider khi phù hợp.

Luồng chức năng

sequenceDiagram
    actor User
    participant App as FE/Mobile
    participant SePay
    participant BE
    participant Wallet
    participant WS as WebSocket

    Note over User,SePay: IPN checkout form
    User->>SePay: Hoàn tất checkout
    SePay->>BE: POST /payments/sepay/ipn
    BE->>BE: Verify X-Secret-Key + invoice number
    BE->>SePay: Query order detail
    BE->>BE: Validate invoice, amount, status CAPTURED
    BE->>Wallet: Confirm payment order, cộng V-Credits idempotent
    BE->>BE: Publish PaymentOrderConfirmedEvent
    WS-->>App: MESSAGE /user/queue/payment-orders
    BE-->>SePay: {"success": true}

    Note over User,SePay: Bank-transfer QR webhook
    User->>SePay: Chuyển khoản QR đúng nội dung
    SePay->>BE: POST /payments/sepay/webhook
    BE->>BE: Verify HMAC/API key theo config
    BE->>BE: Match payload.code với providerOrderId
    BE->>BE: Validate transferType=in + amount khớp
    BE->>Wallet: Confirm payment order, cộng V-Credits idempotent
    BE->>BE: Publish PaymentOrderConfirmedEvent
    WS-->>App: MESSAGE /user/queue/payment-orders
    BE-->>SePay: {"success": true}

Acceptance Criteria

  • AC-1: Backend có POST /api/payments/sepay/ipnPOST /api/v1/payments/sepay/ipn.
  • AC-2: IPN verify secret header X-Secret-Key qua provider service.
  • AC-3: IPN phải lấy được invoice number; thiếu invoice number trả lỗi domain phù hợp.
  • AC-4: IPN tìm PaymentOrder theo provider SEPAYproviderOrderId = invoiceNumber.
  • AC-5: IPN query provider detail, validate invoice number và amount trước khi confirm.
  • AC-6: IPN chỉ confirm khi provider status là CAPTURED; CANCELLED/CANCELED sync sang cancel.
  • AC-7: Backend có POST /api/payments/sepay/webhookPOST /api/v1/payments/sepay/webhook.
  • AC-8: Bank-transfer webhook verify request theo mode none, api_key, hoặc hmac.
  • AC-9: Webhook match order bằng payload.code == PaymentOrder.providerOrderId.
  • AC-10: Webhook code rỗng hoặc không match order không được cộng credit.
  • AC-11: Webhook chỉ xử lý giao dịch transferType=in.
  • AC-12: Webhook transferAmount phải bằng PaymentOrder.amount; mismatch không cộng credit.
  • AC-13: Callback lặp trên order đã SUCCESS không cộng credit lần hai.
  • AC-14: Khi callback hợp lệ, backend set order SUCCESS, lưu provider transaction id/payload và tạo wallet ledger idempotent.
  • AC-15: Khi order chuyển SUCCESS, backend publish PaymentOrderConfirmedEvent.
  • AC-16: PaymentOrderRealtimeListener push WebSocket sau transaction commit.
  • AC-17: Client nhận realtime tại /user/queue/payment-orders với PaymentOrderRealtimePayload.
  • AC-18: FE/Mobile cần subscribe /user/queue/payment-orders ở màn chờ thanh toán và reconcile bằng REST order/wallet API khi nhận event.
  • AC-19: FE/Mobile cần fallback polling khi WebSocket mất kết nối hoặc reconnect.

Quy tắc nghiệp vụ

  • Callback từ SePay là server-to-server, không yêu cầu Bearer token.
  • IPN checkout và bank-transfer webhook cùng dùng confirm path; ví chỉ được cộng khi order local còn xử lý được.
  • PaymentOrder.providerOrderId là khóa đối soát chính với SePay.
  • Cộng V-Credits phải idempotent theo payment order.
  • WebSocket chỉ là tín hiệu realtime sau commit; DB/REST API vẫn là nguồn sự thật.
  • Client không được tự cộng số dư chỉ dựa trên WebSocket payload; phải reconcile order/wallet state bằng REST.
  • Local/dev có thể tắt webhook auth bằng SEPAY_WEBHOOK_AUTH_MODE=none; test/prod nên dùng HMAC hoặc API key.

Dữ liệu & Trạng thái

Entity nghiệp vụ:

  • PaymentOrder: order local chứa owner, amount, providerOrderId, status, paidAt, ipnData, histories.
  • PaymentProviderOrderState: snapshot provider detail dùng cho IPN checkout.
  • SepayWebhookPayload: bank-transfer webhook payload gồm code, transferType, transferAmount, transaction id.
  • WalletTransaction: ledger ví được tạo khi order confirm thành công.
  • PaymentOrderConfirmedEvent: event sau khi order được xác nhận thanh toán.
  • PaymentOrderRealtimePayload: payload realtime gửi tới owner.

Trạng thái user-facing:

  • CREATED — order đang chờ callback hoặc detail sync.
  • SUCCESS — callback hợp lệ đã xác nhận thanh toán và ví đã cộng credit.
  • REALTIME_NOTIFIED — client đã nhận WebSocket event và nên refresh order/wallet.
  • CANCELLED — provider hoặc user đã huỷ.
  • EXPIRED — order quá hạn, không nên xử lý tự động.
  • FAILED — order lỗi, không cộng credit.

Endpoint BE hiện có:

  • POST /api/payments/sepay/ipn
  • POST /api/v1/payments/sepay/ipn
  • POST /api/payments/sepay/webhook
  • POST /api/v1/payments/sepay/webhook
  • WebSocket destination client subscribe: /user/queue/payment-orders

Liên quan