fix BML POS QRs
This commit is contained in:
@@ -33,6 +33,16 @@ TLV path: **root tag `35` → sub-tag `20` → sub-sub-tag `01`**
|
||||
|
||||
The value at tag `01` is the full `https://pay.bml.com.mv/app/...` URL.
|
||||
|
||||
### 3. POS QR (`mv.com.bml.qtr`)
|
||||
|
||||
BML POS terminals emit an EMVCo-style dynamic QR with no tag `26` and supplementary domain
|
||||
`mv.com.bml.qtr`. The same TLV path (`35` → `20` → `01`) holds a bare reference such as
|
||||
`02:215c7b9f15ce4ed28e15697ea976db99` instead of a URL. That bare reference — not the whole payload,
|
||||
which is rejected with code `112` — is what gets Base64-encoded into the payrequest lookup below.
|
||||
The reference does **not** resolve as a `pay.bml.com.mv/app/` short code in a browser; only the API
|
||||
understands it. See
|
||||
[PayMV QR Format → BML POS QR](../thijooree/18-paymv-qr-format.md#bml-pos-qr-mvcombmlqtr).
|
||||
|
||||
---
|
||||
|
||||
## PayMV QR Format (TLV)
|
||||
@@ -71,19 +81,29 @@ PayMV QRs (static, PayMV-native) use a decimal TLV encoding (not BER-TLV):
|
||||
GET https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments/payrequest/{base64Url}
|
||||
```
|
||||
|
||||
`{base64Url}` is the full QR URL (e.g. `https://pay.bml.com.mv/app/...`) base64-encoded with standard encoding (with padding).
|
||||
`{base64Url}` is the lookup key, base64-encoded with standard encoding — the full QR URL
|
||||
(e.g. `https://pay.bml.com.mv/app/...`), or for POS QRs the bare `35` → `20` → `01` reference.
|
||||
BML's own app omits the `=` padding; the padded form resolves as well, so the client matches the app
|
||||
rather than relying on either being required.
|
||||
|
||||
### Headers
|
||||
|
||||
| Header | Value |
|
||||
|---|---|
|
||||
| `accept` | `application/json` — **required in practice**, see below |
|
||||
| `Authorization` | `Bearer <access_token>` |
|
||||
| `User-Agent` | `bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})` |
|
||||
| `x-app-version` | `2.1.44.348` |
|
||||
|
||||
> **`accept: application/json` is not optional.** Handled errors (codes 103, 112, …) come back as
|
||||
> JSON regardless, but when the route throws, the server renders the Internet Banking HTML login
|
||||
> page — under HTTP **200** — instead of a JSON error body. A client without the header then sees a
|
||||
> "successful" HTML response it cannot parse. All BML API requests set it in `bmlApiRequest()`.
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url 'https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments/payrequest/<base64Url>' \
|
||||
--header 'accept: application/json' \
|
||||
--header 'Authorization: Bearer <access_token>' \
|
||||
--header 'User-Agent: bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})' \
|
||||
--header 'x-app-version: 2.1.44.348'
|
||||
@@ -116,6 +136,16 @@ curl --request GET \
|
||||
| `amount` | Payment amount (`"0.00"` for static QRS) |
|
||||
| `currency` | Currency code (typically `"MVR"`) |
|
||||
|
||||
### Failure Responses
|
||||
|
||||
`success: false` comes back with a code and a user-facing message; the client shows BML's own
|
||||
wording and keeps the user on the Transfer screen.
|
||||
|
||||
| Code | Message |
|
||||
|---|---|
|
||||
| `103` | The payment request has expired |
|
||||
| `112` | Unsupported payment link |
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Pay (3-Step TOTP Flow)
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
> **This flow no longer lives in a dedicated fragment.** `BmlQrPayFragment.kt` still exists as a source file but is unreachable — no callers, no nav graph entry, no intent action routes here. The actual BML gateway QR flow runs inside `TransferFragment` via `TransferFragment.newInstanceFromBmlQr(qrUrl, fromAccountNumber)`. See [Transfer Flows — BML QR Merchant Payment Flow](20-transfer-flows.md).
|
||||
|
||||
The on-the-wire payment protocol is unchanged — see [BML QR Payment API](../bmlapi/13-qr-payment.md) for the 3-step TOTP flow (`approve` → `channel: token` → `otp`).
|
||||
The on-the-wire payment protocol is unchanged — see [BML QR Payment API](../bmlapi/13-qr-payment.md) for the 3-step TOTP flow (`approve` → `channel: token` → `otp`), and [Transfer Flows — BML QR Merchant Payment Flow](20-transfer-flows.md#bml-qr-merchant-payment-flow) for the three QR sub-modes the live path handles.
|
||||
|
||||
Two differences remain in the stale fragment, should it ever be revived: it calls `lookupPayRequest()` with the scanned URL only (no `PaymvQrParser.bmlPayRequestKey()`, so POS QRs would not resolve), and it swallows every lookup error into the hardcoded `bml_qr_lookup_failed` toast instead of surfacing BML's own message.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -28,12 +28,13 @@ Tags and lengths are always exactly 2 decimal digits. Fields are concatenated di
|
||||
| `00` | Format indicator | Always `"01"` |
|
||||
| `01` | Point-of-initiation method | `"11"` = static QR, `"12"` = dynamic QR |
|
||||
| `26` | Merchant account information | Container — see sub-tags below |
|
||||
| `35` | BML/gateway merchant info | Container — present in combined EMV+BML QRs only |
|
||||
| `35` | BML/gateway merchant info | Container — present in combined EMV+BML QRs and in BML POS QRs |
|
||||
| `52` | Merchant category code | `"0000"` (generic) |
|
||||
| `53` | Transaction currency | `"462"` = MVR (ISO 4217 numeric) |
|
||||
| `54` | Transaction amount | Decimal string (e.g. `"1.50"`); absent for open-amount QRs |
|
||||
| `58` | Country code | `"MV"` |
|
||||
| `59` | Merchant / recipient name | Max 25 characters |
|
||||
| `60` | Merchant city / store code | BML POS QRs only |
|
||||
| `62` | Additional data field | Container — see sub-tags below |
|
||||
| `63` | CRC | `6304` prefix + 4-char hex checksum — always last |
|
||||
| `80` | Supplementary data | Container — timestamp and domain |
|
||||
@@ -153,6 +154,67 @@ The value at sub-sub-tag `01` is a full `https://pay.bml.com.mv/app/...` URL. Ex
|
||||
|
||||
Plain BML QR codes (not combined) start with `https://pay.bml.com.mv/app/` directly.
|
||||
|
||||
`PaymvQrParser.bmlQrPayTarget()` covers all of these: it returns the URL for plain URL QRs and for
|
||||
combined QRs, the whole EMV payload for POS QRs (below, recognised by the `mv.com.bml.qtr`
|
||||
supplementary domain), and null for everything else — PayMV QRs keep falling through to `parse()`.
|
||||
|
||||
---
|
||||
|
||||
## BML POS QR (`mv.com.bml.qtr`)
|
||||
|
||||
BML POS terminals emit a third shape — an EMVCo-style dynamic QR whose supplementary data domain
|
||||
(tag `80` → `00`) is `mv.com.bml.qtr` and which has **no tag `26`**, so there is no PayMV account
|
||||
number to transfer to. The same `35` → `20` container is used as in combined QRs, but sub-tag `01`
|
||||
holds a bare reference instead of a URL.
|
||||
|
||||
Example (CRC verified, same CRC-16/CCITT-FALSE as above):
|
||||
|
||||
```
|
||||
00020101021235752071000202013502:215c7b9f15ce4ed28e15697ea976db9902109809724081030874009538520400005303462540436005802MV5911BEST BANANA6006LD044262220510dtyams497d0804POPE80470014mv.com.bml.qtr01252026-09-21T13:33:22.00000630443FA
|
||||
```
|
||||
|
||||
| TLV path | Value | Notes |
|
||||
|---|---|---|
|
||||
| `00` | `01` | Format indicator |
|
||||
| `01` | `12` | Dynamic QR |
|
||||
| `35`→`20`→`00` | `02` | Version / format of the container (combined QRs use the URL form) |
|
||||
| `35`→`20`→`01` | `02:215c7b9f15ce4ed28e15697ea976db99` | Payment reference — `<type>:<32 hex>` |
|
||||
| `35`→`20`→`02` | `9809724081` | Merchant identifier (10 digits) |
|
||||
| `35`→`20`→`03` | `74009538` | Terminal identifier (8 digits) |
|
||||
| `52` | `0000` | MCC |
|
||||
| `53` | `462` | MVR |
|
||||
| `54` | `3600` | Amount — **no decimal point**, unlike PayMV's `"1.50"` |
|
||||
| `58` / `59` / `60` | `MV` / `BEST BANANA` / `LD0442` | Country, merchant name, merchant city/store code |
|
||||
| `62`→`05` | `dtyams497d` | Reference / bill number |
|
||||
| `62`→`08` | `POPE` | Purpose / terminal label |
|
||||
| `80`→`00` | `mv.com.bml.qtr` | Domain — identifies the POS format |
|
||||
| `80`→`01` | `2026-09-21T13:33:22.00000` | Timestamp |
|
||||
|
||||
### Pay-Request Lookup Key
|
||||
|
||||
The lookup key is the **bare reference**, Base64-encoded without padding:
|
||||
|
||||
```
|
||||
GET .../walletpayments/payrequest/MDI6MjE1YzdiOWYxNWNlNGVkMjhlMTU2OTdlYTk3NmRiOTk
|
||||
```
|
||||
|
||||
BML's own app omits the `=` padding, so `BmlQrPayClient.lookupPayRequest()` encodes with
|
||||
`NO_WRAP or NO_PADDING` for every QR type; the padded form resolves too. What the request *must*
|
||||
carry is `accept: application/json` — see
|
||||
[QR Payment → Step 1 headers](../bmlapi/13-qr-payment.md#headers).
|
||||
|
||||
Confirmed against the live API (the example QR had already expired, so BML answered `103` rather
|
||||
than with merchant details — but `103` means the reference itself resolved):
|
||||
|
||||
| Key tried | Response |
|
||||
|---|---|
|
||||
| `02:215c7b9f15ce4ed28e15697ea976db99` | `103` — "The payment request has expired" ✅ recognised |
|
||||
| the whole EMV payload | `112` — "Unsupported payment link" ❌ |
|
||||
| `https://pay.bml.com.mv/app/02:215c…` | `103` — recognised too; the host itself 400s on that path, so the backend must strip the prefix |
|
||||
|
||||
`PaymvQrParser.bmlPayRequestKey()` returns the URL for URL QRs and the reference for POS QRs, so
|
||||
exactly one request is made either way.
|
||||
|
||||
---
|
||||
|
||||
## Example Payload
|
||||
|
||||
@@ -13,7 +13,7 @@ The transfer screen (`TransferFragment`) handles all outgoing payments across MI
|
||||
| `newInstance(accountNumber, displayName, subtitle, colorHex, imageHash)` | Pre-fills the "To" card from a contact, recents pick, or About → Donate |
|
||||
| `newInstanceFrom(account: BankAccount)` | Pre-selects the given account in the "From" dropdown |
|
||||
| `newInstanceFromQr(accountNumber, displayName, amount, remarks, fromAccountNumber?)` | Pre-fills recipient + optional amount/remarks from a PayMV QR scan |
|
||||
| `newInstanceFromBmlQr(qrUrl, fromAccountNumber?)` | BML card/gateway QR merchant payment mode — locks recipient, may pre-fill amount |
|
||||
| `newInstanceFromBmlQr(qrUrl, fromAccountNumber?)` | BML card/gateway/POS QR merchant payment mode — locks recipient, may pre-fill amount |
|
||||
| `newInstanceWithAutoScan()` | Opens the [QR scanner](25-qr-scanner.md) immediately on load |
|
||||
|
||||
---
|
||||
@@ -215,22 +215,29 @@ If channel fetch fails or returns empty, the flow is aborted and the form is re-
|
||||
|
||||
## BML QR Merchant Payment Flow
|
||||
|
||||
Triggered when the transfer screen is opened via `newInstanceFromBmlQr()` or when a BML ebanking/pay.bml URL is scanned from the QR scanner.
|
||||
Triggered when the transfer screen is opened via `newInstanceFromBmlQr()`, which every scanner caller reaches through `PaymvQrParser.bmlQrPayTarget(raw)` — it returns the value to pay with, or null for a QR that is not BML's.
|
||||
|
||||
Two sub-modes:
|
||||
Three sub-modes:
|
||||
|
||||
| Mode | Trigger | Extra step |
|
||||
| Mode | `bmlQrPayTarget()` returns | Extra step |
|
||||
|---|---|---|
|
||||
| Static card QR | URL starts with `https://ebanking.bankofmaldives.com.mv/qrpay/` | None |
|
||||
| Gateway QR | URL starts with `https://pay.bml.com.mv/app/` | `BmlQrPayClient.preInitiatePayment()` required before initiate |
|
||||
| Static card QR | the QR text, when it starts with `https://ebanking.bankofmaldives.com.mv/qrpay/` | None |
|
||||
| Gateway QR | the QR text, or the URL at TLV `35`→`20`→`01` in a combined EMV QR | `BmlQrPayClient.preInitiatePayment()` required before initiate |
|
||||
| POS QR | the whole EMV payload, for QRs whose tag `80`→`00` domain is `mv.com.bml.qtr` | Treated as a gateway QR — see the note below |
|
||||
|
||||
`BmlTransferHandler.lookupQrMerchant()` passes that value through `PaymvQrParser.bmlPayRequestKey()`, which hands the URL to the lookup for URL QRs and the bare `35`→`20`→`01` reference for POS QRs. See [PayMV QR Format — BML POS QR](18-paymv-qr-format.md#bml-pos-qr-mvcombmlqtr).
|
||||
|
||||
Flow:
|
||||
1. `lookupBmlQrMerchant()` — fetches merchant info via `BmlQrPayClient.lookupPayRequest()`. Locks the "To" row.
|
||||
1. `lookupQrMerchant()` — fetches merchant info via `BmlQrPayClient.lookupPayRequest()`. Locks the "To" row.
|
||||
2. For dynamic QRs (`info.amount > 0`), pre-fills the amount and locks the amount field.
|
||||
3. Remarks field is locked (not applicable for merchant payments).
|
||||
4. On confirm: TOTP is generated, then `initiatePayment()` → (for gateway QR: `preInitiatePayment()` first) → `confirmPayment()` with a fresh TOTP.
|
||||
5. On success: a success dialog is shown (no receipt saved). Back-press returns to previous screen.
|
||||
|
||||
**Lookup failure:** the user stays on the Transfer screen with the "To" row restored via `resetToFieldVisibility()` — the screen is no longer popped. When BML answered with `success: false`, its own wording is toasted (`BmlQrPayLookupException.message`, e.g. "The payment request has expired"); network, empty and non-JSON responses fall back to the `bml_qr_lookup_failed` string.
|
||||
|
||||
> **Unverified:** POS QRs are treated as gateway QRs (pre-initiate before initiate) because they carry a preset amount. No POS payment has been completed end-to-end yet — the reference captured for testing had already expired.
|
||||
|
||||
---
|
||||
|
||||
## Transfer Button Enable Conditions
|
||||
@@ -238,7 +245,7 @@ Flow:
|
||||
The transfer button is only enabled when all of the following are true:
|
||||
|
||||
- A source account is selected
|
||||
- A recipient is resolved (`resolvedAccountNumber` not blank, or `bmlQrInfo` is set)
|
||||
- A recipient is resolved (`resolvedAccountNumber` not blank, or the BML handler's `qrInfo` is set)
|
||||
- Amount is greater than `0`
|
||||
- No connectivity error for `NO_INTERNET` or for the source bank
|
||||
|
||||
|
||||
@@ -57,10 +57,10 @@ Each caller registers an `ActivityResultContracts.StartActivityForResult` launch
|
||||
|
||||
| Caller | Result handling |
|
||||
|---|---|
|
||||
| [TransferFragment](07-transfer.md) | Routes PayMV / BML URL via `PaymvQrParser` + `extractBmlGatewayUrl` |
|
||||
| [TransferFragment](07-transfer.md) | `PaymvQrParser.bmlQrPayTarget()` first (URL, combined and POS QRs → BML QR pay), then M-Faisa numeric ids, then `PaymvQrParser.parse()` |
|
||||
| [PayMvQrFragment](11-paymv-qr-screen.md) | Generation only — does not call the scanner directly |
|
||||
| `CredentialsFragment` (login) | `OtpauthParser.parse(raw)` → fills `etOtpSeed` or shows a chooser for multi-entry QRs |
|
||||
| [DashboardFragment](21-dashboard.md) | BML URL → BML QR pay; PayMV → pre-fill Transfer; otherwise toast |
|
||||
| [DashboardFragment](21-dashboard.md) | BML QR (URL or POS) → BML QR pay; PayMV → pre-fill Transfer; otherwise toast |
|
||||
| [CardsFragment](22-cards.md) | Same routing as Dashboard, scoped to the active BML card |
|
||||
|
||||
### Share-to-Scan Fast Path
|
||||
|
||||
Reference in New Issue
Block a user