Files
thijooree/docs/mfaisaapi/04-transfer.md
T
shihaam 43f3cca2aa
Auto Tag on Version Change / check-version (push) Failing after 13m41s
ooredoo mfaisa
2026-06-27 14:25:46 +05:00

283 lines
9.1 KiB
Markdown

# Transfer Money (Wallet-to-Wallet)
Send MVR from the user's M-Faisa pocket to another M-Faisa subscriber, identified by phone number.
There is no "account number" concept on M-Faisa — recipients are addressed by mobile number, and the server resolves the destination pocket itself. The flow is three calls, ending with an OTP delivered by SMS to the sender.
> **Currency / pocket constraints:** Thijooree only sends from the user's MVR (EMONEY) pocket to the recipient's MVR pocket. PayPal-USD pockets are out of scope (the HAR captures don't cover them and we have no Frida-extracted recipe).
---
## Flow
```
Client Server
| |
| POST /Pocket/basicBeneDetails | ← user typed a phone and tapped 🔍
| formData = { beneficaryDetails, initiator } |
|--------------------------------------------->|
| [{ success, response:[[pocket, pocket,…]]}] |
|<---------------------------------------------|
| |
| (show recipient name, accept amount + remarks)
| |
| POST /initiateFTRequest |
| formData = { sourceDetails, recipient, |
| transactionAmount, … } |
|--------------------------------------------->|
| { 2FARequired:"OTP", response:[{ |
| responseObject:{ referenceId, |
| chargeDetails, … } }] } |
|<---------------------------------------------|
| |
| (server sends SMS OTP to sender's phone)
| (user types OTP) |
| |
| POST /confirmFTRequest |
| formData = { referenceId } |
| transactionAuthDetails = { OTP encrypted } |
|--------------------------------------------->|
| { success:true, |
| message:"Transfer Completed Successfully" }
|<---------------------------------------------|
```
All three endpoints carry the same anti-replay pair (`rndValue` + `csValue`) derived from the request's `formData` JSON — see [01-encryption.md → rndValue / csValue](01-encryption.md#anti-replay-envelope-rndvalue--csvalue).
---
## Step 1: `Pocket/basicBeneDetails` — recipient lookup
### Endpoint
```
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/Pocket/basicBeneDetails
```
### Request
**Content-Type:** `application/x-www-form-urlencoded`
| Field | Value |
|---|---|
| `role` | `RETAIL_SUBSCRIBER` |
| `channel` | `SubscriberApp` |
| `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
{
"beneficaryDetails": {
"MDNId": "<encryptMobile(recipientMsisdn), base64>",
"actorRoleType": "RETAIL_SUBSCRIBER"
},
"initiatorDetailsDTO": {
"initiatingMDN": "<encryptMobile(myMsisdn), base64>",
"initiatingRoleId": "<my suscriberId>",
"initiatorRole": "RETAIL_SUBSCRIBER"
}
}
```
> `suscriberId` (note the server's typo) comes from the top level of the `doMobileLogin` response — see [02-login.md](02-login.md#step-2-domobilelogin).
### Response — happy path
```json
[
{
"success": true,
"message": "Operation Completed Successfully",
"response": [
[
{ "pocketId": "<paypal pocket id>", "pocketCurrency": "USD",
"pocketValueType": "PAYPAL_USD", "name": "<Recipient Name>", "MDNId": "<recipient msisdn>",
"walletId": "<recipient wallet id>", "actorId": "<recipient actor id>", "...": "..." },
{ "pocketId": "<mvr pocket id>", "pocketCurrency": "MVR",
"pocketValueType": "EMONEY", "name": "<Recipient Name>", "MDNId": "<recipient msisdn>",
"walletId": "<recipient wallet id>", "actorId": "<recipient actor id>", "...": "..." }
]
]
}
]
```
Note the nesting — `response` is an array (one element only seen in practice) of arrays of pocket objects (one per pocket the recipient owns).
### Response — recipient not found
```json
[{ "success": false, "message": "Pocket details not found." }]
```
---
## Step 2: `initiateFTRequest` — initiate, server SMSes OTP
### Endpoint
```
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/initiateFTRequest
```
### Request
**Content-Type:** `application/x-www-form-urlencoded`
| Field | Value |
|---|---|
| `identifier` | `<encryptMobile(recipientMsisdn), base64>` — independent encryption from `formData.MDNId` |
| `role` | `RETAIL_SUBSCRIBER` |
| `transferMode` | `MOBILE` |
| `channel` | **`C03`** (the top-level value differs from the inner `formData.channel`, which is `SubscriberApp`) |
| `tPin` | empty string `""` (a relic — the OTP step authenticates) |
| `loginExchangeKey` | From login |
| `rndValue` / `csValue` | Standard anti-replay (derived from the `formData` below) |
| `formData` | JSON below |
```json
{
"MDNId": "960<recipientMsisdn>", /* PLAINTEXT '960' + recipient phone */
"beneDetails": {
"miscDetails": "<remarks>",
"transferMode":"MOBILE"
},
"channel": "SubscriberApp",
"commodityType": "WALLET",
"description": "<remarks>",
"inputDetailsDTO": { "deviceId": "…", "simId": "…" },
"mfs-transactionType": "send-money-to-mobile",
"pocketId": "",
"sourceDetails": {
"MDNId": "960<myMsisdn>", /* PLAINTEXT '960' + my phone */
"actorRoleType":"RETAIL_SUBSCRIBER",
"pocketId": "<my source pocket id>" /* from login.pocketDetails[0].pocketSummaryDetailsArrayDTO */
},
"transactionAmount": "<amount>", /* string, MVR */
"transactionCurrency":"MVR",
"transferMode": "MOBILE"
}
```
`deviceId` and `simId` are both `Settings.Secure.ANDROID_ID` in Thijooree's implementation — matching the device-info pattern from login.
### Response — happy path
```json
{
"2FARequired": "OTP",
"authenticationType": "OTP",
"success": true,
"message": "Operation Completed Successfully",
"response": [
{
"requestObject": { "...": "..." },
"responseObject": {
"referenceId": "<reference id>",
"transactionAmount": { "amount": 1.0, "currencyCode": "MVR" },
"netAmount": { "amount": 1.0, "currencyCode": "MVR" },
"chargeDetailsDTO": { "totalFeesInTenantCurrency": { "amount": 0.0, "...": "..." }, "...": "..." },
"isCompleted": false,
"...": "..."
}
}
]
}
```
The server SMSes a 6-digit OTP to the sender's phone immediately. Cache `referenceId` for step 3.
---
## Step 3: `confirmFTRequest` — submit OTP
### Endpoint
```
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/confirmFTRequest
```
### 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` | JSON below |
```json
{
"authenticationType": "OTP",
"authenticationValue": "<encryptPin(otpCode), hex>",
"otpTransactionType": "TRANSACTION",
"referenceId": "<from step 2>"
}
```
The OTP code is encrypted with the same `encryptPin` routine used for the mPIN — i.e. RSA-OAEP-SHA1 against the 2048-bit mPin key, with a fresh 6-character salt. See [01-encryption.md](01-encryption.md#mpin-key-2048-bit).
### Response — happy path
```json
{
"success": true,
"message": "Transfer Completed Successfully.",
"response": [
{
"responseObject": {
"isCompleted": true,
"balanceInquiryDTO": {
"currencyCode": "MVR",
"pocketAmount": 0.45,
"pocketId": "<source pocket id>",
"pocketBalanceMap": { "...": "..." }
},
"status": { "replyCode": 0.0, "replyText": "Success" },
"...": "..."
}
}
]
}
```
### Response — wrong OTP
The server returns its standard error envelope as a JSON array:
```json
[{
"success": false, "message": "validation errors",
"error": [{ "attributeName":"OTP", "errorMessage":"<details>" }]
}]
```
`MfaisaTransferClient` parses this into [`MfaisaInvalidOtpException`](../../app/src/main/java/sh/sar/basedbank/api/mfaisa/MfaisaModels.kt) so the caller can re-prompt without losing the `referenceId`.
### 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).
---
## curl Reference
```bash
# Step 1 (search a recipient)
python tmp/mfaisa_transfer.py <myMsisdn> <myMpin> <recipientMsisdn>
# Steps 2 + 3 require a live phone OTP and are documented in tmp/mfaisa_transfer.py
```
---
&nbsp;
---
> **← Back to** [Transaction History](03-history.md) | [README](README.md)