266 lines
9.0 KiB
Markdown
266 lines
9.0 KiB
Markdown
# QR Merchant Payment ("Smart Pay")
|
|
|
|
Pay an Ooredoo M-Faisa merchant by scanning their QR. The QR encodes only a numeric `qrCodeId` (e.g. `1594103440350`) — no URL, no EMV TLV envelope. The flow is three calls and **does not require OTP** (`2FARequired=NONE`).
|
|
|
|
> **Currency / pocket constraints:** captures cover MVR→MVR purchases from the user's EMONEY pocket only. The `transactionCurrency` field is taken from the QR lookup; we have not seen a non-MVR variant.
|
|
|
|
---
|
|
|
|
## Flow
|
|
|
|
```
|
|
Client Server
|
|
| |
|
|
| POST /QRCodeUtility/fetchQRCodeById | ← user scanned a QR
|
|
| formData = { qrCodeId, tenantCode } |
|
|
|------------------------------------------------>|
|
|
| [{ success, response:[{ commercialName, |
|
|
| customerId, mobileNumber, currencyCode, |
|
|
| txnAmount, status, ... }] }] |
|
|
|<------------------------------------------------|
|
|
| |
|
|
| (show merchant, accept amount + remarks)
|
|
| |
|
|
| POST /initiateNewBuy |
|
|
| formData = { merchantId, mobileNumber, |
|
|
| sourceDetails, transactionAmount, |
|
|
| transactionType:"PURCHASE", … } |
|
|
|------------------------------------------------>|
|
|
| [{ 2FARequired:"NONE", |
|
|
| authenticationType:"NONE", |
|
|
| success:true, |
|
|
| response:[{ responseObject:{ referenceId, |
|
|
| chargeDetails, … } }] }] |
|
|
|<------------------------------------------------|
|
|
| |
|
|
| (no OTP — go straight to confirm)
|
|
| |
|
|
| POST /confirmNewBuy |
|
|
| formData = { referenceId } |
|
|
| transactionAuthDetails = "null" ← literal |
|
|
|------------------------------------------------>|
|
|
| [{ success:true, |
|
|
| message:"Payment Completed Successfully", |
|
|
| response:[{ responseObject:{ isCompleted, |
|
|
| balanceInquiryDTO, ... } }] }] |
|
|
|<------------------------------------------------|
|
|
```
|
|
|
|
All three endpoints carry the standard anti-replay pair (`rndValue` + `csValue`) derived from each request's `formData` JSON — see [01-encryption.md → rndValue / csValue](01-encryption.md#anti-replay-envelope-rndvalue--csvalue).
|
|
|
|
Unlike the transfer flow, **every endpoint here returns its envelope as a JSON array** `[{...}]` for both success and error.
|
|
|
|
---
|
|
|
|
## Step 1: `QRCodeUtility/fetchQRCodeById` — resolve merchant
|
|
|
|
### Endpoint
|
|
|
|
```
|
|
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/QRCodeUtility/fetchQRCodeById
|
|
```
|
|
|
|
### Request
|
|
|
|
**Content-Type:** `application/x-www-form-urlencoded`
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| `role` | **`R01`** (the other two endpoints use `RETAIL_SUBSCRIBER` — this one does not) |
|
|
| `channel` | `C03` |
|
|
| `loginExchangeKey` | From login |
|
|
| `rndValue` / `csValue` | [Standard anti-replay](01-encryption.md#anti-replay-envelope-rndvalue--csvalue) |
|
|
| `formData` | JSON below ([html-safe `=` escaping](01-encryption.md#html-safe-gson--escape)) |
|
|
|
|
```json
|
|
{
|
|
"qrCodeId": "<numeric id from QR>",
|
|
"tenantCode": "ooredoo"
|
|
}
|
|
```
|
|
|
|
### Response — happy path
|
|
|
|
```json
|
|
[
|
|
{
|
|
"success": true,
|
|
"message": "QRCode fetched Successfully.",
|
|
"response": [
|
|
{
|
|
"mobileNumber": "9609569506", /* merchant's '960' + msisdn */
|
|
"customerId": "72518", /* used as merchantId in step 2 */
|
|
"commercialName": "Family Room", /* merchant display name */
|
|
"qrCodeId": "1594103440350",
|
|
"qrImageString": "<base64 PNG>", /* unused */
|
|
"accountNumber": null,
|
|
"txnAmount": null, /* static QR; dynamic QRs put a number here */
|
|
"currencyCode": "MVR",
|
|
"status": "Active",
|
|
"role": "AGENT",
|
|
"tenantCode": "ooredoo",
|
|
"...": "..."
|
|
}
|
|
]
|
|
}
|
|
]
|
|
```
|
|
|
|
`accountNumber` / `txnAmount` are JSON `null` for **static QRs** (the user chooses the amount). For **dynamic QRs** the server returns the fixed amount in `txnAmount` and the client should lock the amount field.
|
|
|
|
The `mobileNumber` field already includes the `960` country prefix.
|
|
|
|
### Response — QR not found / inactive
|
|
|
|
```json
|
|
[{ "success": false, "message": "QRCode not found." }]
|
|
```
|
|
|
|
The client also rejects entries with `status != "Active"`.
|
|
|
|
---
|
|
|
|
## Step 2: `initiateNewBuy` — initiate purchase (no OTP triggered)
|
|
|
|
### Endpoint
|
|
|
|
```
|
|
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/initiateNewBuy
|
|
```
|
|
|
|
### Request
|
|
|
|
**Content-Type:** `application/x-www-form-urlencoded`
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| `role` | `RETAIL_SUBSCRIBER` |
|
|
| `channel` | `C03` (top-level differs from inner `formData.channel`, which is `SubscriberApp`) |
|
|
| `loginExchangeKey` | From login |
|
|
| `rndValue` / `csValue` | Standard anti-replay (derived from the `formData` below) |
|
|
| `formData` | JSON below |
|
|
|
|
```json
|
|
{
|
|
"channel": "SubscriberApp",
|
|
"commodityType": "WALLET",
|
|
"description": "<remarks>", /* free text, may be empty */
|
|
"merchantId": "<customerId from step 1>",
|
|
"mobileNumber": "<merchant '960' msisdn from step 1>",
|
|
"sourceDetails": {
|
|
"MDNId": "960<myMsisdn>", /* PLAINTEXT — '960' + my phone */
|
|
"actorRoleType": "RETAIL_SUBSCRIBER",
|
|
"pocketId": "<my source pocket id>" /* EMONEY pocket from login */
|
|
},
|
|
"transactionAmount": "<amount>", /* string, e.g. "7.56" */
|
|
"transactionCurrency": "MVR",
|
|
"transactionType": "PURCHASE"
|
|
}
|
|
```
|
|
|
|
Unlike the transfer flow's `initiateFTRequest`, this endpoint does **not** take an `identifier` header field or `tPin`.
|
|
|
|
### Response — happy path
|
|
|
|
```json
|
|
[
|
|
{
|
|
"2FARequired": "NONE", /* ← the key difference */
|
|
"authenticationType": "NONE",
|
|
"success": true,
|
|
"message": "Purchase Initiated Successfully",
|
|
"response": [
|
|
{
|
|
"requestObject": { "...": "..." },
|
|
"responseObject": {
|
|
"referenceId": "685011023630",
|
|
"transactionAmount": { "amount": 7.56, "currencyCode": "MVR" },
|
|
"netAmount": { "amount": 7.56, "currencyCode": "MVR" },
|
|
"chargeDetailsDTO": { "totalFeesInTenantCurrency": { "amount": 0.0, "...": "..." }, "...": "..." },
|
|
"isCompleted": false,
|
|
"authenticationType":"NONE",
|
|
"...": "..."
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
```
|
|
|
|
Cache `referenceId` for step 3. No SMS is sent — proceed straight to confirm.
|
|
|
|
> **Defensive check:** the Thijooree client throws if it ever sees `2FARequired != "NONE"` on this endpoint so a no-op confirm can't silently complete. If Ooredoo ever turns 2FA on for QR pay, you'll see a clear error instead of a partial transaction.
|
|
|
|
---
|
|
|
|
## Step 3: `confirmNewBuy` — settle purchase
|
|
|
|
### Endpoint
|
|
|
|
```
|
|
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/confirmNewBuy
|
|
```
|
|
|
|
### Request
|
|
|
|
**Content-Type:** `application/x-www-form-urlencoded`
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| `role` | `RETAIL_SUBSCRIBER` |
|
|
| `channel` | `C03` |
|
|
| `loginExchangeKey` | From login |
|
|
| `rndValue` / `csValue` | Anti-replay derived from `formData` below |
|
|
| `formData` | `{"referenceId": "<from step 2>"}` |
|
|
| `transactionAuthDetails` | **literal string `"null"`** (not a JSON null, not an empty string — the captured request sends the four-character string `null`) |
|
|
|
|
### Response — happy path
|
|
|
|
```json
|
|
[
|
|
{
|
|
"success": true,
|
|
"message": "Payment Completed Successfully",
|
|
"response": [
|
|
{
|
|
"responseObject": {
|
|
"isCompleted": true,
|
|
"balanceInquiryDTO": {
|
|
"currencyCode": "MVR",
|
|
"pocketAmount": 92.85,
|
|
"pocketId": "<source pocket id>",
|
|
"pocketBalanceMap": { "...": "..." }
|
|
},
|
|
"status": { "replyCode": 0.0 },
|
|
"...": "..."
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
```
|
|
|
|
### Session expiry
|
|
|
|
Same envelope as elsewhere — `attributeValue: "SESSION_EXPIRED"` with HTTP 200; the client throws `MfaisaSessionExpiredException`. See [03-history.md → Session expiry](03-history.md#session-expiry).
|
|
|
|
---
|
|
|
|
## Optional: `save/smart-pay-recipient` — bookkeeping
|
|
|
|
After a successful confirm the official Ooredoo app saves the merchant to a server-side "recent recipients" list:
|
|
|
|
```
|
|
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/save/smart-pay-recipient
|
|
```
|
|
|
|
This is **not** required for the payment itself — the transfer is final once `confirmNewBuy` succeeds. Thijooree skips it; the merchant is kept in the local picker `RecentsCache` under an `mfaisaqr:<qrCodeId>` synthetic accountNumber (mirroring the BML QR `bmlqr:` scheme).
|
|
|
|
---
|
|
|
|
|
|
|
|
---
|
|
|
|
> **← Back to** [Transfer Money](04-transfer.md) | [README](README.md)
|