diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlValidateClient.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlValidateClient.kt index 50cca52..540f35c 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlValidateClient.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlValidateClient.kt @@ -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 } diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt index d666bc2..19033ee 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt @@ -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 ) diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibTransferClient.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibTransferClient.kt index 90e52f2..f69b11e 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibTransferClient.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibTransferClient.kt @@ -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 ) } } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AddContactSheetFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AddContactSheetFragment.kt index a64c732..0955cb8 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AddContactSheetFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AddContactSheetFragment.kt @@ -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 { @@ -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) + } + } } } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt index 513e052..cd2acae 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt @@ -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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37d5c38..67ecf36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -280,7 +280,8 @@ Confirm Transfer Successful Contact Required - 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. + 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. + 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. Account data is incomplete — please re-login to refresh. Verify Payment Send verification code via