feat(payment): add SePay gateway (VietQR bank transfer, VND) - #5673
Open
trinknx wants to merge 25 commits into
Open
feat(payment): add SePay gateway (VietQR bank transfer, VND)#5673trinknx wants to merge 25 commits into
trinknx wants to merge 25 commits into
Conversation
Real sandbox testing showed SePay extracts payment codes as contiguous alphanumeric strings, dropping the sub2_ underscore, while order lookup used case-insensitive-only matching and QueryOrder searched q= with the raw out_trade_no. Add a shared payment.NormalizeTransferCode, a normalized pending-order scan fallback in the service layer, and a normalized q= retry in QueryOrder.
The SePay QR dialog previously showed only the QR image with no transfer details, leaving customers unable to pay manually or simulate transfers. Expose transfer_info (account number/name, bank bin, amount, transfer content) on the create-order response for sepay instances and render it under the QR with a copy button for the transfer content.
Real banks keep transfer content verbatim, so shipping sub2_<date>... with the underscore broke SePay's alphanumeric-only payment-code extraction. Emit the normalized form (SUB22026...) in the QR and transfer_info display; webhook/query resolution already maps it back to the canonical out_trade_no.
Recharging through VND methods credited 1 balance unit per VND paid (1000 VND -> $1000) because the recharge multiplier cannot express 1/25000 — the config formatter rounds it to two decimals and it silently resets to 1. Divide VND recharge amounts by SUBSCRIPTION_USD_TO_VND_RATE (the multiplier still composes), reject VND recharge when the rate is unset, store the multiplier with exact precision, and mirror the conversion in the checkout credited-amount display.
Emit sub22026... (separators stripped, case preserved) instead of the uppercased normalized form, matching the format validated against the SePay sandbox. Extraction stays case-insensitive on both sides.
The locally generated EMV payload did not scan reliably in banking apps. Return a battle-tested VietQR image URL (vietqr.app, the generator SePay recommends) as qr_image_url; the frontend renders it instead of the self-drawn canvas when present. The EMV payload stays as fallback data.
Contributor
|
Thank you for your contribution! Before we can merge this PR, we need you to sign our Contributor License Agreement (CLA). To sign, please reply with the following comment:
You only need to sign once — it will be valid for all your future contributions to this project. I have read the CLA Document and I hereby sign the CLA You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds SePay (https://sepay.vn — a popular Vietnamese bank-transfer payment gateway) as a new payment provider, implementing the VietQR + webhook flow:
POST /api/v1/payment/webhook/sepay) → the order is confirmed and fulfilled automaticallyQueryOrderpolls SePay API v2 (/v2/transactions) so missed webhooks are reconciled by the verify/cancel flowsFeatures
backend/internal/payment/provider/sepay.go): offline payment creation (no upstream call), QueryOrder via API v2, webhook verification with HMAC-SHA256 (X-SePay-Signature, ±300 s replay window, raw-body signing, constant-time compare) or API-key fallback, refund disabled (SePay has no refund API — enabling it is rejected at instance create/update){"success":true}HTTP 200 as SePay requires; lenient transfer-code resolution (banks may uppercase content and SePay extracts codes as contiguous alphanumeric strings, e.g.sub220260815...fromsub2_20260815...), guarded strictly onprovider == sepaySUBSCRIPTION_USD_TO_VND_RATEsetting powers both directions — subscriptionUSD × rate = VNDcharge and rechargeVND ÷ rate = USDbalance (with a clear error when unset; also fixes the recharge multiplier being silently rounded to 2 decimals on save)Transfer content format
sub+ 17 alphanumeric chars (order-id date + random, separators stripped). Admins configure SePay's payment-code extraction accordingly (prefixsub, suffix length 17, digits+letters). Matching is case-insensitive and tolerates prefix/separator mutations end-to-end (webhook → instance resolution → fulfillment → query).Testing
provider == sepay; CNY conversion output is byte-identical (locked by existing tests)Admin setup (production)
https://<panel>/api/v1/payment/webhook/sepay(HMAC, secret matching the instance config), and the payment-code prefix config aboveSUBSCRIPTION_USD_TO_VND_RATEif subscriptions/recharge should convert VND↔USDNotes for reviewers
docs/superpowers/*are the (Vietnamese) design/plan working documents from the implementation process — happy to translate, relocate, or drop them from the PR if preferred