Hợp đồng IPN VNPAY
Chuẩn & kiểm chứng theo code
Nguồn-sự-thật-duy-nhất cho hợp đồng thanh toán BANA ↔ VNPAY, kiểm chứng theo third-parties/mq-pay và packages/{payment,sale}. Nếu các trang integrations/mq-pay hay trang khác mâu thuẫn, trang này thắng.
mq-pay không phải service riêng - nó là component (MQPayComponent) bên trong service payment. Production chỉ wire QR-MMS và PhonePOS; Smart POS có trong code nhưng CHƯA provision (xem Vấn đề đã biết).
Luồng - hai chặng
- VNPAY → service payment. VNPAY POST IPN tới endpoint của provider; mq-pay verify checksum, đẩy job confirmation, và trả ack
{ code, message }(HTTP 200). - service payment → sale. Khi có event kết thúc, webhook dispatcher POST
{ eventType, timestamp, payload }tới/webhooks/paymentcủa sale.
Chặng 1 - VNPAY → payment (endpoint IPN)
Đường dẫn tuyệt đối = APP_ENV_SERVER_BASE_PATH (quy ước /v1/api/payment) + path tương đối bên dưới. Đây là URL đăng ký trên cổng merchant VNPAY.
| Provider | Endpoint IPN | Outbound base (TEST / PROD) | Trạng thái |
|---|---|---|---|
| QR-MMS | POST /v1/api/payment/payments/vnpay/qr-mms/ipn | doitac-tran.vnpaytest.vn / doitac-tran.vnpay.vn | ✅ wired |
| PhonePOS | POST /v1/api/payment/payments/vnpay/phone-pos/ipn | - (SDK khởi tạo; chỉ nhận IPN) | ✅ wired |
| Smart POS | POST /v1/api/payment/payments/vnpay/smart-pos/ipn | spos-api.vnpaytest.vn / spos-api.vnpay.vn | ⚠️ chưa provision |
Chữ ký (checksum) từng provider
Checksum đi trong field body checksum (không phải header), thuật toán khác nhau theo provider:
| Provider | Thuật toán | Nguồn secret | Verify IPN vào? |
|---|---|---|---|
| QR-MMS | MD5 - nối secret vào chuỗi data pipe-join (không phải HMAC thật) | per-merchant (credential getter) | ✅ có |
| PhonePOS | HMAC-SHA256, Base64 | per-merchant (credential getter) | ✅ có - thiếu checksum bị từ chối |
| Smart POS | HMAC-SHA256, hex (in hoa) | static config (secretKey.ipn) | ⚠️ yếu - chấp nhận khi thiếu checksum (stub test) |
QR-MMS chuỗi ký IPN (verify):
code | msgType | txnId | qrTrace | bankCode | mobile | accountNo | amount | payDate | merchantCode | secretKey→ MD5 hex, so sánh không phân biệt hoa thường. PhonePOS chuỗi ký IPN:secret + (merchantMethodCode | orderCode | amount | "" | responseCode)→ HMAC-SHA256 Base64.
Ack trả về VNPAY
Không phải { RspCode, Message } cổ điển. mq-pay luôn trả HTTP 200 với { code, message, data? }:
- QR-MMS thành công:
{ "code": "00", "message": "Success", "data": { "txnId": "…" } }- auth-fail06, đã thanh toán03, sai amount07. - PhonePOS / Smart POS thành công:
{ "code": "200", "message": "…" }- sai checksum410, đã xử lý412.
Chặng 2 - payment → sale (webhook nội bộ)
- Endpoint:
POST /webhooks/payment(service sale). - Body:
{ eventType: string, timestamp: number, payload: { transaction?, attempt?, source?, timestamp } }.transaction={ id, uid, status, total, paid, sourceType?, sourceId?, metadata? }attempt={ id, uid, status, amount, paymentProvider, reason?, metadata? }
- Response: HTTP
200{ success: true, message? }; lỗi validate schema trả400 { error, code? }. - Chỉ event kết thúc mới được dispatch;
TRANSACTION_CREATED/ATTEMPT_CREATED/ATTEMPT_SENTchỉ qua WebSocket. - Xác thực: hiện chưa có. Core có sẵn helper
X-Webhook-SignatureHMAC-SHA256 và dispatcher có thể đính kèm, nhưng sale không verify - xem Vấn đề đã biết.
Status code - giá trị thật
Từ MQPay*Statuses (alias Statuses của IGNIS). Band tối đa là 5xx - không có 600.
| Miền | Member = giá trị |
|---|---|
| Transaction status | NEW 100_NEW · PARTIAL 300_PARTIAL · SETTLED 304_SETTLED · BLOCKED 403_BLOCKED · CLOSED 404_CLOSED · CANCELLED 505_CANCELLED |
| Attempt status | NEW 100_NEW · SENT 204_SENT · SUCCESS 302_SUCCESS · FAIL 500_FAIL · EXPIRED 501_EXPIRED |
| Attempt type | MAKE_PAYMENT 100_MAKE_PAYMENT · CANCEL_PAYMENT 200_CANCEL_PAYMENT · REFUND_PAYMENT 300_REFUND_PAYMENT |
Lỗi doc thường gặp (đã sửa)
300_SUCCESS sai - 300_ là PARTIAL; thành công là 302_SUCCESS. SETTLED 600 sai - SETTLED là 304_SETTLED. Lấy giá trị từ bảng trên.
Vấn đề đã biết
- Connector Smart POS chết trong service payment (không bao giờ được truyền vào
IMQPayOptions); verify IPN trả hợp lệ khi thiếu checksum, thứ tự field ký chưa xác nhận. Đừng dựa vào nó. /webhooks/paymentchưa xác thực ở phía sale - tin headerX-Webhook-Event-Typevà có thể hoàn tất đơn mà không verifyX-Webhook-Signaturecủa dispatcher. Việc siết (verify HMAC + cửa sổ chống replay) là một mục Security.