Skip to content

feat: market-native settlement QR — VietQR, SEPA, UPI, PromptPay, Pix + link fallback (closes #11) - #17

Merged
ngthson553-create merged 2 commits into
mainfrom
feat/payment-rails
Sep 21, 2026
Merged

ngthson553-create merged 2 commits into
mainfrom
feat/payment-rails

Conversation

@ngthson553-create

Copy link
Copy Markdown
Owner

Vấn đề

Người dùng ngoài Việt Nam không thể dùng VietQR (chuẩn NAPAS chỉ tồn tại trong hệ ngân hàng VN): không quét được và không có số tài khoản VN để điền. Issue #11.

Giải pháp: mỗi thị trường dùng chuẩn QUỐC GIA của thị trường đó

Nghiên cứu ~15 thị trường (ma trận đầy đủ trong docs/PAYMENTS.md) → 5 chuẩn hội đủ 3 điều kiện: sinh payload thuần client, không cần đăng ký merchant, spec mở do ngân hàng/quốc gia công bố. Thị trường còn lại (US, UK, CA, AU, JP, KR, SG, MY, ID…) rơi về link/handle tự do — đúng mô hình Splitwise đã chứng minh 10+ năm: không bao giờ động vào tiền, chỉ cho mỗi người dán cách người khác trả mình.

Rail Thị trường Kiểu payload Vector kiểm thử
VietQR 🇻🇳 như trước — không đổi bộ test cũ
EPC/SEPA QR 🇪🇺 Eurozone 12 dòng text (IBAN + tên) cấu trúc EPC069-12 v2.1
UPI intent 🇮🇳 upi://pay?pa=VPA&pn=tên spec NPCI
PromptPay 🇹🇭 EMVCo TLV + CRC16 khớp từng byte fixture dtinth/promptpay-qr (3 vector, CRC 6197/29C1/7B5A)
Pix BR Code 🇧🇷 EMVCo TLV + CRC16 nguyên ví dụ static trong manual Bacen (CRC 1D3D)
Handle còn lại URL → QR / text + copy —

Điểm thiết kế đáng chú ý

  • Thuần offline như VietQR: 4 builder mới đều chỉ dựng chuỗi (kèm CRC16-CCITT-FALSE tự viết, verify bằng giá trị kiểm chuẩn 123456789 → 29B1). Không API ngoài, không dependency mới.
  • Số tiền chỉ nhúng trong VietQR (sổ cái đang base VND). 4 chuẩn ngoài để trống amount — cả 4 đều cho phép — và sheet hiện dòng nhắc "nhập số tiền hiển thị phía trên". Khi có per-group base currency (roadmap), mỗi rail nhúng đúng đơn vị của nó.
  • Người Việt không đổi gì: 3 field bank cũ được ưu tiên khi phân giải; cột mới (payment_rail, payment_data trên group_members + profiles, migration 0035) đều nullable.
  • UI: component PaymentMethodFields dùng chung (chọn rail + field theo rail, tái dùng BankFields cho VietQR) lắp vào 3 nơi: onboarding, cài đặt tài khoản nhận tiền, sheet thành viên. Sheet thanh toán dispatch theo rail: hàng thông tin + nút copy riêng từng chuẩn, nút mở link cho handle URL, hint amount cho rail ngoài VN.
  • Sửa kèm: quay về VietQR giờ XOÁ sạch rail cũ trong DB (trước đó sót vì patch undefined bị bỏ qua).

Kiểm chứng

  • 13 test vector mới (PromptPay/Pix khớp từng ký tự + CRC chuẩn + EPC 12 dòng + UPI URI + resolve)
  • Toàn bộ 112/112 test pass, lint 0, typecheck 0, build OK
  • Smoke thật bằng Playwright: member dùng SEPA (IBAN Đức) → sheet Pay hiển thị QR EPC + hàng IBAN/BIC/tên + hint amount — đúng từng chi tiết
  • Đã xác minh spec từ nguồn chính thức: EPC069-12, NPCI, Bank of Thailand, BCB BR Code Manual v2.0.0

Closes #11

Foreign users cannot scan VietQR, so the payout profile is now market-aware.
Six rails, chosen per member: VietQR (Vietnam), EPC/SEPA QR (Eurozone),
UPI (India), PromptPay (Thailand), Pix (Brazil) - plus a freeform link/handle
rail (PayPal.Me, Venmo, PayID, Zelle...) for markets with no open P2P
standard: US, UK, CA, AU, JP, KR, SG, MY, ID... This is the Splitwise model -
Splitz still never holds money, it just writes the recipient into the
market's own standard so the payer's banking app opens prefilled.

All four new QR payloads are generated offline as pure strings, same as
VietQR: EMVCo TLV + CRC16-CCITT for PromptPay and Pix, a 12-line EPC payload
for SEPA, an intent link for UPI. Builders are pinned to official test vectors
(PromptPay byte-for-byte against dtinth/promptpay-qr fixtures, Pix against
the static example in the Bacen BR Code manual incl. CRC 1D3D) - 13 new tests.

Amounts are embedded only in VietQR (ledger is VND); the other standards omit
the amount - all four allow it - and the sheet tells the payer to type the
amount shown. When per-group base currency lands later, each rail can embed
the amount in its own unit.

Vietnamese users are untouched: existing bank fields keep working and take
priority in resolution; the new columns (payment_rail, payment_data on
group_members and profiles, migration 0035) are nullable.

UI: shared PaymentMethodFields component (rail picker + per-rail fields,
reusing BankFields for VietQR) wired into onboarding, payout settings and the
member sheet; the payment sheet dispatches per rail with per-rail info rows,
copy buttons and an open-link button for URL handles. docs/PAYMENTS.md records
the market matrix, spec sources and how to add a market.
@ngthson553-create
ngthson553-create merged commit 6b07536 into main Sep 21, 2026
2 checks passed
@ngthson553-create
ngthson553-create deleted the feat/payment-rails branch September 21, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Settlement methods beyond VietQR (PayPal.me, Wise, Revolut…)

1 participant