diff --git a/docs/mfaisaapi/04-transfer.md b/docs/mfaisaapi/04-transfer.md index 65d1659..d619718 100644 --- a/docs/mfaisaapi/04-transfer.md +++ b/docs/mfaisaapi/04-transfer.md @@ -279,4 +279,4 @@ python tmp/mfaisa_transfer.py --- -> **← 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) diff --git a/docs/mfaisaapi/05-qr-pay.md b/docs/mfaisaapi/05-qr-pay.md new file mode 100644 index 0000000..3d73770 --- /dev/null +++ b/docs/mfaisaapi/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": "", + "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) diff --git a/docs/mfaisaapi/README.md b/docs/mfaisaapi/README.md index e673ec7..ed6dcd5 100644 --- a/docs/mfaisaapi/README.md +++ b/docs/mfaisaapi/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`) | ---