283 lines
9.1 KiB
Markdown
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
|
|
```
|
|
|
|
---
|
|
|
|
|
|
|
|
---
|
|
|
|
> **← Back to** [Transaction History](03-history.md) | [README](README.md)
|