|
|
|
@@ -0,0 +1,247 @@
|
|
|
|
|
# Transfer Flows
|
|
|
|
|
|
|
|
|
|
The transfer screen (`TransferFragment`) handles all outgoing payments across MIB, BML, and Fahipay. This document covers how the UI routes transfers, how recipients are looked up, which combinations are allowed, and which are rejected.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Entry Points
|
|
|
|
|
|
|
|
|
|
`TransferFragment` can be launched in several modes depending on context:
|
|
|
|
|
|
|
|
|
|
| Factory method | Behaviour |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `newInstance(account, name, ...)` | Pre-fills the "To" card from a contact or recents pick |
|
|
|
|
|
| `newInstanceFrom(account)` | Pre-selects the given account in the "From" dropdown |
|
|
|
|
|
| `newInstanceFromQr(account, name, amount, remarks)` | Pre-fills recipient + optional amount/remarks from a PayMV QR scan |
|
|
|
|
|
| `newInstanceFromBmlQr(qrUrl, fromAccountNumber?)` | BML card/gateway QR merchant payment mode — locks recipient, may pre-fill amount |
|
|
|
|
|
| `newInstanceWithAutoScan()` | Opens the QR scanner immediately on load |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Account Input Detection
|
|
|
|
|
|
|
|
|
|
The raw "To" field input is normalised first (spaces stripped, `+960`/`960` country prefix removed if the result is 7 digits), then classified:
|
|
|
|
|
|
|
|
|
|
| Pattern | Type |
|
|
|
|
|
|---|---|
|
|
|
|
|
| Starts with `9`, exactly 17 digits | `MIB_ACCOUNT` |
|
|
|
|
|
| Starts with `7`, exactly 13 digits | `BML_ACCOUNT` |
|
|
|
|
|
| Starts with `7` or `9`, exactly 7 digits | `PHONE` |
|
|
|
|
|
| Starts with `A` followed by 6 digits | `NATIONAL_ID` |
|
|
|
|
|
| Contains `@` | `EMAIL` |
|
|
|
|
|
| Anything else | `UNKNOWN` |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Recipient Lookup
|
|
|
|
|
|
|
|
|
|
Lookup behaviour depends on the **source account's bank**.
|
|
|
|
|
|
|
|
|
|
### Fahipay source
|
|
|
|
|
|
|
|
|
|
Only `PHONE` input is accepted. Any other type is rejected immediately with an error on the "To" field.
|
|
|
|
|
|
|
|
|
|
Phone lookup hits both Dhiraagu and Ooredoo in parallel (order depends on the first digit):
|
|
|
|
|
|
|
|
|
|
- Numbers starting with `7`: Dhiraagu first, Ooredoo fallback
|
|
|
|
|
- Numbers starting with `9`: Ooredoo first, Dhiraagu fallback
|
|
|
|
|
|
|
|
|
|
The result maps to one or more Fahipay services:
|
|
|
|
|
|
|
|
|
|
| Carrier result | Service shown |
|
|
|
|
|
|---|---|
|
|
|
|
|
| Dhiraagu `RELOAD` | Dhiraagu Reload |
|
|
|
|
|
| Dhiraagu `BILL_PAY` | Dhiraagu Bill Pay |
|
|
|
|
|
| Ooredoo `PRE` or `HYBRID` | Raastas (prepaid top-up) |
|
|
|
|
|
| Ooredoo `POST` or `HYBRID` | Ooredoo Bill Pay |
|
|
|
|
|
|
|
|
|
|
If exactly one service matches, it is auto-selected. If multiple match (Ooredoo `HYBRID` gives two), a chip group is shown for the user to choose.
|
|
|
|
|
|
|
|
|
|
### BML source
|
|
|
|
|
|
|
|
|
|
1. If the input type is `MIB_ACCOUNT`, calls `BmlValidateClient.verifyMibAccount()`.
|
|
|
|
|
2. Otherwise calls `BmlValidateClient.validateAccount()`.
|
|
|
|
|
3. If either BML call fails and a MIB session is available, falls back to `MibTransferClient.lookup()`.
|
|
|
|
|
4. If both fail, shows the error from the MIB lookup (or a generic "account not found").
|
|
|
|
|
|
|
|
|
|
There is also a short-circuit: if the input matches a saved contact whose `transferCyDesc` is not `MVR`, the contact is used directly without a network lookup.
|
|
|
|
|
|
|
|
|
|
### MIB source
|
|
|
|
|
|
|
|
|
|
Calls `MibTransferClient.lookup()` directly. Errors from `MibLookupException` are shown verbatim to the user.
|
|
|
|
|
|
|
|
|
|
### BML-only session (no MIB session)
|
|
|
|
|
|
|
|
|
|
Falls back to `BmlValidateClient.validateAccount()` only.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Transfer Type Routing
|
|
|
|
|
|
|
|
|
|
Once the source and destination are resolved, the transfer type is determined as follows. This applies for both BML personal (`doBmlTransfer`) and BML business (`startBmlBusinessOtpFlow`) — the routing logic is identical.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Source: BML
|
|
|
|
|
|
|
|
|
|
├── isSrcCard (BML_PREPAID / BML_CREDIT / BML_DEBIT)
|
|
|
|
|
│ └── type = CAD creditAccount = dest BML CASA internalId (or dest account number)
|
|
|
|
|
│
|
|
|
|
|
├── isDestMyCard (destination is user's own BML card)
|
|
|
|
|
│ └── type = CPA creditAccount = card internalId
|
|
|
|
|
│
|
|
|
|
|
├── isDestMib && currency == MVR
|
|
|
|
|
│ └── type = DOT creditAccount = MIB account number bank = "MIB"
|
|
|
|
|
│
|
|
|
|
|
├── isDestMib && currency == USD
|
|
|
|
|
│ └── Requires a saved BML contact for that MIB account (see Rejections)
|
|
|
|
|
│ type = DOT creditAccount = contact.benefNo (numeric) bank = null
|
|
|
|
|
│
|
|
|
|
|
└── everything else (BML → BML CASA, BML → other local bank)
|
|
|
|
|
└── type = IAT creditAccount = dest account number
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Source: MIB
|
|
|
|
|
|
|
|
|
|
├── isDestMib (17-digit 9… account)
|
|
|
|
|
│ └── bankNo = 2 endpoint = transferInternal
|
|
|
|
|
│
|
|
|
|
|
└── everything else (BML or other local bank)
|
|
|
|
|
└── bankNo = 3 endpoint = transferLocal
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Source: Fahipay
|
|
|
|
|
|
|
|
|
|
└── Routed to the selected service:
|
|
|
|
|
FAHIPAY_TRANSFER, RAASTAS, OOREDOO_BILL, DHIRAAGU_RELOAD, DHIRAAGU_BILL
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Rejected Combinations
|
|
|
|
|
|
|
|
|
|
These combinations are blocked before a transfer is attempted.
|
|
|
|
|
|
|
|
|
|
### BML USD → MIB (no saved contact)
|
|
|
|
|
|
|
|
|
|
**Condition:** source is BML, currency is USD, destination is a MIB account, and no BML contact exists for that account number.
|
|
|
|
|
|
|
|
|
|
**Result:** dialog shown — "Contact required". The user must first add the MIB account as a BML contact before a USD cross-bank transfer can proceed.
|
|
|
|
|
|
|
|
|
|
> This is enforced in `initiateTransfer()` before reaching `doBmlTransfer`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### BML QR payment — non-card source
|
|
|
|
|
|
|
|
|
|
**Condition:** in BML QR merchant payment mode and the user selects a non-card account (i.e. not `BML_PREPAID`, `BML_CREDIT`, or `BML_DEBIT`) from the "From" dropdown.
|
|
|
|
|
|
|
|
|
|
**Result:** selection is rejected with a toast: "Unsupported for BML QR — select a card". The dropdown resets.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Fahipay — non-phone destination
|
|
|
|
|
|
|
|
|
|
**Condition:** source is Fahipay and the input type is anything other than `PHONE`.
|
|
|
|
|
|
|
|
|
|
**Result:** inline error on the "To" field: "Only phone numbers are supported for Fahipay transfers."
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### No source account selected
|
|
|
|
|
|
|
|
|
|
**Condition:** user taps the lookup button or the transfer button without selecting a "From" account.
|
|
|
|
|
|
|
|
|
|
**Result:** toast: "Please select a source account first."
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Inactive BML card as source
|
|
|
|
|
|
|
|
|
|
**Condition:** a BML card (`BML_PREPAID`, `BML_CREDIT`, `BML_DEBIT`) with `statusDesc != "Active"` appears in the dropdown but is not selectable — `getAccount()` returns `null` for it and `isEnabled()` returns `false`.
|
|
|
|
|
|
|
|
|
|
**Result:** the row is shown at 40% opacity and cannot be tapped.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Missing internalId
|
|
|
|
|
|
|
|
|
|
**Condition:** a BML source account has a blank `internalId` (needed as the `debitAccount` in BML API calls).
|
|
|
|
|
|
|
|
|
|
**Result:** transfer is aborted with a toast: "Missing internal account ID — please refresh your accounts."
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Warnings (allowed but flagged)
|
|
|
|
|
|
|
|
|
|
These combinations proceed after user confirmation but show a prominent red warning in the confirm dialog.
|
|
|
|
|
|
|
|
|
|
### USD source → MVR destination
|
|
|
|
|
|
|
|
|
|
> "You are transferring from a USD account to an MVR account. The currency will be converted at the bank's rate and this cannot be reversed!"
|
|
|
|
|
|
|
|
|
|
**Condition:** `src.currencyName == "USD"` and the resolved destination account's currency is `MVR`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### BML credit card as source
|
|
|
|
|
|
|
|
|
|
> "Transferring from a credit card is treated as a cash advance. Cash advance fees will be charged on the 10th of the month."
|
|
|
|
|
|
|
|
|
|
**Condition:** `src.profileType == "BML_CREDIT"`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## BML Business Profile OTP Flow
|
|
|
|
|
|
|
|
|
|
Business profiles use a manual OTP delivered via email or SMS rather than a TOTP seed. The flow replaces the standard single-step confirm:
|
|
|
|
|
|
|
|
|
|
1. **Initiate** — `startBmlBusinessOtpFlow()` calls `BmlAccountClient.fetchTransferChannels()` to list available channels (email, SMS).
|
|
|
|
|
2. **Channel selection** — a channel picker is shown inline. Transfer fields are locked (dimmed, disabled).
|
|
|
|
|
3. **Initiate with channel** — `BmlTransferClient.initiateTransfer()` is called with the chosen channel, which triggers the OTP dispatch.
|
|
|
|
|
4. **OTP entry** — an OTP input field appears. The transfer button label changes to "Verify Payment".
|
|
|
|
|
5. **Confirm** — `BmlTransferClient.confirmTransfer()` is called with the entered OTP (not a generated TOTP).
|
|
|
|
|
|
|
|
|
|
If channel fetch fails or returns empty, the flow is aborted and the form is re-enabled.
|
|
|
|
|
|
|
|
|
|
**Profile detection:** `isBusinessProfile()` checks `bmlProfilesMap[loginId]` for a profile entry matching `src.profileId` with `profileType == "business"`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## BML QR Merchant Payment Flow
|
|
|
|
|
|
|
|
|
|
Triggered when the transfer screen is opened via `newInstanceFromBmlQr()` or when a BML ebanking/pay.bml URL is scanned from the QR scanner.
|
|
|
|
|
|
|
|
|
|
Two sub-modes:
|
|
|
|
|
|
|
|
|
|
| Mode | Trigger | Extra step |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| Static card QR | URL starts with `https://ebanking.bankofmaldives.com.mv/qrpay/` | None |
|
|
|
|
|
| Gateway QR | URL starts with `https://pay.bml.com.mv/app/` | `BmlQrPayClient.preInitiatePayment()` required before initiate |
|
|
|
|
|
|
|
|
|
|
Flow:
|
|
|
|
|
1. `lookupBmlQrMerchant()` — fetches merchant info via `BmlQrPayClient.lookupPayRequest()`. Locks the "To" row.
|
|
|
|
|
2. For dynamic QRs (`info.amount > 0`), pre-fills the amount and locks the amount field.
|
|
|
|
|
3. Remarks field is locked (not applicable for merchant payments).
|
|
|
|
|
4. On confirm: TOTP is generated, then `initiatePayment()` → (for gateway QR: `preInitiatePayment()` first) → `confirmPayment()` with a fresh TOTP.
|
|
|
|
|
5. On success: a success dialog is shown (no receipt saved). Back-press returns to previous screen.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Transfer Button Enable Conditions
|
|
|
|
|
|
|
|
|
|
The transfer button is only enabled when all of the following are true:
|
|
|
|
|
|
|
|
|
|
- A source account is selected
|
|
|
|
|
- A recipient is resolved (`resolvedAccountNumber` not blank, or `bmlQrInfo` is set)
|
|
|
|
|
- Amount is greater than `0`
|
|
|
|
|
- No connectivity error for `NO_INTERNET` or for the source bank
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
[Back to app docs index](README.md)
|