This commit is contained in:
@@ -279,4 +279,4 @@ python tmp/mfaisa_transfer.py <myMsisdn> <myMpin> <recipientMsisdn>
|
||||
|
||||
---
|
||||
|
||||
> **← Back to** [Transaction History](03-history.md) | [README](README.md)
|
||||
> **← Back to** [Transaction History](03-history.md) | [README](README.md) | **Next →** [QR Merchant Payment](05-qr-pay.md)
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
# 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)
|
||||
@@ -88,6 +88,7 @@ Client Server
|
||||
| 2 | [Login](02-login.md) | Subscriber lookup + mPIN login |
|
||||
| 3 | [Transaction History](03-history.md) | Paginated history per session |
|
||||
| 4 | [Transfer Money](04-transfer.md) | Three-step wallet-to-wallet send: recipient lookup → initiate (server SMSes OTP) → confirm |
|
||||
| 5 | [QR Merchant Payment](05-qr-pay.md) | Three-step "smart pay" scan-to-merchant: QR lookup → initiate → confirm. **No OTP** (`2FARequired=NONE`) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user