Files
fksar/docs/thijooree/20-transfer-flows.md
T
2026-06-13 21:30:12 +05:00

10 KiB

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(accountNumber, displayName, subtitle, colorHex, imageHash) Pre-fills the "To" card from a contact, recents pick, or About → Donate
newInstanceFrom(account: BankAccount) Pre-selects the given account in the "From" dropdown
newInstanceFromQr(accountNumber, displayName, amount, remarks, fromAccountNumber?) 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.

Destination currency resolution

resolvedDestCurrency (TransferFragment.kt:89) holds the currency reported by the lookup, defaulting to "" until verified. When BML returns nothing useful but a MIB session exists, the MIB lookup is used as a fallback to verify the destination currency — this is what allows the BML USD → MIB rejection dialog to know whether it can pre-fill the recipient name and currency into the auto-add contact sheet.


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 (TransferFragment.kt:1136-1164).

Result: "Contact required" dialog with a Save button that opens AddContactSheetFragment.newInstance(bmlProfileId, accountNumber, recipientName, currency) pre-filled. If resolvedDestCurrency was verified via the MIB fallback lookup the recipient name and currency are passed through; otherwise the user fills them in manually. After saving the contact the user can retry the transfer.

The dialog body text is switched based on whether the destination currency was verified (R.string.transfer_bml_contact_required_msg_bml_limit vs. R.string.transfer_bml_contact_required_msg).


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. InitiatestartBmlBusinessOtpFlow() 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 channelBmlTransferClient.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. ConfirmBmlTransferClient.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

 


← Account Parser Architecture