Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,91 @@ If you find this project useful, please consider supporting it:
- [Litecoin](https://www.blockchain.com/explorer/addresses/ltc/ltc1ql8dvx0wv0nh2vncpt9j3zqefaehsd25cwp7pfx)

List of sponsors: [![GitHub Sponsors](https://img.shields.io/github/sponsors/bchainhub?label=Sponsors&logo=githubsponsors&color=EA4AAA)](https://github.com/sponsors/bchainhub)

## PayQR national/interoperable QR payments

Use `payto://qr/{country}/{identifier}` with lowercase ISO country codes. PayQR is global; **Pay QR** is the website's label. Current mappings are BN tarusQR, KH KHQR, ID QRIS, LA LaoQR, MY DuitNow QR, MM MMQR, PH QR Ph, SG PayNow/SGQR, TH PromptPay/Thai QR and VN VietQR.

See [PayQR URI API, field coverage and validation limits](docs/PAYQR.md). A PayTo URI, national payment payload, QR image and payment-network connection are separate things. Generating a syntactically valid QR does not imply participation in or authorization to acquire transactions from a payment network. Unsupported national profiles fail closed.

PayQR support encodes/decodes URI and structured payment data only. National QR payload generation and rendering are owned by the PayTo website.

### Philippine QR Ph payment data

Canonical destination: `payto://qr/ph/{identifier}`; network `qr`, country `ph`, scheme `qrph`.
The identifier is an opaque issuer-provided value, not a verified bank account, mobile number or merchant ID.
The existing generic target stores optional `payment-mode=p2p|p2m` and `qr-type=static|dynamic` in its parameters.
`Payto.paymentMode` and `Payto.qrType` expose validated getters/setters and JSON object properties.
`payment-mode` is a PayTo extension; `mode` remains the existing pass presentation property.

Example (illustrative URI, not an official payment destination or native test vector):

```text
payto://qr/ph/Demo%40Issuer?amount=PHP%3A15.25&payment-mode=p2m&qr-type=dynamic&reference=Bill%201
```

PHP amounts use positive decimal strings with at most two decimal places and the existing 13-character amount limit. No floating-point conversion is used by the QR URI codec. Dynamic requests require an amount in this supported PayTo profile. Static data does not imply a recurring debit mandate. We preserve generic receiver-name/reference fields without claiming a verified native tag mapping. We do not impose participant-specific transaction limits or infer recipient requirements from another country.

**Native QR Ph payload encoding and decoding are not implemented:** researched public sources did not establish the Philippine account templates, routing identifiers and official conformance vectors. A PayTo URI is not a QR Ph payload accepted by banking apps. No generic EMV payload is classified as QR Ph merely from PH/PHP.

Architecture, when a verified native profile becomes available:

```text
PayTo URI → structured payment data → native encoder → payload string
scanned payload string → native decoder → structured data → PayTo URI
```

The native arrows above are currently unavailable for QR Ph. Libraries stop at data, never render QR images, scan cameras, enroll merchants, connect to InstaPay, transfer funds or check payment status. QR URI input is capped at 8192 characters; duplicate query keys, controls, malformed escaping and invalid amounts are rejected.

Sources: [BSP QR Ph](https://www.bsp.gov.ph/SitePages/MediaAndResearch/Multimedia_QRPh.aspx), [BSP P2P FAQ](https://www.bsp.gov.ph/Media_and_Research/Primers%20Faqs/QR_Ph_P2P_FAQs.pdf), [BSP P2M FAQ](https://www.bsp.gov.ph/Media_and_Research/Primers%20Faqs/QR_Ph_P2M_FAQs.pdf), [PayMongo MPM API](https://docs.paymongo.com/reference/generate-mpm-qr), [PayMongo QR Ph acceptance](https://docs.paymongo.com/docs/payment-acceptance-qr-ph). Provider API fields are evidence for supported payment concepts, not the national TLV layout.

### Indonesian QRIS payment data

Canonical URI: `payto://qr/id/{identifier}`. Network `qr`, country `id`, scheme `qris`, domestic currency `IDR`.
The path remains an opaque acquirer-issued identifier (`issuer` type). It is **not** relabeled NMID or Merchant PAN: the reviewed public material does not establish sufficient routing semantics for that mapping. Do not invent these provider-issued values.

```text
payto://qr/id/Demo?qr-type=static
payto://qr/id/Demo?amount=IDR%3A50000&qr-type=dynamic&reference=Bill%201
```

These illustrative links are not native QRIS payloads. Optional `qr-type=static|dynamic` uses the existing `qrType` property. Explicit static requests reject amount; dynamic requests require it. Unspecified type preserves a portable suggested amount without generating a native transaction. Amounts are positive IDR decimal strings, at most two fractional digits, capped at IDR 10,000,000 using integer minor-unit comparison. Decimal precision is the existing portable profile, not a claim that every provider accepts fractional rupiah. Generic `receiver-name` and `reference` remain portable metadata without a verified QRIS tag mapping. No static-to-dynamic payload conversion is implemented.

`inspectEmvMpm(payload)` provides bounded **raw ASCII EMV envelope inspection** in both libraries. It checks TLV boundaries, duplicate fields, nested template boundaries and CRC, then returns `fields` and `templates`. Its JSON shape matches across TypeScript and Dart (Dart uses `.toJson()`). It does not return a national scheme, NMID, Merchant PAN or PayTo target and is not a QRIS compliance validator. It rejects non-ASCII input and payloads over 4096 characters; nested unknown subfields stay raw. The official Midtrans example is a fixture with published CRC `A623`.

**Native QRIS encoding and semantic decoding remain unsupported.** Full authoritative routing/template requirements were not available in the reviewed public material. ASPI documents a specification request process. Neither a correct CRC nor ID/360 proves QRIS validity. No guessed national tags, transaction identifiers or provider credentials are generated. The website uses PayTo for this country and hides the native-format switch.

```text
PayTo URI ↔ structured portable QR data
provider payload → raw EMV inspection (no payment destination inference)
structured QRIS → native encoder → payload → website renderer [not implemented]
```

Libraries render no images, perform no scanning or network calls, register no merchants, issue no identifiers and perform no transfers, status checks or settlement. Static QR does not authorize recurring debits. CPM, QRIS TAP, Tuntas, cross-border routing/FX and provider connectivity are outside this implementation.

Sources: [Bank Indonesia QRIS](https://www.bi.go.id/id/fungsi-utama/sistem-pembayaran/ritel/kanal-layanan/QRIS/default.aspx), [ASPI QRIS modes](https://aspi-indonesia.or.id/standar-dan-layanan/qris/), [ASPI specification-request process, annual report p. 46](https://aspi-indonesia.or.id/files/2024/12/AR%20ASPI%202023-FA_all_rev.pdf), [Midtrans official dynamic example](https://docs.midtrans.com/docs/gopay-qris-pos-integration), [EMVCo QR specifications](https://www.emvco.com/emv-technologies/qr-codes/), [BI cross-border QRIS](https://www.bi.go.id/id/fungsi-utama/sistem-pembayaran/ritel/kanal-layanan/QRIS/QRIS-Antarnegara/default.aspx).


### Presentation formats and URI extensions

`Payto.formats` returns `['payto', 'epc']` for IBAN, or `['payto', '<scheme>']`
for supported Pay QR countries: `khqr`, `laoqr`, `duitnow`, `mmqr`, `paynow`,
`promptpay`, and `vietqr`. PayTo-only methods (including Brunei, QR Ph, and QRIS)
omit `formats` from object JSON; the getter returns `null`.
PayTo is always first and is the default. Capability metadata does not validate
whether the supplied fields are sufficient for a native payment format.

`reference`, `purpose`, and `information` are readable/writable PayTo query
extensions, also exposed in object JSON. Assign `null` to remove them. Unknown
query extensions remain preserved by the URI codec. `formats` is derived metadata,
not a query parameter; reading it never changes the PayTo link.

These libraries encode and decode **PayTo links only**. They do not generate or
parse EPC or national payment payloads, or render barcodes. Applications implement
those formats and validate their requirements separately.


**Validation coverage is partial:** all current Pay QR fields round-trip through
the parameter map, but dedicated field accessors/top-level JSON properties and
country-specific validation are not complete. See [field coverage and known
validation gaps](docs/PAYQR.md#field-coverage-and-validation-boundaries).
128 changes: 128 additions & 0 deletions docs/PAYQR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# PayQR data support

This library only encodes/decodes PayTo URIs and structured data, with validation and normalization. QR payload generation and rendering belong exclusively to the PayTo website.

## Portable URI contract

```text
payto://qr/{country}/{identifier}
```

The country is a lowercase ISO 3166-1 alpha-2 code and selects the default scheme. Do not add a scheme path segment, `scheme` query parameter, or an alternate national-scheme authority. Country and authority input are case-insensitive; serialization produces lowercase. Identifiers retain their exact case, leading zeros and Unicode; they are never trimmed, numerically converted or Unicode-normalized.

```text
payto://qr/la/ABC123
payto://qr/kh/john_smith%40devb?amount=USD%3A12.50
payto://qr/sg/%2B6581234567
payto://qr/th/0066812345678?amount=THB%3A12.50&receiver-name=Test
```

These are syntax examples, not verified destinations.

The supported URI profile has exactly two nonempty path components, no credentials, port or fragment. Reject extra segments, URLs as identifiers, control/whitespace characters, malformed percent encoding/UTF-8, duplicate query keys and dot segments before URL normalization. Unknown countries (including `br`, `hk` and `in` until registered) are rejected. The global design permits adding those countries without changing this contract.

Identifiers without a verified national format use a deliberately limited **issuer** envelope: 1–128 Unicode letters/digits or `_.@+-`. This is a portable opaque identifier, **not national account validation**. It may exclude legitimate issuer formats not yet researched. The UI discloses this distinction. Do not treat an accepted issuer identifier as a payable account.

Query values are percent-encoded; keys are sorted for deterministic serialization. Default `identifier-type` is omitted; nondefault types use `identifier-type`. Common properties are `amount=ISO4217:decimal`, `receiver-name`, `reference`, `message`, and the existing library's `org`. `currency` is not a separate query property. Amounts use strings, not floating-point conversion: positive, no exponent/sign/grouping, no leading zeros except `0.x`, at most two decimal digits and 13 value characters. KHR and VND use whole units in this profile. These are conservative supported-profile constraints, not a claim that every scheme shares every monetary limit. Empty currency metadata means unverified; amounts are rejected, not unrestricted. Unknown extension queries are preserved, subject to key syntax, a 512 UTF-16-unit value bound and no controls. Adapters reject extensions they cannot encode.

LaoQR uses `application-id` for the institution-supplied 16-character AID and `acquirer-id` for its six-digit IIN. The receiver ID alone cannot produce a routable LaoQR. The constructor applies the encoder’s ASCII receiver-ID restriction; the portable URI envelope continues accepting Unicode identifiers.

National-specific optional query fields currently validated include `acquirer-id`, `merchant-city`, `mcc`, and `qr-type`. KHQR uses `qr-currency=KHR|USD` for a reusable static QR with no amount (default KHR); if an amount is supplied, its currency must agree. MMQR uses the acquirer-supplied `scheme-id` reverse domain (maximum 32 ASCII characters) and `local-name` (1–25 Myanmar characters). VietQR uses `acquirer-id` for the six-digit receiving bank BIN. A dynamic request in the supported profile requires an amount. This rule does not assert that every national dynamic scheme requires an amount.


## API and extension points

TypeScript exports `PayQrCountry`, `PayQrScheme`, `PayQrTarget`, `payQrSchemes`, `parsePayQr`, `serializePayQr`, `validatePayQrIdentifier`, `validatePayQrParameters` from the existing package entry point. Existing `Payto` exposes `payQr`, `country`, `scheme`, `identifier`, `identifierType`, `reference`; setters exist for country, identifier, identifierType and reference. `address` aliases the decoded PayQR identifier. `toJSON()` remains a URI string; `toJSONObject()` adds the new fields. Common PayQR parameter setters are validated atomically. Low-level mutable URL/searchParams APIs are revalidated when reading a PayQR target or serializing.

```ts
import Payto, { parsePayQr, serializePayQr } from 'payto-rl';
const payment = new Payto('payto://qr/th/0066812345678');
payment.amount = 'THB:12.50';
payment.receiverName = 'Test';
const target = parsePayQr(payment.toString());
const canonicalUri = serializePayQr(target);
```

Dart exports `PayQrScheme`, `payQrSchemes` and immutable `PayQrTarget` through `flutter_paytorl.dart`. Use `PayQrTarget.parse(uri)`, the named-argument constructor, `toString()` and `toJson()`. Existing `Payto` has equivalent properties/setters; `toJson()` remains a string and `toJsonObject()` adds country/scheme/identifier/type/reference. No existing required JSON constructor arguments were changed.

```dart
final payment = Payto('payto://qr/kh/john_smith%40devb');
payment.amount = 'USD:12.50';
final target = PayQrTarget.parse(payment.toString());
final canonicalUri = target.toString();
final details = payment.toJsonObject().toJson();
```

To extend globally: add country/scheme metadata and a documented identifier/currency profile in each language; add a website country schema and the desired presentation grouping; implement and register national adapters in the website only; add matching contract fixtures and scheme vectors. No authority or URI redesign is needed. The same checked-in JSON fixtures in both libraries guard parity without imposing a cross-repository dependency on Dart tests.


## Field coverage and validation boundaries

All current Pay QR constructor fields can round-trip through the libraries' URI
parameter maps. This does **not** mean every field has a dedicated `Payto`
getter/setter, a top-level JSON property, or complete country-specific validation.

| Data | URI representation |
| --- | --- |
| Country, identifier | `payto://qr/{country}/{identifier}` path |
| Scheme | Derived from country; not a separate URI parameter |
| Identifier type | `identifier-type` (default type omitted) |
| Amount and currency | `amount=CURRENCY:value`; KH static currency uses `qr-currency` |
| Common details | `receiver-name`, `reference`, `message`, `org` |
| Routing and merchant identity | `acquirer-id`, `application-id`, `scheme-id`, `recipient-type`, `merchant-id`, `acquiring-bank`, `account-information` |
| Merchant details | `merchant-city`, `mcc`, `local-name`, `merchant-mobile`, `postal-code` |
| Additional details | `bill-number`, `store-label`, `terminal-label` |
| Request settings | `payment-mode`, `qr-type`, `amount-editable`, `expiry-date`, `creation-timestamp`, `expiration-timestamp` |

Use `Payto.payQr.parameters` or the parsed target's `parameters` for fields without
dedicated accessors. TypeScript updates can use `serializePayQr` with a copied
parameter map; Dart uses a new `PayQrTarget` with a copied map. Object JSON from
`Payto` does not flatten every parameter; keep the URI or target parameter map
when complete field preservation is needed.

Both libraries validate URI structure/escaping, supported identifiers, monetary
syntax/currencies, selected enums, field lengths and some country-specific rules.
Unknown extensions are preserved under the generic query constraints. Validation
is **not yet fully aligned** with the website: examples of additional website
checks include KH expiry after creation, Lao receiver-ID ASCII/length limits,
printable-ASCII merchant fields, and some field/country and static/amount
combinations. The two library implementations also have remaining differences
(for example, Lao merchant-city length handling). Native-generation prerequisites
remain application responsibilities; a parsed PayTo link does not prove that a
national barcode can be generated or that an account is registered.

## Format metadata and IBAN extensions

`Payto.formats` describes presentation capabilities, not implemented library
encoders and not validation of the current payment data.

| Payment method | `formats` in object JSON |
| --- | --- |
| IBAN | `['payto', 'epc']` |
| Cambodia | `['payto', 'khqr']` |
| Laos | `['payto', 'laoqr']` |
| Malaysia | `['payto', 'duitnow']` |
| Myanmar | `['payto', 'mmqr']` |
| Singapore | `['payto', 'paynow']` |
| Thailand | `['payto', 'promptpay']` |
| Vietnam | `['payto', 'vietqr']` |
| Brunei, Philippines, Indonesia, other PayTo-only methods | Omitted |

PayTo is first and is the application default. A PayTo-only getter returns
`undefined` in TypeScript or `null` in Dart, never `['payto']`. The metadata appears
in `toJSONObject()` (TypeScript) and `toJsonObject().toJson()` (Dart), not in URI
serialization and not automatically on the lower-level Pay QR target.

`reference`, `purpose` and `information` are readable/writable PayTo query
extensions and object JSON properties. Setting them to `null` removes them.
The libraries preserve IBAN extension values without applying the application's
EPC RF checksum, purpose-code or payload-length rules. EPC encoding/decoding and
national payload encoding/decoding are not library capabilities. The existing
`inspectEmvMpm` helper only inspects a raw EMV envelope; it does not decode a
national payment into a PayTo target.

The website's `/pass` endpoint accepts `design.qrFormat` with `payto`, `epc` or the
matching supported country scheme name. It defaults to PayTo when the format or
entire design object is omitted, for both JSON and form requests and both wallets.
This is an application API, not a library encoder. Payment links omit `format`;
shared PayPass presentation links use it only for a nondefault format.
4 changes: 4 additions & 0 deletions lib/flutter_paytorl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ library flutter_paytorl;
export 'src/payto.dart';
export 'src/models/payto_json.dart';
export 'src/exceptions/payto_exception.dart';

export 'src/payqr.dart';

export 'src/emv_mpm.dart';
Loading
Loading