# 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": "", "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": "", /* 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": "", /* free text, may be empty */ "merchantId": "", "mobileNumber": "", "sourceDetails": { "MDNId": "960", /* PLAINTEXT — '960' + my phone */ "actorRoleType": "RETAIL_SUBSCRIBER", "pocketId": "" /* EMONEY pocket from login */ }, "transactionAmount": "", /* 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": ""}` | | `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": "", "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:` synthetic accountNumber (mirroring the BML QR `bmlqr:` scheme). ---   --- > **← Back to** [Transfer Money](04-transfer.md) | [README](README.md)