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ặchmac. - Match payment order bằng invoice/order id của IPN hoặc
payload.codecủa webhook. - Validate provider status, transaction type và amount trước khi confirm order.
- Publish
PaymentOrderConfirmedEventkhi order chuyểnSUCCESS. - Push
PaymentOrderRealtimePayloadtới owner qua/user/queue/payment-orderssau 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
- Là hệ thống payment, tôi muốn xử lý IPN/webhook từ SePay để xác nhận order đã thanh toán.
- Là 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.
- Là hệ thống ví, tôi muốn chỉ cộng V-Credits sau khi callback match đúng order và amount.
- Là 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/ipnvàPOST /api/v1/payments/sepay/ipn. - AC-2: IPN verify secret header
X-Secret-Keyqua 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
PaymentOrdertheo providerSEPAYvàproviderOrderId = 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/CANCELEDsync sang cancel. - AC-7: Backend có
POST /api/payments/sepay/webhookvàPOST /api/v1/payments/sepay/webhook. - AC-8: Bank-transfer webhook verify request theo mode
none,api_key, hoặchmac. - AC-9: Webhook match order bằng
payload.code == PaymentOrder.providerOrderId. - AC-10: Webhook
coderỗ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
transferAmountphải bằngPaymentOrder.amount; mismatch không cộng credit. - AC-13: Callback lặp trên order đã
SUCCESSkhô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 publishPaymentOrderConfirmedEvent. - AC-16:
PaymentOrderRealtimeListenerpush WebSocket sau transaction commit. - AC-17: Client nhận realtime tại
/user/queue/payment-ordersvớiPaymentOrderRealtimePayload. - 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.providerOrderIdlà 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ồmcode,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/ipnPOST /api/v1/payments/sepay/ipnPOST /api/payments/sepay/webhookPOST /api/v1/payments/sepay/webhook- WebSocket destination client subscribe:
/user/queue/payment-orders