This commit is contained in:
+209
-149
@@ -2,197 +2,244 @@
|
||||
|
||||
Two-step process: look up the recipient to validate the account and get the holder name, then execute the transfer.
|
||||
|
||||
All endpoints are on the WebView subdomain. See [README](README.md) for cookie and AJAX header format.
|
||||
|
||||
```
|
||||
Referer: https://faisamobilex-wv.mib.com.mv/transfer/quick
|
||||
```
|
||||
All endpoints are on the encrypted API (`sfunc=n`, session key). See [README](README.md) for the request format. The source account must belong to the **currently selected** profile — call `P47` for its profile first (see [03-accounts.md](03-accounts.md)).
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Account Lookup
|
||||
## Step 1 — Account Lookup — `routePath: B40`
|
||||
|
||||
The lookup endpoint depends on the format of the input:
|
||||
One route resolves every supported input:
|
||||
|
||||
| Input format | Endpoint | Body field |
|
||||
| Input format | Example | Resolves to |
|
||||
|---|---|---|
|
||||
| Starts with `7`, exactly 13 digits | `AjaxAlias/getIPSAccount` | `benefAccount` |
|
||||
| Starts with `9`, exactly 17 digits | `ajaxBeneficiary/getAccountName` | `accountNo` |
|
||||
| Starts with `7` or `9`, exactly 7 digits | `AjaxAlias/getAlias` | `aliasName` |
|
||||
| Starts with `A` followed by 6 digits | `AjaxAlias/getAlias` | `aliasName` |
|
||||
| Contains `@` (email) | `AjaxAlias/getAlias` | `aliasName` |
|
||||
| Starts with `7`, exactly 13 digits | `7700000000000` | BML / local bank account |
|
||||
| Starts with `9`, exactly 17 digits | `90100000000000000` | MIB account |
|
||||
| Starts with `7` or `9`, exactly 7 digits | `9600001` | Favara mobile alias |
|
||||
| `A` followed by 6 digits | `A000001` | Favara National ID alias |
|
||||
| Contains `@` | `user@example.com` | Favara email alias |
|
||||
|
||||
---
|
||||
|
||||
### 1a. IPS Account — BML / local bank (13 digits, starts with `7`)
|
||||
|
||||
```
|
||||
POST https://faisamobilex-wv.mib.com.mv/AjaxAlias/getIPSAccount
|
||||
```
|
||||
|
||||
Body: `benefAccount=7700000000000`
|
||||
|
||||
**Response**:
|
||||
**Request**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"responseCode": "2",
|
||||
"accountName": "ACCOUNT HOLDER NAME",
|
||||
"bankBic": "MALBMVMV"
|
||||
}
|
||||
```
|
||||
|
||||
- `accountName` — account holder name
|
||||
- `bankBic` — bank BIC (e.g. `MALBMVMV` for BML)
|
||||
- Account number is the input itself — not returned in response
|
||||
|
||||
Use `bankNo=3` and `transferLocal` for the transfer.
|
||||
|
||||
> **USD cross-bank accounts**: `getIPSAccount` only succeeds for **MVR** cross-bank accounts. A 13-digit `7…` account that is denominated in USD returns `success: false` here and cannot be resolved via IPS at all. The client hardcodes `currency = "MVR"` for IPS results (`MibTransferClient.kt:135-137`). For USD BML→MIB transfers the user must first save a BML contact (see Notes below).
|
||||
|
||||
---
|
||||
|
||||
### 1b. MIB Internal Account (17 digits, starts with `9`)
|
||||
|
||||
```
|
||||
POST https://faisamobilex-wv.mib.com.mv/ajaxBeneficiary/getAccountName
|
||||
```
|
||||
|
||||
Body: `accountNo=90100000000000000`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"accountName": "ACCOUNT HOLDER NAME",
|
||||
"currencyCode": "462"
|
||||
}
|
||||
```
|
||||
|
||||
- `accountName` may be at root level or inside a `data` object — check both (`MibTransferClient.kt:160-161`)
|
||||
- `currencyCode` may also be at root level or inside `data` (`MibTransferClient.kt:162-163`). `"462"` = MVR, `"840"` = USD. The client maps this into `MibIpsAccountInfo.currency` ∈ `{"MVR", "USD", ""}` — this is the **MIB→MIB USD detection** fix from commit `16fd909`.
|
||||
- Bank is always MIB (`MADVMVMV`)
|
||||
|
||||
Use `bankNo=2` and `transferInternal` for the transfer.
|
||||
|
||||
---
|
||||
|
||||
### 1c. Favara Alias — shortcodes, A-IDs, emails
|
||||
|
||||
```
|
||||
POST https://faisamobilex-wv.mib.com.mv/AjaxAlias/getAlias
|
||||
```
|
||||
|
||||
Body: `aliasName=<alias>`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"responseCode": "2",
|
||||
"sfunc": "n",
|
||||
"xxid": "<session xxid>",
|
||||
"data": {
|
||||
"BfyNm": "Account Holder Name",
|
||||
"CdtrAcct": {
|
||||
"Acct": "90100000000000000",
|
||||
"FinInstnId": "MADVMVMV"
|
||||
}
|
||||
"search": "7700000000000",
|
||||
"nonce": "<computed nonce>",
|
||||
"appId": "<appId>",
|
||||
"sodium": "<random 20-bit int>",
|
||||
"routePath": "B40",
|
||||
"xxid": "<session xxid>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `BfyNm` — beneficiary name (trim whitespace)
|
||||
- `CdtrAcct.Acct` — resolved account number to use for the transfer
|
||||
- `CdtrAcct.FinInstnId` — bank BIC (`MADVMVMV` = MIB, `MALBMVMV` = BML)
|
||||
### Input Rules
|
||||
|
||||
Use `bankNo=2` (MIB) or `3` (BML/local) depending on `FinInstnId`, and the matching transfer endpoint.
|
||||
- **Mobile aliases** must be the bare 7 digits. `+960 9600001` and `+9609600001` return no match.
|
||||
- **National ID aliases** need an uppercase `A` — `a000001` returns no match, `A000001` resolves. The client uppercases the prefix before sending (`MibTransferClient.kt`).
|
||||
- Partial input (e.g. `9600`) returns an empty `data` array.
|
||||
|
||||
---
|
||||
|
||||
### Lookup Errors
|
||||
|
||||
All three lookup endpoints return `success: false` with a human-readable `reasonText` on failure:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"reasonText": "Account not found"
|
||||
}
|
||||
```
|
||||
|
||||
Always show `reasonText` directly to the user.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Execute Transfer
|
||||
|
||||
Two endpoints depending on the destination bank:
|
||||
|
||||
| `bankNo` | Endpoint | Destination |
|
||||
|---|---|---|
|
||||
| `2` | `ajaxTransfer/transferInternal` | MIB internal account |
|
||||
| `3` | `ajaxTransfer/transferLocal` | BML or other local bank |
|
||||
|
||||
```
|
||||
POST https://faisamobilex-wv.mib.com.mv/ajaxTransfer/transferInternal
|
||||
POST https://faisamobilex-wv.mib.com.mv/ajaxTransfer/transferLocal
|
||||
```
|
||||
|
||||
### Request Body (form-urlencoded)
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `benefName` | Recipient name (from lookup) |
|
||||
| `benefNo` | `0` (not a saved contact) |
|
||||
| `fromAccountNo` | Source account number |
|
||||
| `benefAccountNo` | Destination account number |
|
||||
| `transferCy` | Currency numeric code (`"462"` = MVR, `"840"` = USD) |
|
||||
| `benefCurrencyCode` | Same as `transferCy` |
|
||||
| `amount` | Amount as string (e.g. `"100.00"`) |
|
||||
| `bankNo` | `2` = MIB internal, `3` = local/BML |
|
||||
| `purpose` | Transfer purpose; send `"-"` if blank |
|
||||
| `otp` | OTP from the user |
|
||||
| `otpType` | `"3"` (SMS/authenticator OTP) |
|
||||
|
||||
### Response — Success
|
||||
### Response
|
||||
|
||||
**BML account**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"trxId": "TRX20260516001",
|
||||
"date": "2026-05-16 15:10:25"
|
||||
"benefName": "ACCOUNT HOLDER NAME",
|
||||
"benefAccount": "7700000000000",
|
||||
"bankCode": "MALBMVMV",
|
||||
"benefBankName": "Bank of Maldives"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**MIB account**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"benefName": "ACCOUNT HOLDER NAME",
|
||||
"benefAccount": "90100000000000000",
|
||||
"bankCode": "MIB",
|
||||
"benefBankName": "MIB",
|
||||
"transferCy": "462",
|
||||
"transferCyDesc": "MVR"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Favara alias** (resolving to an MIB account):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"benefName": "Account Holder Name ",
|
||||
"benefAccount": "90100000000000000",
|
||||
"bankCode": "MADVMVMV",
|
||||
"benefBankName": "Maldives Islamic Bank PLC."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `benefName` | Account holder name — trim whitespace |
|
||||
| `benefAccount` | Resolved account number to use for the transfer |
|
||||
| `bankCode` | `"MIB"` or a bank BIC (`MADVMVMV` = MIB, `MALBMVMV` = BML). Normalize `"MIB"` to `MADVMVMV`. |
|
||||
| `benefBankName` | Bank display name |
|
||||
| `transferCy` / `transferCyDesc` | Account currency — present for MIB account lookups only |
|
||||
|
||||
### Lookup Errors
|
||||
|
||||
No match returns `success: true` with an empty `data` array:
|
||||
|
||||
```json
|
||||
{ "success": true, "data": [] }
|
||||
```
|
||||
|
||||
Show "account not found" in that case.
|
||||
|
||||
### Currency
|
||||
|
||||
| Lookup | Currency |
|
||||
|---|---|
|
||||
| MIB account | From `transferCyDesc` (`"MVR"` / `"USD"`) |
|
||||
| Favara alias → MIB | Not returned — unknown |
|
||||
| BML / local | `"MVR"` — cross-bank lookups only resolve MVR accounts |
|
||||
|
||||
> **USD cross-bank accounts**: a BML account denominated in USD cannot be resolved here. For USD BML→MIB transfers the user must first save a BML contact (see Notes below).
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Execute Transfer
|
||||
|
||||
Two routes depending on the destination bank:
|
||||
|
||||
| `bankNo` | `routePath` | Destination |
|
||||
|---|---|---|
|
||||
| `2` | `T50` | MIB internal account |
|
||||
| `3` | `T51` | BML or other local bank (via Favara) |
|
||||
|
||||
**Request**:
|
||||
```json
|
||||
{
|
||||
"sfunc": "n",
|
||||
"xxid": "<session xxid>",
|
||||
"data": {
|
||||
"benefName": "ACCOUNT HOLDER NAME",
|
||||
"benefNo": "0",
|
||||
"fromAccountNo": "90101000000001000",
|
||||
"benefAccountNo": "7700000000000",
|
||||
"transferCy": "462",
|
||||
"benefCurrencyCode": "462",
|
||||
"amount": "100.00",
|
||||
"bankNo": "3",
|
||||
"purpose": "",
|
||||
"otp": "123456",
|
||||
"otpType": "3",
|
||||
"nonce": "<computed nonce>",
|
||||
"appId": "<appId>",
|
||||
"sodium": "<random 20-bit int>",
|
||||
"routePath": "T51",
|
||||
"xxid": "<session xxid>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `benefName` | Recipient name (from lookup) |
|
||||
| `benefNo` | `"0"` (not a saved contact) |
|
||||
| `fromAccountNo` | Source account number |
|
||||
| `benefAccountNo` | Destination account number |
|
||||
| `transferCy` | Currency numeric code (`"462"` = MVR, `"840"` = USD) |
|
||||
| `benefCurrencyCode` | Same as `transferCy` |
|
||||
| `amount` | Amount as string (e.g. `"100.00"`, `".01"`) |
|
||||
| `bankNo` | `"2"` = MIB internal, `"3"` = local/BML |
|
||||
| `purpose` | Transfer remarks; may be empty |
|
||||
| `otp` | Current TOTP code |
|
||||
| `otpType` | `"3"` (authenticator OTP) |
|
||||
|
||||
### Response — Success
|
||||
|
||||
**`T50`** (internal):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"responseCode": "2",
|
||||
"reasonCode": "2011",
|
||||
"reasonText": "Internal transfer success",
|
||||
"data": [
|
||||
{ "date": "2026-05-16 15:10:25", "trxId": "97999863", "availableBalance": "37.41" }
|
||||
],
|
||||
"accountBalance": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
**`T51`** (local):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"responseCode": "2",
|
||||
"reasonCode": "2011",
|
||||
"reasonText": "Local transfer completed successfully via Favara",
|
||||
"data": [
|
||||
{ "date": "2026-05-16 15:10:25", "trxId": "817054125" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `trxId` | Transaction ID |
|
||||
| `date` | Completion timestamp |
|
||||
| `availableBalance` | `T50` only — source account balance after the transfer |
|
||||
| `accountBalance` | `T50` only — refreshed balances for the selected profile (same shape as in [03-accounts.md](03-accounts.md)) |
|
||||
|
||||
### Response — Failure
|
||||
|
||||
Failures return HTTP `400` with an encrypted body:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"reasonText": "Insufficient balance"
|
||||
"responseCode": "1",
|
||||
"reasonCode": "213",
|
||||
"reasonText": "Invalid OTP",
|
||||
"data": [ { "otpId": "50487016" } ]
|
||||
}
|
||||
```
|
||||
|
||||
`reasonText` contains the error reason. On HTTP `419`, the session has expired — re-login required.
|
||||
| `reasonCode` | `reasonText` | Meaning |
|
||||
|---|---|---|
|
||||
| `213` | `Invalid OTP` | Wrong or stale OTP |
|
||||
| `707` | `That OTP is already used!` | The OTP was already accepted for an earlier request |
|
||||
| `201` | `Transaction currency does not match From Account currency!` | `transferCy` doesn't match the source account |
|
||||
|
||||
Show `reasonText` to the user.
|
||||
|
||||
### OTP Retry
|
||||
|
||||
On `213` or `707` the client waits for the next 30-second TOTP window (plus 1 second for clock skew), generates a new code and resends the transfer **once**. A second failure of any kind is shown to the user (`MibTransferHandler.kt`).
|
||||
|
||||
---
|
||||
|
||||
## Transfer Type Summary
|
||||
|
||||
| Recipient | Lookup endpoint | `bankNo` | Transfer endpoint |
|
||||
| Recipient | Lookup | `bankNo` | Transfer |
|
||||
|---|---|---|---|
|
||||
| MIB (17-digit `9…`) | `getAccountName` | `2` | `transferInternal` |
|
||||
| BML (13-digit `7…`) | `getIPSAccount` | `3` | `transferLocal` |
|
||||
| Favara alias → MIB | `getAlias` | `2` | `transferInternal` |
|
||||
| Favara alias → BML | `getAlias` | `3` | `transferLocal` |
|
||||
| MIB (17-digit `9…`) | `B40` | `2` | `T50` |
|
||||
| BML (13-digit `7…`) | `B40` | `3` | `T51` |
|
||||
| Favara alias → MIB | `B40` | `2` | `T50` |
|
||||
| Favara alias → BML | `B40` | `3` | `T51` |
|
||||
|
||||
---
|
||||
|
||||
@@ -205,23 +252,36 @@ POST https://faisamobilex-wv.mib.com.mv/ajaxTransfer/transferLocal
|
||||
|
||||
---
|
||||
|
||||
## Other Transfer-Screen Routes
|
||||
|
||||
Called by the official app when opening the transfer screen. Not consumed by thijooree.
|
||||
|
||||
| `routePath` | Request | Returns |
|
||||
|---|---|---|
|
||||
| `T63` | — | Currencies with rates (`numericCode`, `code`, `name`, `rate`) |
|
||||
| `T66` | `currencyCode` | Sell rate for a currency (`sellRate`) |
|
||||
| `T70` | — | Upload settings (`numFiles`, `fileSize`) |
|
||||
| `T71` | optional `transferCy` | AML thresholds (`eddAmount`, `cddAmount`, `currencyCode`) |
|
||||
|
||||
---
|
||||
|
||||
## `MibIpsAccountInfo` (client model)
|
||||
|
||||
All three lookups return this unified structure (`MibModels.kt:42-47`):
|
||||
`B40` lookups are mapped into this structure (`MibModels.kt`):
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `accountName` | Account holder name (trimmed) |
|
||||
| `accountNumber` | Resolved account number |
|
||||
| `bankId` | Bank BIC (`MADVMVMV` = MIB, `MALBMVMV` = BML, etc.) |
|
||||
| `currency` | `"MVR"`, `"USD"`, or `""` (unknown). Populated from `currencyCode` for MIB internal lookups; hardcoded `"MVR"` for IPS lookups; default `""` for alias lookups. |
|
||||
| `currency` | `"MVR"`, `"USD"`, or `""` (unknown) — see Currency above |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **BML → MIB USD transfers** require a saved BML contact first. Because `getIPSAccount` rejects USD accounts (`success: false`), the app cannot validate the BML USD account number directly. The workaround in `TransferFragment.kt` is to call `MibContactsClient.createContact` (see [09-contacts.md](09-contacts.md)) to auto-add the BML account as a beneficiary, then transfer to that beneficiary. Introduced in commit `16fd909`.
|
||||
- **Session expiry**: HTTP `419` on either lookup or transfer means the session expired. See [README](README.md) for the unified expiry detection rules.
|
||||
- **BML → MIB USD transfers** require a saved BML contact first. Because USD BML accounts can't be resolved via `B40`, the app cannot validate the account number directly. The workaround in `TransferFragment.kt` is to add the BML account as a beneficiary (see [09-contacts.md](09-contacts.md)), then transfer to that beneficiary.
|
||||
- **Session expiry**: see [README](README.md) for the unified expiry detection rules.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user