fix some currency detection issues with MIB to MIB, and also Option to automatically add contact during BML to MIB USD transfer #35 (its not fully auto)
Auto Tag on Version Change / check-version (push) Failing after 13m51s

This commit is contained in:
2026-06-13 19:02:34 +05:00
parent a95ca0e7a5
commit 16fd909c7f
6 changed files with 139 additions and 18 deletions
@@ -71,7 +71,8 @@ class BmlValidateClient {
originalInput = account,
name = root.optString("name"),
alias = null,
currency = "MVR",
// BML's MIB verify endpoint doesn't return the MIB account's currency.
currency = "",
agnt = root.optString("agnt").takeIf { it.isNotBlank() }
)
} catch (_: Exception) { null }
@@ -42,7 +42,8 @@ data class MibTransferResult(
data class MibIpsAccountInfo(
val accountName: String,
val accountNumber: String,
val bankId: String
val bankId: String,
val currency: String = "" // "MVR", "USD", or "" if unknown
)
@@ -130,7 +130,10 @@ class MibTransferClient {
MibIpsAccountInfo(
accountName = json.optString("accountName").trim(),
accountNumber = accountNumber,
bankId = json.optString("bankBic")
bankId = json.optString("bankBic"),
// MIB IPS only returns success for MVR cross-bank accounts;
// USD cross-bank accounts fail this lookup entirely.
currency = "MVR"
)
}
}
@@ -156,10 +159,18 @@ class MibTransferClient {
// accountName may be at root or inside a "data" object
val name = json.optString("accountName").takeIf { it.isNotBlank() }
?: json.optJSONObject("data")?.optString("accountName") ?: ""
val currencyCode = json.optString("currencyCode").takeIf { it.isNotBlank() }
?: json.optJSONObject("data")?.optString("currencyCode") ?: ""
val currency = when (currencyCode) {
"840" -> "USD"
"462" -> "MVR"
else -> ""
}
MibIpsAccountInfo(
accountName = name.trim(),
accountNumber = accountNumber,
bankId = "MADVMVMV" // MIB
bankId = "MADVMVMV", // MIB
currency = currency
)
}
}
@@ -89,6 +89,52 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
categories = cats.filter { it.id != "BML" }
if (selectedDest?.isBml == false) setupCategoryDropdown()
}
applyPrefillArgs()
}
private fun applyPrefillArgs() {
val args = arguments ?: return
val bmlProfileId = args.getString(ARG_BML_PROFILE_ID)
val accountNumber = args.getString(ARG_ACCOUNT_NUMBER)
val recipientName = args.getString(ARG_RECIPIENT_NAME)
val currency = args.getString(ARG_CURRENCY)
if (bmlProfileId != null) {
val match = destinations.firstOrNull { it.isBml && it.bmlLoginId == bmlProfileId }
if (match != null) {
selectedDest = match
binding.actvDestination.setText(match.label, false)
updateMibOnlyVisibility()
}
}
if (accountNumber != null) {
binding.etAccount.setText(accountNumber)
}
// Skip lookup only when we have a MIB-verified name+currency from the caller.
if (selectedDest != null && accountNumber != null &&
!recipientName.isNullOrBlank() && !currency.isNullOrBlank()
) {
val bankBic = when {
accountNumber.matches(Regex("^9\\d{16}$")) -> "MADVMVMV"
accountNumber.matches(Regex("^7\\d{12}$")) -> "MALBMVMV"
else -> ""
}
val trnType = if (accountNumber.matches(Regex("^9\\d{16}$"))) "DOT" else "IAT"
val validation = BmlAccountValidation(
trnType = trnType,
validationType = "prefilled",
account = accountNumber,
originalInput = accountNumber,
name = recipientName,
alias = null,
currency = currency,
agnt = bankBic.takeIf { it.isNotBlank() }
)
showLookupResult(validation, accountNumber)
}
}
private fun buildDestinations(): List<DestinationOption> {
@@ -517,5 +563,24 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
companion object {
// BML's internal UUID for MIB bank — used as the "swift" field when saving DOT contacts
private const val MIB_SWIFT_ON_BML = "F4E79935-3E73-E611-80DD-00155D020F0A"
private const val ARG_BML_PROFILE_ID = "bml_profile_id"
private const val ARG_ACCOUNT_NUMBER = "account_number"
private const val ARG_RECIPIENT_NAME = "recipient_name"
private const val ARG_CURRENCY = "currency"
fun newInstance(
bmlProfileId: String? = null,
accountNumber: String? = null,
recipientName: String? = null,
currency: String? = null
) = AddContactSheetFragment().apply {
arguments = Bundle().apply {
if (bmlProfileId != null) putString(ARG_BML_PROFILE_ID, bmlProfileId)
if (accountNumber != null) putString(ARG_ACCOUNT_NUMBER, accountNumber)
if (recipientName != null) putString(ARG_RECIPIENT_NAME, recipientName)
if (currency != null) putString(ARG_CURRENCY, currency)
}
}
}
}
@@ -86,6 +86,7 @@ class TransferFragment : Fragment() {
private var resolvedAccountNumber = ""
private var resolvedRecipientName = ""
private var resolvedBankName = ""
private var resolvedDestCurrency = "" // "MVR" / "USD" / "" if unknown
private var resolvedToOwnAccount: BankAccount? = null
// Selected Fahipay service when source is Fahipay and destination is a phone number
@@ -661,6 +662,7 @@ class TransferFragment : Fragment() {
}
resolvedAccountNumber = ""
resolvedRecipientName = ""
resolvedDestCurrency = ""
resolvedToOwnAccount = null
selectedFahipayService = null
binding.cardToInfo.visibility = View.GONE
@@ -677,6 +679,7 @@ class TransferFragment : Fragment() {
if (binding.cardToInfo.visibility == View.VISIBLE) {
resolvedAccountNumber = ""
resolvedRecipientName = ""
resolvedDestCurrency = ""
resolvedToOwnAccount = null
binding.cardToInfo.visibility = View.GONE
binding.tilTo.visibility = View.VISIBLE
@@ -760,7 +763,16 @@ class TransferFragment : Fragment() {
"IAT" -> "MALBMVMV"
else -> bmlResult.agnt ?: bmlResult.account
}
MibIpsAccountInfo(accountName = bmlResult.name, accountNumber = bmlResult.account, bankId = bankId)
// BML's MIB verify endpoint doesn't return the account's currency.
// Enrich via MIB lookup when a MIB session is available.
val currency = if (
inputType == AccountInputParser.InputType.MIB_ACCOUNT &&
bmlResult.currency.isBlank() && mibSess != null
) {
try { MibTransferClient().lookup(mibSess, bmlResult.account).currency }
catch (_: Exception) { "" }
} else bmlResult.currency
MibIpsAccountInfo(accountName = bmlResult.name, accountNumber = bmlResult.account, bankId = bankId, currency = currency)
} else if (mibSess != null) {
try { MibTransferClient().lookup(mibSess, accountNumber) }
catch (e: MibLookupException) { errorMsg = e.message; null }
@@ -769,21 +781,29 @@ class TransferFragment : Fragment() {
errorMsg = getString(R.string.transfer_account_not_found); null
}
} else {
if (mibSess != null) {
val mibInfo = if (mibSess != null) {
try { MibTransferClient().lookup(mibSess, accountNumber) }
catch (e: MibLookupException) { errorMsg = e.message; null }
catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null }
} else {
val bmlResult = try { BmlValidateClient().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null }
} else null
if (mibInfo != null) {
mibInfo
} else if (bmlSess != null) {
val bmlResult = try { BmlValidateClient().validateAccount(bmlSess, accountNumber) } catch (_: Exception) { null }
if (bmlResult != null) {
errorMsg = null
val bankId = when (bmlResult.trnType) {
"IAT" -> "MALBMVMV"
else -> bmlResult.agnt ?: bmlResult.account
}
MibIpsAccountInfo(accountName = bmlResult.name, accountNumber = bmlResult.account, bankId = bankId)
MibIpsAccountInfo(accountName = bmlResult.name, accountNumber = bmlResult.account, bankId = bankId, currency = bmlResult.currency)
} else {
errorMsg = getString(R.string.transfer_account_not_found); null
if (errorMsg == null) errorMsg = getString(R.string.transfer_account_not_found)
null
}
} else {
if (errorMsg == null) errorMsg = getString(R.string.transfer_account_not_found)
null
}
}
}
@@ -799,6 +819,7 @@ class TransferFragment : Fragment() {
resolvedAccountNumber = info.accountNumber
resolvedRecipientName = info.accountName
resolvedBankName = info.bankId
resolvedDestCurrency = info.currency
savedToSubtitle = "${info.accountNumber} · ${info.bankId}"
savedToColorHex = colorHex
savedToImageHash = when {
@@ -979,6 +1000,7 @@ class TransferFragment : Fragment() {
private fun prefillToFromContact(accountNumber: String, label: String) {
resolvedAccountNumber = ""
resolvedRecipientName = ""
resolvedDestCurrency = ""
binding.cardToInfo.visibility = View.GONE
binding.tilTo.visibility = View.VISIBLE
binding.btnPickContact.visibility = View.VISIBLE
@@ -1115,10 +1137,27 @@ class TransferFragment : Fragment() {
if (isSrcBml && isDestMib && currency == "USD") {
val hasBmlContact = allContacts.any { it.benefCategoryId == "BML" && it.benefAccount == resolvedAccountNumber }
if (!hasBmlContact) {
// If we verified the dest currency via MIB fallback, the block is purely a BML API limitation.
// Otherwise (no MIB session, currency truly unknown) the generic message applies.
val msgRes = if (resolvedDestCurrency.isNotBlank())
R.string.transfer_bml_contact_required_msg_bml_limit
else
R.string.transfer_bml_contact_required_msg
val bmlProfileId = src.profileId.takeIf { it.isNotBlank() }
?: src.loginTag.removePrefix("bml_").takeIf { it.isNotBlank() }
val verified = resolvedDestCurrency.isNotBlank()
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.transfer_bml_contact_required_title)
.setMessage(R.string.transfer_bml_contact_required_msg)
.setPositiveButton(R.string.close, null)
.setMessage(msgRes)
.setPositiveButton(R.string.contact_save) { _, _ ->
AddContactSheetFragment.newInstance(
bmlProfileId = bmlProfileId,
accountNumber = resolvedAccountNumber,
recipientName = if (verified) resolvedRecipientName else null,
currency = if (verified) resolvedDestCurrency else null
).show(parentFragmentManager, "add_contact")
}
.setNegativeButton(R.string.close, null)
.show()
return
}
@@ -1128,11 +1167,13 @@ class TransferFragment : Fragment() {
val bankNameCapture = resolvedBankName
val capturedToAvatar = (binding.ivToPhoto.drawable as? android.graphics.drawable.BitmapDrawable)?.bitmap
val destCurrency = allAccounts.firstOrNull { it.accountNumber == resolvedAccountNumber }
?.currencyName?.ifBlank { "MVR" }
?: allContacts.firstOrNull { it.benefAccount == resolvedAccountNumber }
?.transferCyDesc?.ifBlank { "MVR" }
?: if (isDestMib) "MVR" else "MVR"
val destCurrency = resolvedDestCurrency.ifBlank {
allAccounts.firstOrNull { it.accountNumber == resolvedAccountNumber }
?.currencyName?.ifBlank { "MVR" }
?: allContacts.firstOrNull { it.benefAccount == resolvedAccountNumber }
?.transferCyDesc?.ifBlank { "MVR" }
?: "MVR"
}
val isUsdToMvr = currency.equals("USD", ignoreCase = true) && destCurrency.equals("MVR", ignoreCase = true)
val isSrcCredit = src.profileType == "BML_CREDIT"
@@ -2030,6 +2071,7 @@ class TransferFragment : Fragment() {
resolvedAccountNumber = ""
resolvedRecipientName = ""
resolvedBankName = ""
resolvedDestCurrency = ""
resolvedToOwnAccount = null
selectedFahipayService = null
binding.cardToInfo.visibility = View.GONE
+2 -1
View File
@@ -280,7 +280,8 @@
<string name="transfer_confirm">Confirm</string>
<string name="transfer_success">Transfer Successful</string>
<string name="transfer_bml_contact_required_title">Contact Required</string>
<string name="transfer_bml_contact_required_msg">To send USD to a MIB account from BML, the recipient must be saved as a BML contact first. This is required by BML\'s API.\n\nPlease add this account as a BML contact, then try again.</string>
<string name="transfer_bml_contact_required_msg">We couldn\'t verify the recipient\'s currency for this transfer.\n\nPlease save them as a contact, manually select the correct currency, then try again.</string>
<string name="transfer_bml_contact_required_msg_bml_limit">BML\'s API requires the recipient to be saved as a contact when sending USD to a non-BML account, even though we verified the account.\n\nPlease save them as a contact, then try again.</string>
<string name="transfer_missing_internal_id">Account data is incomplete — please re-login to refresh.</string>
<string name="transfer_verify_payment">Verify Payment</string>
<string name="transfer_send_otp_via">Send verification code via</string>