From 6d48c273919b5105786559d006368dbb5f61f9e2 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Wed, 20 May 2026 22:43:29 +0500 Subject: [PATCH] huge refactor.. might need to revert later --- .../java/sh/sar/basedbank/BasedBankApp.kt | 16 +- .../sar/basedbank/api/bml/BmlAccountClient.kt | 124 ++++ .../sar/basedbank/api/bml/BmlApiConstants.kt | 21 + .../basedbank/api/bml/BmlContactsClient.kt | 95 +++ .../api/bml/BmlForeignLimitsClient.kt | 64 ++ .../sar/basedbank/api/bml/BmlHistoryClient.kt | 174 +++++ .../sh/sar/basedbank/api/bml/BmlLoginFlow.kt | 592 +----------------- .../sh/sar/basedbank/api/bml/BmlModels.kt | 4 +- .../basedbank/api/bml/BmlTransferClient.kt | 98 +++ .../basedbank/api/bml/BmlValidateClient.kt | 79 +++ .../api/fahipay/FahipayAccountClient.kt | 76 +++ .../api/fahipay/FahipayContactsClient.kt | 69 ++ .../api/fahipay/FahipayHistoryClient.kt | 60 ++ .../basedbank/api/fahipay/FahipayLoginFlow.kt | 165 ----- .../basedbank/api/fahipay/FahipayModels.kt | 4 +- .../sh/sar/basedbank/api/mib/MibLoginFlow.kt | 38 -- .../sh/sar/basedbank/api/mib/MibModels.kt | 64 +- .../sar/basedbank/api/mib/MibProfileClient.kt | 50 ++ .../sh/sar/basedbank/api/models/BankModels.kt | 72 +++ .../ui/home/AccountHistoryAdapter.kt | 18 +- .../ui/home/AccountHistoryFragment.kt | 10 +- .../sar/basedbank/ui/home/AccountsAdapter.kt | 24 +- .../ui/home/AddContactSheetFragment.kt | 22 +- .../sar/basedbank/ui/home/ContactsFragment.kt | 4 +- .../basedbank/ui/home/DashboardFragment.kt | 4 +- .../sh/sar/basedbank/ui/home/HomeActivity.kt | 70 +-- .../sh/sar/basedbank/ui/home/HomeViewModel.kt | 12 +- .../sh/sar/basedbank/ui/home/OtpFragment.kt | 7 +- .../sar/basedbank/ui/home/PayMvQrFragment.kt | 12 +- .../basedbank/ui/home/TransactionAdapter.kt | 14 +- .../sar/basedbank/ui/home/TransferFragment.kt | 51 +- .../ui/home/TransferHistoryFragment.kt | 24 +- .../basedbank/ui/login/CredentialsFragment.kt | 17 +- .../sh/sar/basedbank/util/AccountCache.kt | 24 +- .../basedbank/util/AccountHistoryParser.kt | 4 +- .../sar/basedbank/util/AccountListParser.kt | 4 +- .../sar/basedbank/util/ContactListParser.kt | 6 +- .../sh/sar/basedbank/util/ContactManager.kt | 4 +- .../sh/sar/basedbank/util/ContactsCache.kt | 34 +- .../sh/sar/basedbank/util/HistoryFetcher.kt | 28 +- .../basedbank/util/bmlapi/BmlContactParser.kt | 4 +- .../util/bmlapi/BmlDashboardParser.kt | 6 +- .../basedbank/util/bmlapi/BmlHistoryParser.kt | 4 +- .../util/fahipayapi/FahipayAccountParser.kt | 4 +- .../util/fahipayapi/FahipayContactParser.kt | 4 +- .../util/fahipayapi/FahipayHistoryParser.kt | 4 +- .../basedbank/util/mibapi/MibAccountParser.kt | 4 +- .../basedbank/util/mibapi/MibContactParser.kt | 4 +- .../basedbank/util/mibapi/MibHistoryParser.kt | 4 +- 49 files changed, 1224 insertions(+), 1072 deletions(-) create mode 100644 app/src/main/java/sh/sar/basedbank/api/bml/BmlAccountClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/bml/BmlApiConstants.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/bml/BmlContactsClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/bml/BmlForeignLimitsClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/bml/BmlHistoryClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/bml/BmlTransferClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/bml/BmlValidateClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayAccountClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayContactsClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayHistoryClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/mib/MibProfileClient.kt create mode 100644 app/src/main/java/sh/sar/basedbank/api/models/BankModels.kt diff --git a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt index a7e2516..a0b0148 100644 --- a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt +++ b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt @@ -8,7 +8,7 @@ import sh.sar.basedbank.api.bml.BmlLoginFlow import sh.sar.basedbank.api.bml.BmlProfile import sh.sar.basedbank.api.bml.BmlSession import sh.sar.basedbank.api.fahipay.FahipaySession -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibLoginFlow import sh.sar.basedbank.api.mib.MibProfile import sh.sar.basedbank.api.mib.MibSession @@ -17,14 +17,14 @@ import sh.sar.basedbank.util.CredentialStore class BasedBankApp : Application() { // Held in memory after successful login; cleared on logout - var accounts: List = emptyList() + var accounts: List = emptyList() var fullName: String = "" /** Active MIB sessions keyed by loginId (= MIB username). */ val mibSessions: MutableMap = mutableMapOf() val mibProfilesMap: MutableMap> = mutableMapOf() val mibLoginFlows: MutableMap = mutableMapOf() - var mibAccounts: List = emptyList() + var mibAccounts: List = emptyList() /** * Active BML sessions keyed by profileId (a globally unique GUID per BML profile). @@ -35,16 +35,16 @@ class BasedBankApp : Application() { val bmlProfilesMap: MutableMap> = mutableMapOf() /** BML login flows per loginId — hold the web session (cookies) needed for profile activation. */ val bmlLoginFlows: MutableMap = mutableMapOf() - var bmlAccounts: List = emptyList() + var bmlAccounts: List = emptyList() /** Active Fahipay sessions keyed by loginId (= profileId). */ val fahipaySessions: MutableMap = mutableMapOf() - var fahipayAccounts: List = emptyList() + var fahipayAccounts: List = emptyList() // ─── MIB helpers ────────────────────────────────────────────────────────── /** Returns the MIB session for the given account (matched via loginTag). */ - fun mibSessionFor(account: MibAccount): MibSession? = + fun mibSessionFor(account: BankAccount): MibSession? = mibSessions[account.loginTag.removePrefix("mib_")] /** Returns any available MIB session. */ @@ -73,7 +73,7 @@ class BasedBankApp : Application() { * Returns the BML session for the given account. * Looks up by profileId first (multi-profile), falls back to loginId (legacy single-profile). */ - fun bmlSessionFor(account: MibAccount): BmlSession? { + fun bmlSessionFor(account: BankAccount): BmlSession? { val byProfile = if (account.profileId.isNotBlank()) bmlSessions[account.profileId] else null return byProfile ?: bmlSessions[account.loginTag.removePrefix("bml_")] } @@ -100,7 +100,7 @@ class BasedBankApp : Application() { // ─── Fahipay helpers ────────────────────────────────────────────────────── /** Returns the Fahipay session for the given account (matched via loginTag = "fahipay_${profileId}"). */ - fun fahipaySessionFor(account: MibAccount): FahipaySession? = + fun fahipaySessionFor(account: BankAccount): FahipaySession? = fahipaySessions[account.loginTag.removePrefix("fahipay_")] /** Serialises all MIB profile-switch + request sequences to prevent session corruption. */ diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlAccountClient.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlAccountClient.kt new file mode 100644 index 0000000..1047472 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlAccountClient.kt @@ -0,0 +1,124 @@ +package sh.sar.basedbank.api.bml + +import org.json.JSONObject +import sh.sar.basedbank.api.models.BankAccount + +data class BmlUserInfo( + val fullName: String, + val email: String, + val mobile: String, + val customerId: String, + val idCard: String, + val birthdate: String +) + +class BmlAccountClient { + + private val client = newBmlApiClient() + + fun fetchAccounts( + session: BmlSession, + loginTag: String, + profileName: String = "Personal", + profileId: String = "" + ): List { + val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/dashboard")).execute() + val code = resp.code + val json = resp.body?.string() + resp.close() + if (code == 401 || code == 419) throw AuthExpiredException() + return parseDashboard(json ?: return emptyList(), loginTag, profileName, profileId) + } + + fun fetchUserInfo(session: BmlSession): BmlUserInfo? { + val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/userinfo")).execute() + val json = resp.body?.string() ?: return null + resp.close() + return try { + val root = JSONObject(json) + if (!root.optBoolean("success")) return null + val user = root.optJSONObject("payload")?.optJSONObject("user") ?: return null + BmlUserInfo( + fullName = user.optString("fullname").trim(), + email = user.optString("email").trim(), + mobile = user.optString("mobile_phone").trim(), + customerId = user.optString("customer_number").trim(), + idCard = user.optString("idcard").trim(), + birthdate = user.optString("birthdate").trim() + ) + } catch (_: Exception) { null } + } + + private fun parseDashboard( + json: String, + loginTag: String, + profileName: String, + profileId: String + ): List { + val root = JSONObject(json) + if (!root.optBoolean("success")) return emptyList() + val dashboard = root.optJSONObject("payload")?.optJSONArray("dashboard") ?: return emptyList() + + val casaAccounts = mutableListOf() + val prepaidCards = mutableListOf() + + for (i in 0 until dashboard.length()) { + val item = dashboard.getJSONObject(i) + val currency = item.optString("currency", "MVR") + val accountType = item.optString("account_type", "CASA") + val product = item.optString("product") + val accountNumber = item.optString("account") + val status = item.optString("account_status", "Active") + val internalId = item.optString("id", "") + + if (accountType == "CASA") { + val available = item.optDouble("availableBalance", 0.0) + casaAccounts.add(BankAccount( + bank = "BML", + profileName = profileName, + profileType = "BML", + accountNumber = accountNumber, + accountBriefName = item.optString("alias"), + currencyName = currency, + accountTypeName = product, + availableBalance = "%.2f".format(available), + currentBalance = "%.2f".format(item.optDouble("ledgerBalance", 0.0)), + blockedAmount = "%.2f".format(item.optDouble("lockedAmount", 0.0)), + mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00", + statusDesc = status, + profileImageHash = null, + loginTag = loginTag, + profileId = profileId, + internalId = internalId + )) + } else if (accountType == "Card") { + val isVisible = item.optBoolean("account_visible", false) + if (!isVisible) continue + val isPrepaid = item.optBoolean("prepaid_card", false) + val cardBalance = item.optJSONObject("cardBalance") + val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0 + val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0 + prepaidCards.add(BankAccount( + bank = "BML", + profileName = profileName, + profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT", + accountNumber = accountNumber, + accountBriefName = item.optString("alias").ifBlank { product }, + currencyName = currency, + accountTypeName = product, + availableBalance = "%.2f".format(available), + currentBalance = "%.2f".format(current), + blockedAmount = "0.00", + mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00", + statusDesc = status, + profileImageHash = null, + loginTag = loginTag, + profileId = profileId, + internalId = internalId + )) + } + } + + return casaAccounts + prepaidCards + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlApiConstants.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlApiConstants.kt new file mode 100644 index 0000000..616fd13 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlApiConstants.kt @@ -0,0 +1,21 @@ +package sh.sar.basedbank.api.bml + +import okhttp3.OkHttpClient +import okhttp3.Request +import java.util.concurrent.TimeUnit + +internal const val BML_BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking" +internal const val BML_USER_AGENT = "bml-mobile-banking/348 (POCO; Android 14; 22101320I)" +internal const val BML_APP_VERSION = "2.1.44.348" + +internal fun newBmlApiClient(): OkHttpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + +internal fun bmlApiRequest(session: BmlSession, url: String): Request = + Request.Builder().url(url) + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .build() diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlContactsClient.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlContactsClient.kt new file mode 100644 index 0000000..488c5e7 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlContactsClient.kt @@ -0,0 +1,95 @@ +package sh.sar.basedbank.api.bml + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONArray +import org.json.JSONObject +import sh.sar.basedbank.api.models.BankContact + +class BmlContactsClient { + + private val client = newBmlApiClient() + + fun fetchContacts(session: BmlSession, loginId: String): List { + val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/contacts")).execute() + val json = resp.body?.string() ?: return emptyList() + resp.close() + return parseContacts(json, loginId) + } + + fun saveContact( + session: BmlSession, + contactType: String, + account: String, + alias: String, + currency: String? = null, + name: String? = null, + swift: String? = null + ): Boolean { + val bodyObj = JSONObject().apply { + put("contact_type", contactType) + put("account", account) + put("alias", alias) + if (currency != null) put("currency", currency) + if (name != null) put("name", name) + if (swift != null) put("swift", swift) + } + val resp = client.newCall( + Request.Builder().url("$BML_BASE_URL/api/mobile/contacts") + .post(bodyObj.toString().toRequestBody("application/json".toMediaType())) + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .header("Accept", "application/json") + .build() + ).execute() + val json = resp.body?.string() ?: return false + resp.close() + return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false } + } + + fun deleteContact(session: BmlSession, contactId: String): Boolean { + val body = """{"_method":"delete"}""".toRequestBody("application/json".toMediaType()) + val resp = client.newCall( + Request.Builder().url("$BML_BASE_URL/api/mobile/contacts/$contactId") + .post(body) + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .header("accept", "application/json") + .build() + ).execute() + val bodyStr = resp.body?.string() ?: return false + resp.close() + return try { JSONObject(bodyStr).optBoolean("success") } catch (_: Exception) { false } + } + + private fun parseContacts(json: String, loginId: String): List { + val root = JSONObject(json) + if (!root.optBoolean("success")) return emptyList() + val payload: JSONArray = root.optJSONArray("payload") ?: return emptyList() + val result = mutableListOf() + for (i in 0 until payload.length()) { + val item = payload.getJSONObject(i) + val account = item.optString("account", "") + if (account.isBlank()) continue + result.add(BankContact( + benefNo = "bml_${item.optInt("id")}", + benefName = item.optString("name"), + benefNickName = item.optString("alias", item.optString("name")), + benefAccount = account, + benefType = "I", + bankColor = "#0066A1", + benefBankName = "Bank of Maldives", + bankCode = "", + benefStatus = item.optString("status", "S"), + transferCyDesc = item.optString("currency", "MVR"), + customerImgHash = null, + benefCategoryId = "BML", + profileId = loginId + )) + } + return result + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlForeignLimitsClient.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlForeignLimitsClient.kt new file mode 100644 index 0000000..90d99ac --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlForeignLimitsClient.kt @@ -0,0 +1,64 @@ +package sh.sar.basedbank.api.bml + +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject +import java.util.concurrent.TimeUnit + +class BmlForeignLimitsClient { + + // Foreign limits use a different host than the main BML API + private val BASE_URL = "https://app.bankofmaldives.com.mv/api/v2" + + private val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + fun fetchForeignLimits(session: BmlSession): List { + val resp = client.newCall( + Request.Builder().url("$BASE_URL/foreign-limits") + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .header("Accept", "application/json") + .build() + ).execute() + val code = resp.code + val json = resp.body?.string() + resp.close() + if (code == 401 || code == 419) throw AuthExpiredException() + return parseForeignLimits(json ?: return emptyList()) + } + + private fun parseForeignLimits(json: String): List { + return try { + val root = JSONObject(json) + if (!root.optBoolean("success")) return emptyList() + val payload = root.optJSONArray("payload") ?: return emptyList() + (0 until payload.length()).map { i -> + val item = payload.getJSONObject(i) + val usage = item.optJSONObject("usageByCategory") ?: JSONObject() + val atm = usage.optJSONObject("ATM") ?: JSONObject() + val ecom = usage.optJSONObject("ECOM") ?: JSONObject() + val pos = usage.optJSONObject("POS") ?: JSONObject() + BmlForeignLimit( + type = item.optString("type", "Debit"), + used = item.optDouble("used", 0.0), + totalLimit = item.optDouble("totalLimit", 0.0), + generalCap = item.optDouble("generalCap", 0.0), + generalRemaining = item.optDouble("generalRemaining", 0.0), + medicalRemaining = item.optDouble("medicalRemaining", 0.0), + isAtmEnabled = item.optBoolean("isAtmEnabled", false), + isPosEnabled = item.optBoolean("isPosEnabled", false), + atmRemaining = atm.optDouble("remaining", 0.0), + atmLimit = atm.optDouble("limit", 0.0), + ecomRemaining = ecom.optDouble("remaining", 0.0), + ecomLimit = ecom.optDouble("limit", 0.0), + posRemaining = pos.optDouble("remaining", 0.0), + posLimit = pos.optDouble("limit", 0.0) + ) + } + } catch (_: Exception) { emptyList() } + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlHistoryClient.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlHistoryClient.kt new file mode 100644 index 0000000..c5a465d --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlHistoryClient.kt @@ -0,0 +1,174 @@ +package sh.sar.basedbank.api.bml + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import sh.sar.basedbank.api.models.BankTransaction +import java.text.SimpleDateFormat +import java.util.Locale + +class BmlHistoryClient { + + private val client = newBmlApiClient() + + fun fetchAccountHistory( + session: BmlSession, + accountId: String, + accountDisplayName: String, + accountNumber: String, + page: Int + ): Pair, Int> { + val resp = client.newCall( + Request.Builder().url("$BML_BASE_URL/api/mobile/account/$accountId/history/$page") + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .build() + ).execute() + val code = resp.code + val json = resp.body?.string() ?: return Pair(emptyList(), 0) + resp.close() + if (code == 401 || code == 419) throw AuthExpiredException() + return try { + val root = JSONObject(json) + if (!root.optBoolean("success")) return Pair(emptyList(), 0) + val payload = root.optJSONObject("payload") ?: return Pair(emptyList(), 0) + val totalPages = payload.optInt("totalPages", 0) + val history = payload.optJSONArray("history") ?: return Pair(emptyList(), totalPages) + val transactions = (0 until history.length()).map { i -> + val item = history.getJSONObject(i) + val desc = item.optString("description").trim() + val narrative1 = item.optString("narrative1") + val date = when (desc) { + "Purchase" -> parsePurchaseNarrative1(narrative1) ?: item.optString("bookingDate") + "Transfer Debit", "Transfer Credit" -> parseTransferNarrative1(narrative1) ?: item.optString("bookingDate") + else -> item.optString("bookingDate") + } + BankTransaction( + id = item.optString("id"), + date = date, + description = desc, + amount = item.optDouble("amount", 0.0), + currency = item.optString("currency"), + counterpartyName = item.optString("narrative2").takeIf { it.isNotBlank() }, + reference = item.optString("reference").takeIf { it.isNotBlank() }, + accountNumber = accountNumber, + accountDisplayName = accountDisplayName, + source = "BML" + ) + } + Pair(transactions, totalPages) + } catch (_: Exception) { Pair(emptyList(), 0) } + } + + fun fetchCardHistory( + session: BmlSession, + cardId: String, + accountDisplayName: String, + accountNumber: String, + month: String + ): List { + val body = """{"card":"$cardId","month":"$month"}""" + .toRequestBody("application/json".toMediaType()) + val resp = client.newCall( + Request.Builder().url("$BML_BASE_URL/api/mobile/card/statement").post(body) + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .build() + ).execute() + val code = resp.code + val json = resp.body?.string() ?: return emptyList() + resp.close() + if (code == 401 || code == 419) throw AuthExpiredException() + return try { + val root = JSONObject(json) + if (!root.optBoolean("success")) return emptyList() + val payload = root.optJSONObject("payload") ?: return emptyList() + val result = mutableListOf() + + val authDetails = payload.optJSONObject("outstanding") + ?.optJSONArray("CardOutStdAuthDetails") + if (authDetails != null) { + for (i in 0 until authDetails.length()) { + val item = authDetails.getJSONObject(i) + result.add(BankTransaction( + id = "auth_${item.optString("TranApprCode")}_$i", + date = item.optString("DateTime"), + description = item.optString("TranDesc").trim(), + amount = item.optDouble("BillingAmount", 0.0), + currency = item.optString("BillingCcy", "MVR"), + counterpartyName = null, + reference = item.optString("TranApprCode").takeIf { it.isNotBlank() }, + accountNumber = accountNumber, + accountDisplayName = accountDisplayName, + source = "BML_CARD" + )) + } + } + + val unbilled = payload.optJSONObject("unbilled") + ?.optJSONArray("CardUnbillTxnDetails") + if (unbilled != null) { + for (i in 0 until unbilled.length()) { + val item = unbilled.getJSONObject(i) + result.add(BankTransaction( + id = "unbilled_${item.optString("TranApprCode")}_$i", + date = item.optString("DateTime"), + description = item.optString("TranDesc").trim(), + amount = item.optDouble("BillingAmount", 0.0), + currency = item.optString("BillingCcy", "MVR"), + counterpartyName = null, + reference = item.optString("TranApprCode").takeIf { it.isNotBlank() }, + accountNumber = accountNumber, + accountDisplayName = accountDisplayName, + source = "BML_CARD" + )) + } + } + + val statement = payload.optJSONArray("cardstatement") + if (statement != null) { + for (i in 0 until statement.length()) { + val item = statement.getJSONObject(i) + result.add(BankTransaction( + id = "stmt_${item.optString("TranRef", i.toString())}", + date = item.optString("TransDate", item.optString("TranDate", "")), + description = item.optString("TranDesc", item.optString("Description", "")).trim(), + amount = -item.optDouble("TranAmount", 0.0), + currency = item.optString("TranCcy", "MVR"), + counterpartyName = null, + reference = item.optString("TranRef").takeIf { it.isNotBlank() }, + accountNumber = accountNumber, + accountDisplayName = accountDisplayName, + source = "BML_CARD" + )) + } + } + result + } catch (_: Exception) { emptyList() } + } + + // "12-05-2026 041675" → first 4 digits of time part as HH:mm + private fun parsePurchaseNarrative1(narrative1: String): String? { + return try { + val parts = narrative1.split(" ") + if (parts.size < 2) null + else { + val timePart = parts[1].take(4) + val combined = "${parts[0]} ${timePart.take(2)}:${timePart.drop(2)}:00" + val date = SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.US).parse(combined) + date?.let { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(it) } + } + } catch (_: Exception) { null } + } + + // "11-04-2026 13-17-08" → yyyy-MM-dd HH:mm:ss + private fun parseTransferNarrative1(narrative1: String): String? { + return try { + val date = SimpleDateFormat("dd-MM-yyyy HH-mm-ss", Locale.US).parse(narrative1) + date?.let { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(it) } + } catch (_: Exception) { null } + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt index ad5c772..fa21694 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt @@ -7,19 +7,15 @@ import okhttp3.FormBody import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONArray import org.json.JSONObject -import sh.sar.basedbank.api.mib.MibAccount -import sh.sar.basedbank.api.mib.MibBeneficiary -import sh.sar.basedbank.api.mib.Transaction +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.util.Totp import java.security.MessageDigest import java.security.SecureRandom -import java.text.SimpleDateFormat import java.util.Base64 -import java.util.Locale import java.util.concurrent.TimeUnit class AuthExpiredException : Exception("Session expired") @@ -53,11 +49,6 @@ class BmlLoginFlow { .readTimeout(30, TimeUnit.SECONDS) .build() - private val apiClient = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build() - /** PKCE params — generated once per login and reused across all profile activations. */ private var codeVerifier: String = "" private var codeChallenge: String = "" @@ -234,7 +225,7 @@ class BmlLoginFlow { code: String, profile: BmlProfile, loginTag: String - ): Pair> { + ): Pair> { // Refresh XSRF token before submitting client.newCall( Request.Builder().url("$BASE_URL/web/profile/2fa/business") @@ -275,7 +266,7 @@ class BmlLoginFlow { loginTag: String, profileName: String, profileId: String - ): Pair> { + ): Pair> { val authorizeUrl = HttpUrl.Builder() .scheme("https").host("www.bankofmaldives.com.mv") .addPathSegments("internetbanking/oauth/authorize") @@ -323,419 +314,9 @@ class BmlLoginFlow { .takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed") val session = BmlSession(accessToken = accessToken, deviceId = deviceId) - val accounts = fetchAccounts(session, loginTag, profileName, profileId) + val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profileName, profileId) return Pair(session, accounts) } - - // ─── API methods ───────────────────────────────────────────────────────── - - fun fetchAccounts( - session: BmlSession, - loginTag: String, - profileName: String = "Personal", - profileId: String = "" - ): List { - val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute() - val code = resp.code - val json = resp.body?.string() - resp.close() - if (code == 401 || code == 419) throw AuthExpiredException() - return parseDashboard(json ?: return emptyList(), loginTag, profileName, profileId) - } - - fun fetchForeignLimits(session: BmlSession): List { - val resp = apiClient.newCall( - Request.Builder().url("https://app.bankofmaldives.com.mv/api/v2/foreign-limits") - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .header("Accept", "application/json") - .build() - ).execute() - val code = resp.code - val json = resp.body?.string() - resp.close() - if (code == 401 || code == 419) throw AuthExpiredException() - return parseForeignLimits(json ?: return emptyList()) - } - - data class BmlUserInfo( - val fullName: String, - val email: String, - val mobile: String, - val customerId: String, - val idCard: String, - val birthdate: String - ) - - fun fetchUserInfo(session: BmlSession): BmlUserInfo? { - val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/userinfo")).execute() - val json = resp.body?.string() ?: return null - resp.close() - return try { - val root = JSONObject(json) - if (!root.optBoolean("success")) return null - val user = root.optJSONObject("payload")?.optJSONObject("user") ?: return null - BmlUserInfo( - fullName = user.optString("fullname").trim(), - email = user.optString("email").trim(), - mobile = user.optString("mobile_phone").trim(), - customerId = user.optString("customer_number").trim(), - idCard = user.optString("idcard").trim(), - birthdate = user.optString("birthdate").trim() - ) - } catch (_: Exception) { null } - } - - fun validateAccount(session: BmlSession, input: String): BmlAccountValidation? { - val resp = apiClient.newCall( - Request.Builder().url("$BASE_URL/api/mobile/validate/account/$input") - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .header("Accept", "application/json") - .build() - ).execute() - val json = resp.body?.string() ?: return null - resp.close() - return try { - val root = JSONObject(json) - if (!root.optBoolean("success")) return null - val payload = root.optJSONObject("payload") ?: return null - val trnType = payload.optString("trnType", "") - val validationType = payload.optString("validationType", "") - if (validationType == "alias") { - val cdtrAcct = payload.optJSONObject("CdtrAcct") ?: return null - BmlAccountValidation( - trnType = trnType, - validationType = validationType, - account = cdtrAcct.optString("Acct"), - originalInput = input, - name = payload.optString("contact_name").trim(), - alias = null, - currency = payload.optString("currency", "MVR"), - agnt = cdtrAcct.optString("FinInstnId").takeIf { it.isNotBlank() } - ) - } else { - BmlAccountValidation( - trnType = trnType, - validationType = validationType, - account = payload.optString("account"), - originalInput = input, - name = payload.optString("name"), - alias = payload.optString("alias").takeIf { it.isNotBlank() && it != "null" }, - currency = payload.optString("currency", "MVR") - ) - } - } catch (_: Exception) { null } - } - - fun verifyMibAccount(session: BmlSession, account: String): BmlAccountValidation? { - val resp = apiClient.newCall( - Request.Builder().url("$BASE_URL/api/mobile/favara/account-verification/$account/MIB") - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .header("Accept", "application/json") - .build() - ).execute() - val json = resp.body?.string() ?: return null - resp.close() - return try { - val root = JSONObject(json) - if (!root.optBoolean("success")) return null - BmlAccountValidation( - trnType = "DOT", - validationType = "MIB", - account = root.optString("account"), - originalInput = account, - name = root.optString("name"), - alias = null, - currency = "MVR", - agnt = root.optString("agnt").takeIf { it.isNotBlank() } - ) - } catch (_: Exception) { null } - } - - fun saveContact( - session: BmlSession, - contactType: String, - account: String, - alias: String, - currency: String? = null, - name: String? = null, - swift: String? = null - ): Boolean { - val bodyObj = JSONObject().apply { - put("contact_type", contactType) - put("account", account) - put("alias", alias) - if (currency != null) put("currency", currency) - if (name != null) put("name", name) - if (swift != null) put("swift", swift) - } - val resp = apiClient.newCall( - Request.Builder().url("$BASE_URL/api/mobile/contacts") - .post(bodyObj.toString().toRequestBody("application/json".toMediaType())) - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .header("Accept", "application/json") - .build() - ).execute() - val json = resp.body?.string() ?: return false - resp.close() - return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false } - } - - fun fetchContacts(session: BmlSession, loginId: String): List { - val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/contacts")).execute() - val json = resp.body?.string() ?: return emptyList() - resp.close() - return parseContacts(json, loginId) - } - - fun initiateTransfer( - session: BmlSession, - debitAccount: String, - creditAccount: String, - amount: Double, - transferType: String, - currency: String, - bank: String? = null - ): Boolean { - val jo = JSONObject().apply { - put("debitAccount", debitAccount) - put("creditAccount", creditAccount) - put("debitAmount", amount) - put("transfertype", transferType) - put("currency", currency) - put("channel", "token") - if (bank != null) put("bank", bank) - } - val body = jo.toString().toRequestBody("application/json".toMediaType()) - val request = Request.Builder() - .url("$BASE_URL/api/mobile/transfer") - .post(body) - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .header("accept", "application/json") - .build() - return apiClient.newCall(request).execute().use { response -> - val bodyStr = response.body?.string() ?: return@use false - try { - val json = JSONObject(bodyStr) - json.optBoolean("success") && json.optInt("code") == 22 - } catch (_: Exception) { false } - } - } - - fun confirmTransfer( - session: BmlSession, - debitAccount: String, - creditAccount: String, - amount: Double, - transferType: String, - currency: String, - otp: String, - remarks: String = "", - bank: String? = null - ): BmlTransferResult { - val jo = JSONObject().apply { - put("debitAccount", debitAccount) - put("creditAccount", creditAccount) - put("debitAmount", amount) - put("transfertype", transferType) - put("currency", currency) - put("channel", "token") - put("otp", otp) - if (remarks.isNotBlank()) put("remarks", remarks) - if (bank != null) put("bank", bank) - } - val body = jo.toString().toRequestBody("application/json".toMediaType()) - val request = Request.Builder() - .url("$BASE_URL/api/mobile/transfer") - .post(body) - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .header("accept", "application/json") - .build() - return apiClient.newCall(request).execute().use { response -> - val bodyStr = response.body?.string() - ?: return@use BmlTransferResult(false, errorMessage = "No response") - try { - val json = JSONObject(bodyStr) - if (!json.optBoolean("success")) { - BmlTransferResult(false, errorMessage = json.optString("message").ifBlank { "Transfer failed" }) - } else { - val payload = json.optJSONObject("payload") - BmlTransferResult( - success = true, - reference = payload?.optString("reference") ?: "", - timestamp = payload?.optString("timestamp") ?: "", - message = json.optString("message") - ) - } - } catch (_: Exception) { BmlTransferResult(false, errorMessage = "Parse error") } - } - } - - fun deleteContact(session: BmlSession, contactId: String): Boolean { - val body = """{"_method":"delete"}""".toRequestBody("application/json".toMediaType()) - val request = Request.Builder() - .url("$BASE_URL/api/mobile/contacts/$contactId") - .post(body) - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .header("accept", "application/json") - .build() - return apiClient.newCall(request).execute().use { response -> - val bodyStr = response.body?.string() ?: return@use false - try { JSONObject(bodyStr).optBoolean("success") } catch (_: Exception) { false } - } - } - - fun fetchAccountHistory( - session: BmlSession, - accountId: String, - accountDisplayName: String, - accountNumber: String, - page: Int - ): Pair, Int> { - val resp = apiClient.newCall( - Request.Builder().url("$BASE_URL/api/mobile/account/$accountId/history/$page") - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .build() - ).execute() - val code = resp.code - val json = resp.body?.string() ?: return Pair(emptyList(), 0) - resp.close() - if (code == 401 || code == 419) throw AuthExpiredException() - return try { - val root = JSONObject(json) - if (!root.optBoolean("success")) return Pair(emptyList(), 0) - val payload = root.optJSONObject("payload") ?: return Pair(emptyList(), 0) - val totalPages = payload.optInt("totalPages", 0) - val history = payload.optJSONArray("history") ?: return Pair(emptyList(), totalPages) - val transactions = (0 until history.length()).map { i -> - val item = history.getJSONObject(i) - val desc = item.optString("description").trim() - val narrative1 = item.optString("narrative1") - val date = when (desc) { - "Purchase" -> parsePurchaseNarrative1(narrative1) ?: item.optString("bookingDate") - "Transfer Debit", "Transfer Credit" -> parseTransferNarrative1(narrative1) ?: item.optString("bookingDate") - else -> item.optString("bookingDate") - } - Transaction( - id = item.optString("id"), - date = date, - description = desc, - amount = item.optDouble("amount", 0.0), - currency = item.optString("currency"), - counterpartyName = item.optString("narrative2").takeIf { it.isNotBlank() }, - reference = item.optString("reference").takeIf { it.isNotBlank() }, - accountNumber = accountNumber, - accountDisplayName = accountDisplayName, - source = "BML" - ) - } - Pair(transactions, totalPages) - } catch (_: Exception) { Pair(emptyList(), 0) } - } - - fun fetchCardHistory( - session: BmlSession, - cardId: String, - accountDisplayName: String, - accountNumber: String, - month: String - ): List { - val body = """{"card":"$cardId","month":"$month"}""" - .toRequestBody("application/json".toMediaType()) - val resp = apiClient.newCall( - Request.Builder().url("$BASE_URL/api/mobile/card/statement").post(body) - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .build() - ).execute() - val code = resp.code - val json = resp.body?.string() ?: return emptyList() - resp.close() - if (code == 401 || code == 419) throw AuthExpiredException() - return try { - val root = JSONObject(json) - if (!root.optBoolean("success")) return emptyList() - val payload = root.optJSONObject("payload") ?: return emptyList() - val result = mutableListOf() - - val authDetails = payload.optJSONObject("outstanding") - ?.optJSONArray("CardOutStdAuthDetails") - if (authDetails != null) { - for (i in 0 until authDetails.length()) { - val item = authDetails.getJSONObject(i) - result.add(Transaction( - id = "auth_${item.optString("TranApprCode")}_$i", - date = item.optString("DateTime"), - description = item.optString("TranDesc").trim(), - amount = item.optDouble("BillingAmount", 0.0), - currency = item.optString("BillingCcy", "MVR"), - counterpartyName = null, - reference = item.optString("TranApprCode").takeIf { it.isNotBlank() }, - accountNumber = accountNumber, - accountDisplayName = accountDisplayName, - source = "BML_CARD" - )) - } - } - - val unbilled = payload.optJSONObject("unbilled") - ?.optJSONArray("CardUnbillTxnDetails") - if (unbilled != null) { - for (i in 0 until unbilled.length()) { - val item = unbilled.getJSONObject(i) - result.add(Transaction( - id = "unbilled_${item.optString("TranApprCode")}_$i", - date = item.optString("DateTime"), - description = item.optString("TranDesc").trim(), - amount = item.optDouble("BillingAmount", 0.0), - currency = item.optString("BillingCcy", "MVR"), - counterpartyName = null, - reference = item.optString("TranApprCode").takeIf { it.isNotBlank() }, - accountNumber = accountNumber, - accountDisplayName = accountDisplayName, - source = "BML_CARD" - )) - } - } - - val statement = payload.optJSONArray("cardstatement") - if (statement != null) { - for (i in 0 until statement.length()) { - val item = statement.getJSONObject(i) - result.add(Transaction( - id = "stmt_${item.optString("TranRef", i.toString())}", - date = item.optString("TransDate", item.optString("TranDate", "")), - description = item.optString("TranDesc", item.optString("Description", "")).trim(), - amount = -item.optDouble("TranAmount", 0.0), - currency = item.optString("TranCcy", "MVR"), - counterpartyName = null, - reference = item.optString("TranRef").takeIf { it.isNotBlank() }, - accountNumber = accountNumber, - accountDisplayName = accountDisplayName, - source = "BML_CARD" - )) - } - } - result - } catch (_: Exception) { emptyList() } - } - // ─── Parsing ────────────────────────────────────────────────────────────── /** @@ -787,148 +368,6 @@ class BmlLoginFlow { } } catch (_: Exception) { emptyList() } } - - private fun parseDashboard( - json: String, - loginTag: String, - profileName: String = "Personal", - profileId: String = "" - ): List { - val root = JSONObject(json) - if (!root.optBoolean("success")) return emptyList() - val dashboard = root.optJSONObject("payload")?.optJSONArray("dashboard") ?: return emptyList() - - val casaAccounts = mutableListOf() - val prepaidCards = mutableListOf() - - for (i in 0 until dashboard.length()) { - val item = dashboard.getJSONObject(i) - val currency = item.optString("currency", "MVR") - val accountType = item.optString("account_type", "CASA") - val product = item.optString("product") - val accountNumber = item.optString("account") - val status = item.optString("account_status", "Active") - val internalId = item.optString("id", "") - - if (accountType == "CASA") { - val available = item.optDouble("availableBalance", 0.0) - casaAccounts.add(MibAccount( - bank = "BML", - profileName = profileName, - profileType = "BML", - accountNumber = accountNumber, - accountBriefName = item.optString("alias"), - currencyName = currency, - accountTypeName = product, - availableBalance = "%.2f".format(available), - currentBalance = "%.2f".format(item.optDouble("ledgerBalance", 0.0)), - blockedAmount = "%.2f".format(item.optDouble("lockedAmount", 0.0)), - mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00", - statusDesc = status, - profileImageHash = null, - loginTag = loginTag, - profileId = profileId, - internalId = internalId - )) - } else if (accountType == "Card") { - val isVisible = item.optBoolean("account_visible", false) - if (!isVisible) continue - val isPrepaid = item.optBoolean("prepaid_card", false) - val cardBalance = item.optJSONObject("cardBalance") - val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0 - val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0 - prepaidCards.add(MibAccount( - bank = "BML", - profileName = profileName, - profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT", - accountNumber = accountNumber, - accountBriefName = item.optString("alias").ifBlank { product }, - currencyName = currency, - accountTypeName = product, - availableBalance = "%.2f".format(available), - currentBalance = "%.2f".format(current), - blockedAmount = "0.00", - mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00", - statusDesc = status, - profileImageHash = null, - loginTag = loginTag, - profileId = profileId, - internalId = internalId - )) - } - } - - return casaAccounts + prepaidCards - } - - private fun parseForeignLimits(json: String): List { - return try { - val root = JSONObject(json) - if (!root.optBoolean("success")) return emptyList() - val payload = root.optJSONArray("payload") ?: return emptyList() - (0 until payload.length()).map { i -> - val item = payload.getJSONObject(i) - val usage = item.optJSONObject("usageByCategory") ?: JSONObject() - val atm = usage.optJSONObject("ATM") ?: JSONObject() - val ecom = usage.optJSONObject("ECOM") ?: JSONObject() - val pos = usage.optJSONObject("POS") ?: JSONObject() - BmlForeignLimit( - type = item.optString("type", "Debit"), - used = item.optDouble("used", 0.0), - totalLimit = item.optDouble("totalLimit", 0.0), - generalCap = item.optDouble("generalCap", 0.0), - generalRemaining = item.optDouble("generalRemaining", 0.0), - medicalRemaining = item.optDouble("medicalRemaining", 0.0), - isAtmEnabled = item.optBoolean("isAtmEnabled", false), - isPosEnabled = item.optBoolean("isPosEnabled", false), - atmRemaining = atm.optDouble("remaining", 0.0), - atmLimit = atm.optDouble("limit", 0.0), - ecomRemaining = ecom.optDouble("remaining", 0.0), - ecomLimit = ecom.optDouble("limit", 0.0), - posRemaining = pos.optDouble("remaining", 0.0), - posLimit = pos.optDouble("limit", 0.0) - ) - } - } catch (_: Exception) { emptyList() } - } - - private fun parseContacts(json: String, loginId: String = ""): List { - val root = JSONObject(json) - if (!root.optBoolean("success")) return emptyList() - val payload: JSONArray = root.optJSONArray("payload") ?: return emptyList() - val result = mutableListOf() - for (i in 0 until payload.length()) { - val item = payload.getJSONObject(i) - val account = item.optString("account", "") - if (account.isBlank()) continue - result.add(MibBeneficiary( - benefNo = "bml_${item.optInt("id")}", - benefName = item.optString("name"), - benefNickName = item.optString("alias", item.optString("name")), - benefAccount = account, - benefType = "I", - bankColor = "#0066A1", - benefBankName = "Bank of Maldives", - bankCode = "", - benefStatus = item.optString("status", "S"), - transferCyDesc = item.optString("currency", "MVR"), - customerImgHash = null, - benefCategoryId = "BML", - profileId = loginId - )) - } - return result - } - - // ─── Helpers ────────────────────────────────────────────────────────────── - - private fun apiRequest(session: BmlSession, url: String) = - Request.Builder().url(url) - .header("Authorization", "Bearer ${session.accessToken}") - .header("User-Agent", APP_USER_AGENT) - .header("x-app-version", APP_VERSION) - .build() - private fun xsrfToken(): String? = cookieStore["www.bankofmaldives.com.mv"]?.firstOrNull { it.name == "XSRF-TOKEN" }?.value @@ -955,25 +394,4 @@ class BmlLoginFlow { return bytes.joinToString("") { "%02x".format(it) } } - // "12-05-2026 041675" → first 4 digits of time part as HH:mm - private fun parsePurchaseNarrative1(narrative1: String): String? { - return try { - val parts = narrative1.split(" ") - if (parts.size < 2) null - else { - val timePart = parts[1].take(4) - val combined = "${parts[0]} ${timePart.take(2)}:${timePart.drop(2)}:00" - val date = SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.US).parse(combined) - date?.let { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(it) } - } - } catch (_: Exception) { null } - } - - // "11-04-2026 13-17-08" → yyyy-MM-dd HH:mm:ss - private fun parseTransferNarrative1(narrative1: String): String? { - return try { - val date = SimpleDateFormat("dd-MM-yyyy HH-mm-ss", Locale.US).parse(narrative1) - date?.let { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(it) } - } catch (_: Exception) { null } - } } diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlModels.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlModels.kt index d54f75d..aabcaef 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlModels.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlModels.kt @@ -1,6 +1,6 @@ package sh.sar.basedbank.api.bml -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount data class BmlSession( val accessToken: String, @@ -24,7 +24,7 @@ data class BmlOtpChannel( sealed class BmlActivationResult { data class Success( val session: BmlSession, - val accounts: List + val accounts: List ) : BmlActivationResult() data class NeedsBusinessOtp( val channels: List diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlTransferClient.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlTransferClient.kt new file mode 100644 index 0000000..0fc6d11 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlTransferClient.kt @@ -0,0 +1,98 @@ +package sh.sar.basedbank.api.bml + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject + +class BmlTransferClient { + + private val client = newBmlApiClient() + + /** Step 1: initiate the transfer (triggers OTP). Returns true if the server accepted it. */ + fun initiateTransfer( + session: BmlSession, + debitAccount: String, + creditAccount: String, + amount: Double, + transferType: String, + currency: String, + bank: String? = null + ): Boolean { + val jo = JSONObject().apply { + put("debitAccount", debitAccount) + put("creditAccount", creditAccount) + put("debitAmount", amount) + put("transfertype", transferType) + put("currency", currency) + put("channel", "token") + if (bank != null) put("bank", bank) + } + val request = Request.Builder() + .url("$BML_BASE_URL/api/mobile/transfer") + .post(jo.toString().toRequestBody("application/json".toMediaType())) + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .header("accept", "application/json") + .build() + return client.newCall(request).execute().use { response -> + val bodyStr = response.body?.string() ?: return@use false + try { + val json = JSONObject(bodyStr) + json.optBoolean("success") && json.optInt("code") == 22 + } catch (_: Exception) { false } + } + } + + /** Step 2: confirm with OTP. Returns a [BmlTransferResult] with success/reference/error. */ + fun confirmTransfer( + session: BmlSession, + debitAccount: String, + creditAccount: String, + amount: Double, + transferType: String, + currency: String, + otp: String, + remarks: String = "", + bank: String? = null + ): BmlTransferResult { + val jo = JSONObject().apply { + put("debitAccount", debitAccount) + put("creditAccount", creditAccount) + put("debitAmount", amount) + put("transfertype", transferType) + put("currency", currency) + put("channel", "token") + put("otp", otp) + if (remarks.isNotBlank()) put("remarks", remarks) + if (bank != null) put("bank", bank) + } + val request = Request.Builder() + .url("$BML_BASE_URL/api/mobile/transfer") + .post(jo.toString().toRequestBody("application/json".toMediaType())) + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .header("accept", "application/json") + .build() + return client.newCall(request).execute().use { response -> + val bodyStr = response.body?.string() + ?: return@use BmlTransferResult(false, errorMessage = "No response") + try { + val json = JSONObject(bodyStr) + if (!json.optBoolean("success")) { + BmlTransferResult(false, errorMessage = json.optString("message").ifBlank { "Transfer failed" }) + } else { + val payload = json.optJSONObject("payload") + BmlTransferResult( + success = true, + reference = payload?.optString("reference") ?: "", + timestamp = payload?.optString("timestamp") ?: "", + message = json.optString("message") + ) + } + } catch (_: Exception) { BmlTransferResult(false, errorMessage = "Parse error") } + } + } +} 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 new file mode 100644 index 0000000..50cca52 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlValidateClient.kt @@ -0,0 +1,79 @@ +package sh.sar.basedbank.api.bml + +import okhttp3.Request +import org.json.JSONObject + +class BmlValidateClient { + + private val client = newBmlApiClient() + + fun validateAccount(session: BmlSession, input: String): BmlAccountValidation? { + val resp = client.newCall( + Request.Builder().url("$BML_BASE_URL/api/mobile/validate/account/$input") + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .header("Accept", "application/json") + .build() + ).execute() + val json = resp.body?.string() ?: return null + resp.close() + return try { + val root = JSONObject(json) + if (!root.optBoolean("success")) return null + val payload = root.optJSONObject("payload") ?: return null + val trnType = payload.optString("trnType", "") + val validationType = payload.optString("validationType", "") + if (validationType == "alias") { + val cdtrAcct = payload.optJSONObject("CdtrAcct") ?: return null + BmlAccountValidation( + trnType = trnType, + validationType = validationType, + account = cdtrAcct.optString("Acct"), + originalInput = input, + name = payload.optString("contact_name").trim(), + alias = null, + currency = payload.optString("currency", "MVR"), + agnt = cdtrAcct.optString("FinInstnId").takeIf { it.isNotBlank() } + ) + } else { + BmlAccountValidation( + trnType = trnType, + validationType = validationType, + account = payload.optString("account"), + originalInput = input, + name = payload.optString("name"), + alias = payload.optString("alias").takeIf { it.isNotBlank() && it != "null" }, + currency = payload.optString("currency", "MVR") + ) + } + } catch (_: Exception) { null } + } + + fun verifyMibAccount(session: BmlSession, account: String): BmlAccountValidation? { + val resp = client.newCall( + Request.Builder().url("$BML_BASE_URL/api/mobile/favara/account-verification/$account/MIB") + .header("Authorization", "Bearer ${session.accessToken}") + .header("User-Agent", BML_USER_AGENT) + .header("x-app-version", BML_APP_VERSION) + .header("Accept", "application/json") + .build() + ).execute() + val json = resp.body?.string() ?: return null + resp.close() + return try { + val root = JSONObject(json) + if (!root.optBoolean("success")) return null + BmlAccountValidation( + trnType = "DOT", + validationType = "MIB", + account = root.optString("account"), + originalInput = account, + name = root.optString("name"), + alias = null, + currency = "MVR", + agnt = root.optString("agnt").takeIf { it.isNotBlank() } + ) + } catch (_: Exception) { null } + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayAccountClient.kt b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayAccountClient.kt new file mode 100644 index 0000000..1107810 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayAccountClient.kt @@ -0,0 +1,76 @@ +package sh.sar.basedbank.api.fahipay + +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject +import sh.sar.basedbank.api.models.BankAccount +import java.util.concurrent.TimeUnit + +class FahipayAccountClient { + + private val BASE_URL = "https://fahipay.mv" + private val UA = "okhttp/4.12.0" + + private val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + private fun Request.Builder.auth(session: FahipaySession): Request.Builder = this + .header("authid", session.authId) + .header("Cookie", "__Secure-sess=${session.sessionCookie}") + .header("content-type", "multipart/form-data") + .header("User-Agent", UA) + + fun fetchProfile(session: FahipaySession): FahipayUserProfile { + val resp = client.newCall( + Request.Builder().url("$BASE_URL/actions/getprofile/?lang=en") + .auth(session).build() + ).execute() + val json = resp.body?.string() ?: throw Exception("Empty profile response") + resp.close() + val obj = JSONObject(json) + val props = obj.optJSONObject("props") ?: JSONObject() + return FahipayUserProfile( + fullName = obj.optString("fullname").trim(), + email = obj.optString("email").trim(), + mobile = obj.optString("mobile").trim(), + nid = obj.optString("nid").trim(), + profileId = obj.optString("profileID").trim(), + walletAccount = props.optString("acc", ""), + linkedAccounts = props.optJSONObject("accs")?.toString() ?: "{}" + ) + } + + fun fetchBalance(session: FahipaySession): Double { + val resp = client.newCall( + Request.Builder().url("$BASE_URL/actions/getbalance/?lang=en") + .auth(session).build() + ).execute() + val json = resp.body?.string() ?: return 0.0 + resp.close() + return try { + val obj = JSONObject(json) + if (obj.optBoolean("error")) 0.0 else obj.optDouble("balance", 0.0) + } catch (_: Exception) { 0.0 } + } + + fun buildAccount(profile: FahipayUserProfile, balance: Double, loginTag: String): BankAccount = + BankAccount( + bank = "FAHIPAY", + profileName = profile.fullName.ifBlank { "Fahipay" }, + profileType = "FAHIPAY", + accountNumber = profile.walletAccount, + accountBriefName = "Fahipay Wallet", + currencyName = "MVR", + accountTypeName = "Digital Wallet", + availableBalance = "%.2f".format(balance), + currentBalance = "%.2f".format(balance), + blockedAmount = "0.00", + mvrBalance = "%.2f".format(balance), + statusDesc = "Active", + profileImageHash = null, + loginTag = loginTag, + internalId = profile.profileId + ) +} diff --git a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayContactsClient.kt b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayContactsClient.kt new file mode 100644 index 0000000..4bc5804 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayContactsClient.kt @@ -0,0 +1,69 @@ +package sh.sar.basedbank.api.fahipay + +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject +import sh.sar.basedbank.api.models.BankContact +import sh.sar.basedbank.util.AccountInputParser +import java.util.concurrent.TimeUnit + +class FahipayContactsClient { + + private val BASE_URL = "https://fahipay.mv" + private val UA = "okhttp/4.12.0" + + private val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + fun fetchContacts(session: FahipaySession): List { + val endpoints = listOf( + Triple("FAHIPAY_RAASTAS", "Raastas", "ooredooraastas"), + Triple("FAHIPAY_RELOAD", "Reload", "dhiraagureload"), + Triple("FAHIPAY_OOREDOO_BILL", "Ooredoo Bill", "ooredoobillpay"), + Triple("FAHIPAY_DHIRAAGU_BILL", "Dhiraagu Bill", "dhiraagubillpay") + ) + val result = mutableListOf() + for ((catId, label, page) in endpoints) { + try { + val resp = client.newCall( + Request.Builder() + .url("$BASE_URL/api/app/favs/?page=$page&lang=en") + .header("authid", session.authId) + .header("Cookie", "__Secure-sess=${session.sessionCookie}") + .header("User-Agent", UA) + .build() + ).execute() + val json = resp.body?.string() ?: continue + resp.close() + val obj = JSONObject(json) + val groupObj = obj.optJSONObject(page) ?: continue + val contacts = mutableListOf() + for (key in groupObj.keys()) { + val entry = groupObj.getJSONObject(key) + val number = entry.optString("number") + val name = entry.optString("name").trim().ifBlank { number } + if (AccountInputParser.detect(number) != AccountInputParser.InputType.PHONE) continue + contacts.add(BankContact( + benefNo = "fp_${page}_$number", + benefName = "", + benefNickName = name, + benefAccount = number, + benefType = "FAHIPAY", + bankColor = "#FF6B00", + benefBankName = label, + bankCode = "", + benefStatus = "", + transferCyDesc = "", + customerImgHash = null, + benefCategoryId = catId, + profileId = "" + )) + } + if (contacts.isNotEmpty()) result.add(FahipayContactGroup(catId, label, contacts)) + } catch (_: Exception) {} + } + return result + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayHistoryClient.kt b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayHistoryClient.kt new file mode 100644 index 0000000..63c1b74 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayHistoryClient.kt @@ -0,0 +1,60 @@ +package sh.sar.basedbank.api.fahipay + +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject +import sh.sar.basedbank.api.models.BankTransaction +import java.util.concurrent.TimeUnit + +class FahipayHistoryClient { + + private val BASE_URL = "https://fahipay.mv" + private val UA = "okhttp/4.12.0" + private val PAGE_SIZE = 15 + + private val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + fun fetchHistory( + session: FahipaySession, + accountDisplayName: String, + accountNumber: String, + start: Int + ): Pair, Int> { + val resp = client.newCall( + Request.Builder() + .url("$BASE_URL/actions/activity/?s=$start&l=$PAGE_SIZE&lang=en") + .header("authid", session.authId) + .header("Cookie", "__Secure-sess=${session.sessionCookie}") + .header("content-type", "multipart/form-data") + .header("User-Agent", UA) + .build() + ).execute() + val json = resp.body?.string() ?: return Pair(emptyList(), 0) + resp.close() + return try { + val obj = JSONObject(json) + val total = obj.optInt("total", 0) + val entries = obj.optJSONArray("entries") ?: return Pair(emptyList(), total) + val list = (0 until entries.length()).map { i -> + val e = entries.getJSONObject(i) + BankTransaction( + id = e.optString("transaction"), + date = e.optString("date"), + description = e.optString("name").trim(), + amount = e.optDouble("amount", 0.0), + currency = "MVR", + counterpartyName = e.optString("details").takeIf { it.isNotBlank() }, + reference = e.optString("transaction").takeIf { it.isNotBlank() }, + accountNumber = accountNumber, + accountDisplayName = accountDisplayName, + source = "FAHIPAY", + iconUrl = e.optString("icon").takeIf { it.isNotBlank() } + ) + } + Pair(list, total) + } catch (_: Exception) { Pair(emptyList(), 0) } + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt index 8adf1df..1a03f65 100644 --- a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt @@ -10,10 +10,6 @@ import okhttp3.Request import okhttp3.RequestBody import okio.Buffer import org.json.JSONObject -import sh.sar.basedbank.api.mib.MibAccount -import sh.sar.basedbank.api.mib.MibBeneficiary -import sh.sar.basedbank.api.mib.Transaction -import sh.sar.basedbank.util.AccountInputParser import java.security.SecureRandom import java.util.concurrent.TimeUnit @@ -21,8 +17,6 @@ class FahipayLoginFlow { private val BASE_URL = "https://fahipay.mv" private val UA_WEBVIEW = "Mozilla/5.0 (Linux; Android 14; ${Build.MODEL} Build/AP2A.240905.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36" - private val UA_OKHTTP = "okhttp/4.12.0" - private val PAGE_SIZE = 15 private val cookieStore = mutableMapOf>() private val cookieJar = object : CookieJar { @@ -144,165 +138,6 @@ class FahipayLoginFlow { ?: throw Exception("No authID in OTP response") } - fun fetchProfile(session: FahipaySession): FahipayUserProfile { - val resp = client.newCall( - Request.Builder().url("$BASE_URL/actions/getprofile/?lang=en") - .header("authid", session.authId) - .header("content-type", "multipart/form-data") - .header("User-Agent", UA_OKHTTP) - .build() - ).execute() - val json = resp.body?.string() ?: throw Exception("Empty profile response") - resp.close() - - val obj = JSONObject(json) - val props = obj.optJSONObject("props") ?: JSONObject() - return FahipayUserProfile( - fullName = obj.optString("fullname").trim(), - email = obj.optString("email").trim(), - mobile = obj.optString("mobile").trim(), - nid = obj.optString("nid").trim(), - profileId = obj.optString("profileID").trim(), - walletAccount = props.optString("acc", ""), - linkedAccounts = props.optJSONObject("accs")?.toString() ?: "{}" - ) - } - - fun fetchBalance(session: FahipaySession): Double { - val resp = client.newCall( - Request.Builder().url("$BASE_URL/actions/getbalance/?lang=en") - .header("authid", session.authId) - .header("content-type", "multipart/form-data") - .header("User-Agent", UA_OKHTTP) - .build() - ).execute() - val json = resp.body?.string() ?: return 0.0 - resp.close() - return try { - val obj = JSONObject(json) - if (obj.optBoolean("error")) 0.0 else obj.optDouble("balance", 0.0) - } catch (_: Exception) { 0.0 } - } - - fun buildAccount(profile: FahipayUserProfile, balance: Double, loginTag: String): MibAccount = - MibAccount( - bank = "FAHIPAY", - profileName = profile.fullName.ifBlank { "Fahipay" }, - profileType = "FAHIPAY", - accountNumber = profile.walletAccount, - accountBriefName = "Fahipay Wallet", - currencyName = "MVR", - accountTypeName = "Digital Wallet", - availableBalance = "%.2f".format(balance), - currentBalance = "%.2f".format(balance), - blockedAmount = "0.00", - mvrBalance = "%.2f".format(balance), - statusDesc = "Active", - profileImageHash = null, - loginTag = loginTag, - internalId = profile.profileId - ) - - /** - * Fetches paginated activity history. - * @param start offset (0-based) - * @return Pair of (transactions, total count) - */ - fun fetchHistory( - session: FahipaySession, - accountDisplayName: String, - accountNumber: String, - start: Int - ): Pair, Int> { - val resp = client.newCall( - Request.Builder() - .url("$BASE_URL/actions/activity/?s=$start&l=$PAGE_SIZE&lang=en") - .header("authid", session.authId) - .header("content-type", "multipart/form-data") - .header("User-Agent", UA_OKHTTP) - .build() - ).execute() - val json = resp.body?.string() ?: return Pair(emptyList(), 0) - resp.close() - return try { - val obj = JSONObject(json) - val total = obj.optInt("total", 0) - val entries = obj.optJSONArray("entries") ?: return Pair(emptyList(), total) - val list = (0 until entries.length()).map { i -> - val e = entries.getJSONObject(i) - Transaction( - id = e.optString("transaction"), - date = e.optString("date"), - description = e.optString("name").trim(), - amount = e.optDouble("amount", 0.0), - currency = "MVR", - counterpartyName = e.optString("details").takeIf { it.isNotBlank() }, - reference = e.optString("transaction").takeIf { it.isNotBlank() }, - accountNumber = accountNumber, - accountDisplayName = accountDisplayName, - source = "FAHIPAY", - iconUrl = e.optString("icon").takeIf { it.isNotBlank() } - ) - } - Pair(list, total) - } catch (_: Exception) { Pair(emptyList(), 0) } - } - - /** - * Fetches Fahipay saved favourites for the 4 service groups. - * Only includes entries whose number is a valid 7-digit Maldivian phone number (starts with 7 or 9). - * Groups with no valid entries are omitted. - */ - fun fetchContacts(session: FahipaySession): List { - val endpoints = listOf( - Triple("FAHIPAY_RAASTAS", "Raastas", "ooredooraastas"), - Triple("FAHIPAY_RELOAD", "Reload", "dhiraagureload"), - Triple("FAHIPAY_OOREDOO_BILL", "Ooredoo Bill", "ooredoobillpay"), - Triple("FAHIPAY_DHIRAAGU_BILL", "Dhiraagu Bill", "dhiraagubillpay") - ) - val result = mutableListOf() - for ((catId, label, page) in endpoints) { - try { - val resp = client.newCall( - Request.Builder() - .url("$BASE_URL/api/app/favs/?page=$page&lang=en") - .header("authid", session.authId) - .header("User-Agent", UA_OKHTTP) - .build() - ).execute() - val json = resp.body?.string() ?: continue - resp.close() - val obj = JSONObject(json) - // Empty group comes back as a JSON array [], not an object — optJSONObject returns null - val groupObj = obj.optJSONObject(page) ?: continue - val contacts = mutableListOf() - for (key in groupObj.keys()) { - val entry = groupObj.getJSONObject(key) - val number = entry.optString("number") - val name = entry.optString("name").trim().ifBlank { number } - if (AccountInputParser.detect(number) != AccountInputParser.InputType.PHONE) continue - contacts.add(MibBeneficiary( - benefNo = "fp_${page}_$number", - benefName = "", - benefNickName = name, - benefAccount = number, - benefType = "FAHIPAY", - bankColor = "#FF6B00", - benefBankName = label, - bankCode = "", - benefStatus = "", - transferCyDesc = "", - customerImgHash = null, - benefCategoryId = catId, - profileId = "" - )) - } - if (contacts.isNotEmpty()) result.add(FahipayContactGroup(catId, label, contacts)) - } catch (_: Exception) {} - } - return result - } - private fun deviceParts(deviceUuid: String): Array> = arrayOf( "device[available]" to "true", "device[platform]" to "Android", diff --git a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayModels.kt b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayModels.kt index 322f646..11c799a 100644 --- a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayModels.kt +++ b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayModels.kt @@ -1,5 +1,7 @@ package sh.sar.basedbank.api.fahipay +import sh.sar.basedbank.api.models.BankContact + data class FahipaySession( val authId: String, val sessionCookie: String @@ -23,5 +25,5 @@ data class FahipayLoginStep( data class FahipayContactGroup( val categoryId: String, val label: String, - val contacts: List + val contacts: List ) diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt index 202fa0b..80553ff 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt @@ -356,44 +356,6 @@ class MibLoginFlow(private val credentialStore: CredentialStore) { } } - data class MibPersonalProfile( - val fullName: String, - val username: String, - val email: String, - val mobile: String, - val enrolled: String - ) - - /** Fetches the customer's profile info from the Faisanet personal profile page. */ - fun fetchPersonalProfile(session: MibSession): MibPersonalProfile? { - val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " + - "IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597" - val request = Request.Builder() - .url("https://faisamobilex-wv.mib.com.mv/personalProfile") - .get() - .header("Cookie", cookieHeader) - .build() - return try { - val resp = client.newCall(request).execute() - val html = resp.body?.string() ?: return null - resp.close() - fun scrape(label: String): String { - val r = Regex("""]*>\s*]*>\s*$label\s*]*>.*?]*>([^<]+)""", - setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE)) - return r.find(html)?.groupValues?.get(1)?.trim() ?: "" - } - val nameRegex = Regex("""
\s*([^<]+)\s*
""") - val fullName = nameRegex.find(html)?.groupValues?.get(1)?.trim() ?: return null - MibPersonalProfile( - fullName = fullName, - username = scrape("Username:"), - email = scrape("Email:"), - mobile = scrape("Mobile no:"), - enrolled = scrape("Enrolled:") - ) - } catch (_: Exception) { null } - } - /** Fetches a profile image via P41. Returns base64 JPEG string, or null if not found. */ fun fetchProfileImage(session: MibSession, imageHash: String): String? { val payload = baseData(session, "P41").apply { 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 a0ed66a..5f97b9c 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 @@ -1,5 +1,16 @@ package sh.sar.basedbank.api.mib +import sh.sar.basedbank.api.models.BankAccount +import sh.sar.basedbank.api.models.BankContact +import sh.sar.basedbank.api.models.BankContactCategory +import sh.sar.basedbank.api.models.BankTransaction + +// Kept for source compatibility within the mib package +typealias MibAccount = BankAccount +typealias MibBeneficiary = BankContact +typealias Transaction = BankTransaction +typealias MibBeneficiaryCategory = BankContactCategory + data class MibSession( val appId: String, val xxid: String, @@ -19,25 +30,6 @@ data class MibProfile( val customerImage: String? ) -data class MibAccount( - val bank: String, // "MIB", "BML", "FAHIPAY" — set by the login flow - val profileName: String, - val profileType: String, - val cifType: String = "", // MIB: human-readable profile category (e.g. "Individual", "Sole Propr"); empty for other banks - val accountNumber: String, - val accountBriefName: String, - val currencyName: String, - val accountTypeName: String, - val availableBalance: String, - val currentBalance: String, - val blockedAmount: String, - val mvrBalance: String, - val statusDesc: String, - val profileImageHash: String?, - val loginTag: String = "", - val profileId: String = "", // MIB profile ID; empty for BML accounts - val internalId: String = "" // BML internal UUID; empty for MIB accounts -) data class MibTransferResult( val success: Boolean, @@ -46,27 +38,6 @@ data class MibTransferResult( val errorMessage: String = "" ) -data class MibBeneficiaryCategory( - val id: String, - val categoryName: String, - val numBenef: Int -) - -data class MibBeneficiary( - val benefNo: String, - val benefName: String, - val benefNickName: String, - val benefAccount: String, - val benefType: String, // L=Local, I=Internal(MIB), S=Swift - val bankColor: String, - val benefBankName: String, - val bankCode: String, - val benefStatus: String, - val transferCyDesc: String, - val customerImgHash: String?, - val benefCategoryId: String, // "0" = uncategorized - val profileId: String = "" // MIB profile ID; empty for BML contacts -) data class MibIpsAccountInfo( val accountName: String, @@ -74,19 +45,6 @@ data class MibIpsAccountInfo( val bankId: String ) -data class Transaction( - val id: String, - val date: String, // "YYYY-MM-DD HH:mm:ss" for MIB, ISO8601 for BML - val description: String, - val amount: Double, // negative = debit, positive = credit - val currency: String, - val counterpartyName: String?, - val reference: String?, - val accountNumber: String, - val accountDisplayName: String, - val source: String, // "MIB", "BML", "BML_CARD", "FAHIPAY" - val iconUrl: String? = null // merchant icon URL (Fahipay only) -) data class MibFinanceDeal( val dealNo: String, diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibProfileClient.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibProfileClient.kt new file mode 100644 index 0000000..e368000 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibProfileClient.kt @@ -0,0 +1,50 @@ +package sh.sar.basedbank.api.mib + +import okhttp3.OkHttpClient +import okhttp3.Request +import java.util.concurrent.TimeUnit + +data class MibPersonalProfile( + val fullName: String, + val username: String, + val email: String, + val mobile: String, + val enrolled: String +) + +class MibProfileClient { + + private val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + fun fetchPersonalProfile(session: MibSession): MibPersonalProfile? { + val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " + + "IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597" + val request = Request.Builder() + .url("https://faisamobilex-wv.mib.com.mv/personalProfile") + .get() + .header("Cookie", cookieHeader) + .build() + return try { + val resp = client.newCall(request).execute() + val html = resp.body?.string() ?: return null + resp.close() + fun scrape(label: String): String { + val r = Regex("""]*>\s*]*>\s*$label\s*]*>.*?]*>([^<]+)""", + setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE)) + return r.find(html)?.groupValues?.get(1)?.trim() ?: "" + } + val nameRegex = Regex("""
\s*([^<]+)\s*
""") + val fullName = nameRegex.find(html)?.groupValues?.get(1)?.trim() ?: return null + MibPersonalProfile( + fullName = fullName, + username = scrape("Username:"), + email = scrape("Email:"), + mobile = scrape("Mobile no:"), + enrolled = scrape("Enrolled:") + ) + } catch (_: Exception) { null } + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/models/BankModels.kt b/app/src/main/java/sh/sar/basedbank/api/models/BankModels.kt new file mode 100644 index 0000000..81213be --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/models/BankModels.kt @@ -0,0 +1,72 @@ +package sh.sar.basedbank.api.models + +/** + * Unified account model used across all banks (MIB, BML, Fahipay, ...). + * The [bank] field identifies which bank owns this account. + */ +data class BankAccount( + val bank: String, // "MIB", "BML", "FAHIPAY" — set by the login flow + val profileName: String, + val profileType: String, + val cifType: String = "", // MIB: human-readable profile category (e.g. "Individual", "Sole Propr"); empty for other banks + val accountNumber: String, + val accountBriefName: String, + val currencyName: String, + val accountTypeName: String, + val availableBalance: String, + val currentBalance: String, + val blockedAmount: String, + val mvrBalance: String, + val statusDesc: String, + val profileImageHash: String?, + val loginTag: String = "", + val profileId: String = "", // profile ID used by the bank; empty if not applicable + val internalId: String = "" // bank-internal UUID or ID; empty if not applicable +) + +/** + * Unified contact/beneficiary model used across all banks. + * Each bank may interpret fields differently; see per-bank notes below. + */ +data class BankContact( + val benefNo: String, + val benefName: String, + val benefNickName: String, + val benefAccount: String, + val benefType: String, // MIB: L=Local, I=Internal, S=Swift; BML: "I"; Fahipay: "FAHIPAY" + val bankColor: String, + val benefBankName: String, + val bankCode: String, + val benefStatus: String, + val transferCyDesc: String, + val customerImgHash: String?, + val benefCategoryId: String, // MIB: numeric category ID or "0"; BML: "BML"; Fahipay: "FAHIPAY" + val profileId: String = "" // owning profile ID; empty where not applicable +) + +/** + * Contact category (group) used across MIB and Fahipay. + */ +data class BankContactCategory( + val id: String, + val categoryName: String, + val numBenef: Int +) + +/** + * Unified transaction model used across all banks. + * [source] identifies the originating bank/account type. + */ +data class BankTransaction( + val id: String, + val date: String, // "YYYY-MM-DD HH:mm:ss" (MIB/BML normalised) or ISO8601 + val description: String, + val amount: Double, // negative = debit, positive = credit + val currency: String, + val counterpartyName: String?, + val reference: String?, + val accountNumber: String, + val accountDisplayName: String, + val source: String, // "MIB", "BML", "BML_CARD", "FAHIPAY" + val iconUrl: String? = null // merchant icon URL (Fahipay only) +) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt index 5a03be4..22bf102 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt @@ -9,8 +9,8 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder -import sh.sar.basedbank.api.mib.MibAccount -import sh.sar.basedbank.api.mib.Transaction +import sh.sar.basedbank.api.models.BankAccount +import sh.sar.basedbank.api.models.BankTransaction import sh.sar.basedbank.util.AccountHistoryDisplay import sh.sar.basedbank.databinding.ItemAccountHistoryHeaderBinding import sh.sar.basedbank.databinding.ItemDateHeaderBinding @@ -21,13 +21,13 @@ import java.util.Date import java.util.Locale class AccountHistoryAdapter( - private val account: MibAccount, + private val account: BankAccount, private val display: AccountHistoryDisplay ) : RecyclerView.Adapter() { private sealed class Item { data class DateHeader(val label: String) : Item() - data class Trx(val transaction: Transaction) : Item() + data class Trx(val transaction: BankTransaction) : Item() } private val displayItems = mutableListOf() @@ -36,7 +36,7 @@ class AccountHistoryAdapter( private val iconUrlCache = mutableMapOf() var onImageNeeded: ((counterpartyName: String) -> Unit)? = null var onIconUrlNeeded: ((url: String) -> Unit)? = null - var onTransferClick: ((MibAccount) -> Unit)? = null + var onTransferClick: ((BankAccount) -> Unit)? = null private var hideAmounts: Boolean = false fun setHideAmounts(hide: Boolean) { @@ -84,7 +84,7 @@ class AccountHistoryAdapter( * Display the given (already sorted + filtered) list with date group headers. * Silently resets the loading footer so notifyDataSetChanged covers everything. */ - fun setTransactions(transactions: List) { + fun setTransactions(transactions: List) { _showLoadingFooter = false displayItems.clear() lastInsertedDateKey = "" @@ -105,7 +105,7 @@ class AccountHistoryAdapter( * Appends [newTransactions] (assumed to be older than all existing items) using incremental * notifications, so the RecyclerView doesn't reset scroll position. */ - fun appendTransactions(newTransactions: List) { + fun appendTransactions(newTransactions: List) { if (newTransactions.isEmpty()) return if (_showLoadingFooter) { val pos = itemCount - 1 @@ -184,7 +184,7 @@ class AccountHistoryAdapter( inner class TransactionVH(private val b: ItemTransactionBinding) : RecyclerView.ViewHolder(b.root) { - fun bind(trx: Transaction) { + fun bind(trx: BankTransaction) { val isCredit = trx.amount >= 0 val color = sourceColor(trx.source) val name = trx.counterpartyName ?: trx.description @@ -237,7 +237,7 @@ class AccountHistoryAdapter( b.root.setOnClickListener { showDetail(trx) } } - private fun showDetail(trx: Transaction) { + private fun showDetail(trx: BankTransaction) { val ctx = b.root.context val title = trx.counterpartyName?.takeIf { it.isNotBlank() } ?: trx.description val details = buildString { diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt index c0e6388..6d7dd6e 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt @@ -22,9 +22,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibContactsClient -import sh.sar.basedbank.api.mib.Transaction +import sh.sar.basedbank.api.models.BankTransaction import sh.sar.basedbank.api.mib.TransactionCache import sh.sar.basedbank.databinding.FragmentAccountHistoryBinding import sh.sar.basedbank.util.AccountHistoryParser @@ -39,10 +39,10 @@ class AccountHistoryFragment : Fragment() { private val viewModel: HomeViewModel by activityViewModels() private lateinit var adapter: AccountHistoryAdapter - private lateinit var account: MibAccount + private lateinit var account: BankAccount private lateinit var fetcher: HistoryFetcher - private val allTransactions = mutableListOf() + private val allTransactions = mutableListOf() private var searchQuery = "" private var firstPageDone = false private val pendingImageNames = mutableSetOf() @@ -53,7 +53,7 @@ class AccountHistoryFragment : Fragment() { companion object { private const val ARG_ACCOUNT_NUMBER = "account_number" - fun newInstance(account: MibAccount) = AccountHistoryFragment().apply { + fun newInstance(account: BankAccount) = AccountHistoryFragment().apply { arguments = Bundle().apply { putString(ARG_ACCOUNT_NUMBER, account.accountNumber) } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AccountsAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AccountsAdapter.kt index 0af7fec..01b5fb7 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AccountsAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AccountsAdapter.kt @@ -8,7 +8,7 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.recyclerview.widget.RecyclerView -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.databinding.ItemAccountBinding import sh.sar.basedbank.databinding.ItemCardBinding import sh.sar.basedbank.databinding.ItemDateHeaderBinding @@ -16,22 +16,22 @@ import sh.sar.basedbank.util.AccountListDisplay import sh.sar.basedbank.util.AccountListParser class AccountsAdapter( - accounts: List, - private val onAccountClick: (MibAccount) -> Unit = {} + accounts: List, + private val onAccountClick: (BankAccount) -> Unit = {} ) : RecyclerView.Adapter() { - var onTransferClick: ((MibAccount) -> Unit)? = null + var onTransferClick: ((BankAccount) -> Unit)? = null private var hideAmounts: Boolean = false private sealed class Item { data class SectionTitle(val label: String) : Item() - data class Account(val account: MibAccount, val display: AccountListDisplay) : Item() - data class Card(val account: MibAccount, val display: AccountListDisplay) : Item() + data class Account(val account: BankAccount, val display: AccountListDisplay) : Item() + data class Card(val account: BankAccount, val display: AccountListDisplay) : Item() } private val items: MutableList = buildItems(accounts).toMutableList() - fun updateAccounts(accounts: List) { + fun updateAccounts(accounts: List) { items.clear() items.addAll(buildItems(accounts)) notifyDataSetChanged() @@ -43,12 +43,12 @@ class AccountsAdapter( notifyDataSetChanged() } - private fun buildItems(accounts: List): List = buildList { + private fun buildItems(accounts: List): List = buildList { val displayed = accounts.mapNotNull { acc -> AccountListParser.from(acc)?.let { acc to it } } val nonCards = displayed.filter { !it.second.isCard } val cards = displayed.filter { it.second.isCard } - val groups = LinkedHashMap>>() + val groups = LinkedHashMap>>() for ((acc, display) in nonCards) { val title = sectionTitle(acc) groups.getOrPut(title) { mutableListOf() }.add(acc to display) @@ -64,7 +64,7 @@ class AccountsAdapter( } } - private fun sectionTitle(account: MibAccount): String { + private fun sectionTitle(account: BankAccount): String { val bankName = when (account.bank) { "BML" -> "Bank of Maldives" "FAHIPAY" -> "Fahipay" @@ -112,7 +112,7 @@ class AccountsAdapter( private inner class AccountViewHolder(private val binding: ItemAccountBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(account: MibAccount, display: AccountListDisplay) { + fun bind(account: BankAccount, display: AccountListDisplay) { binding.tvAccountName.text = display.name binding.tvAccountNumber.text = display.number binding.tvAccountType.text = display.typeLabel @@ -128,7 +128,7 @@ class AccountsAdapter( private inner class CardViewHolder(private val binding: ItemCardBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(account: MibAccount, display: AccountListDisplay) { + fun bind(account: BankAccount, display: AccountListDisplay) { binding.ivCardBrand.setImageResource(display.cardBrandIcon) binding.tvCardName.text = display.name binding.tvCardNumber.text = display.number 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 b5e39aa..a64c732 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 @@ -30,8 +30,9 @@ import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R import sh.sar.basedbank.api.bml.BmlAccountValidation -import sh.sar.basedbank.api.bml.BmlLoginFlow -import sh.sar.basedbank.api.mib.MibBeneficiaryCategory +import sh.sar.basedbank.api.bml.BmlContactsClient +import sh.sar.basedbank.api.bml.BmlValidateClient +import sh.sar.basedbank.api.models.BankContactCategory import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.api.mib.MibProfile import sh.sar.basedbank.api.mib.MibTransferClient @@ -64,7 +65,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { private var selectedImageBase64: String = "" private var selectedCategoryId: String = "0" - private var categories: List = emptyList() + private var categories: List = emptyList() private val imagePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> uri ?: return@registerForActivityResult @@ -244,14 +245,12 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { private fun lookupForBml(input: String): BmlAccountValidation? { val loginId = selectedDest?.bmlLoginId ?: return null val bmlSess = app.bmlSessions[loginId] ?: return null - val bmlFlow = BmlLoginFlow() - // 1) Try BML validate - val validated = try { bmlFlow.validateAccount(bmlSess, input) } catch (_: Exception) { null } + val validated = try { BmlValidateClient().validateAccount(bmlSess, input) } catch (_: Exception) { null } if (validated != null) return validated // 2) Try BML MIB verify - val mibVerified = try { bmlFlow.verifyMibAccount(bmlSess, input) } catch (_: Exception) { null } + val mibVerified = try { BmlValidateClient().verifyMibAccount(bmlSess, input) } catch (_: Exception) { null } if (mibVerified != null) return mibVerified // 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML) @@ -295,7 +294,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { // MIB lookup failed (e.g. BML USD account) — fall back to BML validate val bmlSess = app.anyBmlSession() ?: return null - return try { BmlLoginFlow().validateAccount(bmlSess, input) } catch (_: Exception) { null } + return try { BmlValidateClient().validateAccount(bmlSess, input) } catch (_: Exception) { null } } private fun showLookupResult(validation: BmlAccountValidation, input: String) { @@ -421,15 +420,14 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { val loginId = selectedDest?.bmlLoginId ?: return false val bmlSess = app.bmlSessions[loginId] ?: return false val lookup = bmlLookup ?: return false - val bmlFlow = BmlLoginFlow() val account = lookup.account return when { account.matches(Regex("^7\\d{12}$")) -> // BML account → IAT - bmlFlow.saveContact(bmlSess, "IAT", account, alias) + BmlContactsClient().saveContact(bmlSess, "IAT", account, alias) account.matches(Regex("^9\\d{16}$")) -> // MIB internal → DOT; swift is BML's internal UUID for MIB bank - bmlFlow.saveContact(bmlSess, "DOT", account, alias, + BmlContactsClient().saveContact(bmlSess, "DOT", account, alias, currency = lookup.currency, name = lookup.name, swift = MIB_SWIFT_ON_BML) else -> false } @@ -489,7 +487,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { if (dest.isBml) { val loginId = dest.bmlLoginId ?: return@launch val bmlSess = app.bmlSessions[loginId] ?: return@launch - val fresh = BmlLoginFlow().fetchContacts(bmlSess, loginId) + val fresh = BmlContactsClient().fetchContacts(bmlSess, loginId) val existing = viewModel.contacts.value ?: emptyList() val merged = existing.filter { it.benefCategoryId != "BML" } + fresh viewModel.contacts.postValue(merged) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt index 37df3d6..a005021 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R -import sh.sar.basedbank.api.mib.MibBeneficiaryCategory +import sh.sar.basedbank.api.models.BankContactCategory import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.databinding.FragmentContactsBinding import sh.sar.basedbank.util.ContactDisplay @@ -153,7 +153,7 @@ class ContactsFragment : Fragment() { }.also { it.attach() } } - private fun rebuildPager(cats: List) { + private fun rebuildPager(cats: List) { val pages = buildList { add(TabPage(null, getString(R.string.contacts_tab_all))) cats.forEach { add(TabPage(it.id, it.categoryName)) } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt index 05e2bce..ed485a8 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt @@ -12,7 +12,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import sh.sar.basedbank.R import sh.sar.basedbank.api.bml.BmlForeignLimit -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibFinanceDeal import sh.sar.basedbank.databinding.FragmentDashboardBinding import sh.sar.basedbank.databinding.ItemForeignLimitBinding @@ -68,7 +68,7 @@ class DashboardFragment : Fragment() { } } - private fun updateBalances(accounts: List) { + private fun updateBalances(accounts: List) { val hide = viewModel.hideAmounts.value ?: false if (hide) { binding.tvMvrBalance.text = "MVR ••••••" diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt index 0138826..ea4b2e1 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt @@ -36,18 +36,22 @@ import okhttp3.RequestBody.Companion.toRequestBody import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R import sh.sar.basedbank.api.bml.AuthExpiredException +import sh.sar.basedbank.api.bml.BmlAccountClient import sh.sar.basedbank.api.bml.BmlActivationResult -import sh.sar.basedbank.api.bml.BmlLoginFlow +import sh.sar.basedbank.api.bml.BmlContactsClient +import sh.sar.basedbank.api.bml.BmlForeignLimitsClient import sh.sar.basedbank.api.bml.BmlProfile import sh.sar.basedbank.api.bml.BmlSession +import sh.sar.basedbank.api.fahipay.FahipayAccountClient +import sh.sar.basedbank.api.fahipay.FahipayContactsClient import sh.sar.basedbank.api.fahipay.FahipayLoginFlow import sh.sar.basedbank.api.fahipay.FahipaySession -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibLoginFlow import sh.sar.basedbank.databinding.ActivityHomeBinding import sh.sar.basedbank.ui.login.LoginActivity -import sh.sar.basedbank.api.mib.MibBeneficiary -import sh.sar.basedbank.api.mib.MibBeneficiaryCategory +import sh.sar.basedbank.api.models.BankContact +import sh.sar.basedbank.api.models.BankContactCategory import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.api.mib.MibFinancingClient import sh.sar.basedbank.api.mib.MibProfile @@ -490,7 +494,7 @@ fun applyNavLabelVisibility() { val loginTag = "bml_$loginId" val app = application as BasedBankApp val savedProfiles = store.loadBmlProfiles(loginId) - val allAccounts = mutableListOf() + val allAccounts = mutableListOf() var anyExpired = savedProfiles.isEmpty() // Try each saved profile's cached session @@ -499,7 +503,7 @@ fun applyNavLabelVisibility() { if (saved != null) { try { val session = BmlSession(saved.first, saved.second) - val accounts = BmlLoginFlow().fetchAccounts(session, loginTag, profile.name, profile.profileId) + val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profile.name, profile.profileId) app.bmlSessions[profile.profileId] = session allAccounts += accounts } catch (_: AuthExpiredException) { anyExpired = true @@ -517,7 +521,7 @@ fun applyNavLabelVisibility() { if (legacyToken != null) { try { val session = BmlSession(legacyToken.first, legacyToken.second) - val accounts = BmlLoginFlow().fetchAccounts(session, loginTag) + val accounts = BmlAccountClient().fetchAccounts(session, loginTag) app.bmlSessions[loginId] = session allAccounts += accounts anyExpired = false @@ -565,7 +569,7 @@ fun applyNavLabelVisibility() { } if (allAccounts.isNotEmpty()) AccountCache.saveBml(this@HomeActivity, loginId, allAccounts) - allAccounts as List + allAccounts as List } } @@ -581,10 +585,9 @@ fun applyNavLabelVisibility() { if (savedSession != null) { try { val session = FahipaySession(savedSession.first, savedSession.second) - fahipayFlow.setSessionCookie(session.sessionCookie) - val balance = fahipayFlow.fetchBalance(session) - val profile = fahipayFlow.fetchProfile(session) - val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag)) + val balance = FahipayAccountClient().fetchBalance(session) + val profile = FahipayAccountClient().fetchProfile(session) + val accounts = listOf(FahipayAccountClient().buildAccount(profile, balance, loginTag)) val app = application as BasedBankApp app.fahipaySessions[loginId] = session AccountCache.saveFahipay(this@HomeActivity, loginId, accounts) @@ -601,9 +604,9 @@ fun applyNavLabelVisibility() { val cookieValue = fahipayFlow.getSessionCookieValue() ?: "" val session = FahipaySession(authId, cookieValue) store.saveFahipaySession(loginId, authId, cookieValue) - val profile = fahipayFlow.fetchProfile(session) - val balance = fahipayFlow.fetchBalance(session) - val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag)) + val profile = FahipayAccountClient().fetchProfile(session) + val balance = FahipayAccountClient().fetchBalance(session) + val accounts = listOf(FahipayAccountClient().buildAccount(profile, balance, loginTag)) val app = application as BasedBankApp app.fahipaySessions[loginId] = session AccountCache.saveFahipay(this@HomeActivity, loginId, accounts) @@ -636,7 +639,7 @@ fun applyNavLabelVisibility() { } /** Filters accounts whose profileId the user has hidden in settings. */ - private fun List.filterVisibleAccounts(): List { + private fun List.filterVisibleAccounts(): List { val store = CredentialStore(this@HomeActivity) return filter { acc -> when (acc.bank) { @@ -669,11 +672,10 @@ fun applyNavLabelVisibility() { } private fun refreshBmlLimits(session: BmlSession) { - val bmlFlow = BmlLoginFlow() lifecycleScope.launch { try { val (userName, limits) = withContext(Dispatchers.IO) { - Pair(bmlFlow.fetchUserInfo(session)?.fullName ?: "", bmlFlow.fetchForeignLimits(session)) + Pair(BmlAccountClient().fetchUserInfo(session)?.fullName ?: "", BmlForeignLimitsClient().fetchForeignLimits(session)) } val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf() val idx = existing.indexOfFirst { it.userName == userName } @@ -693,7 +695,7 @@ fun applyNavLabelVisibility() { val allBmlContacts = withContext(Dispatchers.IO) { store.getBmlLoginIds().flatMap { loginId -> val session = app.anyBmlSessionFor(loginId) ?: return@flatMap emptyList() - val contacts = BmlLoginFlow().fetchContacts(session, loginId) + val contacts = BmlContactsClient().fetchContacts(session, loginId) if (contacts.isNotEmpty()) ContactsCache.saveBml(this@HomeActivity, loginId, contacts) contacts } @@ -736,13 +738,11 @@ fun applyNavLabelVisibility() { lifecycleScope.launch { try { val groups = withContext(Dispatchers.IO) { - val flow = FahipayLoginFlow() - flow.setSessionCookie(session.sessionCookie) - flow.fetchContacts(session) + FahipayContactsClient().fetchContacts(session) } if (groups.isEmpty()) return@launch val contacts = groups.flatMap { it.contacts } - val categories = groups.map { MibBeneficiaryCategory(it.categoryId, it.label, it.contacts.size) } + val categories = groups.map { BankContactCategory(it.categoryId, it.label, it.contacts.size) } ContactsCache.saveFahipay(this@HomeActivity, contacts, categories) val mibContacts = ContactsCache.loadContacts(this@HomeActivity) val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds() @@ -755,11 +755,11 @@ fun applyNavLabelVisibility() { } private fun mergeContacts( - mib: List, - bml: List - ): List { + mib: List, + bml: List + ): List { val seen = mutableSetOf() - val result = mutableListOf() + val result = mutableListOf() for (c in mib) if (seen.add(c.benefNo)) result.add(c) for (c in bml) if (seen.add(c.benefNo)) result.add(c) return result @@ -774,8 +774,8 @@ fun applyNavLabelVisibility() { val (allContacts, allCategories) = withContext(Dispatchers.IO) { val seenContacts = mutableSetOf() val seenCategories = mutableSetOf() - val contacts = mutableListOf() - val categories = mutableListOf() + val contacts = mutableListOf() + val categories = mutableListOf() for (profile in profiles) { try { flow.switchProfile(session, profile) @@ -803,7 +803,7 @@ fun applyNavLabelVisibility() { } } - fun refreshBalances(src: MibAccount) { + fun refreshBalances(src: BankAccount) { val app = application as BasedBankApp lifecycleScope.launch { val current = viewModel.accounts.value ?: emptyList() @@ -811,12 +811,10 @@ fun applyNavLabelVisibility() { val fresh = withContext(Dispatchers.IO) { val sess = app.fahipaySessionFor(src) ?: return@withContext null try { - val flow = FahipayLoginFlow() - flow.setSessionCookie(sess.sessionCookie) - val balance = flow.fetchBalance(sess) - val profile = flow.fetchProfile(sess) + val balance = FahipayAccountClient().fetchBalance(sess) + val profile = FahipayAccountClient().fetchProfile(sess) val loginTag = "fahipay_${profile.profileId}" - val accounts = listOf(flow.buildAccount(profile, balance, loginTag)) + val accounts = listOf(FahipayAccountClient().buildAccount(profile, balance, loginTag)) val loginId = src.loginTag.removePrefix("fahipay_") AccountCache.saveFahipay(this@HomeActivity, loginId, accounts) app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != src.loginTag } + accounts @@ -830,7 +828,7 @@ fun applyNavLabelVisibility() { val fresh = withContext(Dispatchers.IO) { val sess = app.bmlSessionFor(src) ?: return@withContext null try { - val accounts = BmlLoginFlow().fetchAccounts(sess, src.loginTag) + val accounts = BmlAccountClient().fetchAccounts(sess, src.loginTag) AccountCache.saveBml(this@HomeActivity, loginId, accounts) val otherBml = app.bmlAccounts.filter { it.loginTag != src.loginTag } app.bmlAccounts = otherBml + accounts diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt index 21aba8e..bee384f 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt @@ -3,16 +3,16 @@ package sh.sar.basedbank.ui.home import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import sh.sar.basedbank.api.bml.BmlForeignLimit -import sh.sar.basedbank.api.mib.MibAccount -import sh.sar.basedbank.api.mib.MibBeneficiary -import sh.sar.basedbank.api.mib.MibBeneficiaryCategory +import sh.sar.basedbank.api.models.BankAccount +import sh.sar.basedbank.api.models.BankContact +import sh.sar.basedbank.api.models.BankContactCategory import sh.sar.basedbank.api.mib.MibFinanceDeal class HomeViewModel : ViewModel() { - val accounts = MutableLiveData>(emptyList()) + val accounts = MutableLiveData>(emptyList()) val financing = MutableLiveData>(emptyList()) - val contacts = MutableLiveData>(emptyList()) - val contactCategories = MutableLiveData>(emptyList()) + val contacts = MutableLiveData>(emptyList()) + val contactCategories = MutableLiveData>(emptyList()) data class BmlLimitsData(val userName: String, val limits: List) val bmlLimits = MutableLiveData>(emptyList()) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt index b3fef88..a14c99d 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt @@ -14,7 +14,8 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp -import sh.sar.basedbank.api.bml.BmlLoginFlow +import sh.sar.basedbank.api.bml.BmlAccountClient +import sh.sar.basedbank.api.mib.MibProfileClient import sh.sar.basedbank.api.mib.MibLoginFlow import sh.sar.basedbank.databinding.FragmentOtpBinding import sh.sar.basedbank.databinding.ItemOtpCardBinding @@ -94,7 +95,7 @@ class OtpFragment : Fragment() { val session = app.mibSessions[loginId] ?: continue val flow = app.mibFlowFor(loginId) val profile = withContext(Dispatchers.IO) { - try { flow.fetchPersonalProfile(session) } catch (_: Exception) { null } + try { MibProfileClient().fetchPersonalProfile(session) } catch (_: Exception) { null } } if (profile != null) { store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile( @@ -114,7 +115,7 @@ class OtpFragment : Fragment() { if (store.loadBmlUserProfile(loginId)?.fullName.isNullOrBlank()) { val session = app.bmlSessions[loginId] ?: continue val info = withContext(Dispatchers.IO) { - try { BmlLoginFlow().fetchUserInfo(session) } catch (_: Exception) { null } + try { BmlAccountClient().fetchUserInfo(session) } catch (_: Exception) { null } } if (info != null) { store.saveBmlUserProfile(loginId, CredentialStore.BmlUserProfile( diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/PayMvQrFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/PayMvQrFragment.kt index b60723e..1a8e16c 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/PayMvQrFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/PayMvQrFragment.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import sh.sar.basedbank.R -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.databinding.FragmentPayMvQrBinding import sh.sar.basedbank.databinding.ItemAccountDropdownBinding import sh.sar.basedbank.util.PaymvQrParser @@ -46,7 +46,7 @@ class PayMvQrFragment : Fragment() { private val binding get() = _binding!! private val viewModel: HomeViewModel by activityViewModels() - private var selectedAccount: MibAccount? = null + private var selectedAccount: BankAccount? = null private var generatedBitmap: Bitmap? = null private var generateJob: Job? = null @@ -186,7 +186,7 @@ class PayMvQrFragment : Fragment() { private fun renderQrCard( ctx: Context, - account: MibAccount, + account: BankAccount, qrPayload: String, amountStr: String? ): Bitmap { @@ -378,10 +378,10 @@ class PayMvQrFragment : Fragment() { private inner class QrAccountAdapter( private val context: Context, - private val accounts: List + private val accounts: List ) : BaseAdapter(), Filterable { - fun getAccount(position: Int): MibAccount? = accounts.getOrNull(position) + fun getAccount(position: Int): BankAccount? = accounts.getOrNull(position) override fun getCount() = accounts.size override fun getItem(position: Int) = accounts.getOrNull(position) @@ -411,7 +411,7 @@ class PayMvQrFragment : Fragment() { FilterResults().apply { values = accounts; count = accounts.size } override fun publishResults(c: CharSequence?, r: FilterResults?) = notifyDataSetChanged() override fun convertResultToString(r: Any?) = - (r as? MibAccount)?.let { + (r as? BankAccount)?.let { val prefix = if (it.bank == "BML" && it.profileName.isNotBlank()) "${it.profileName} · " else "" "$prefix${it.accountBriefName}" } ?: "" diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransactionAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransactionAdapter.kt index 325cc93..d8e2ded 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransactionAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransactionAdapter.kt @@ -9,17 +9,17 @@ import android.view.ViewGroup import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder -import sh.sar.basedbank.api.mib.Transaction +import sh.sar.basedbank.api.models.BankTransaction import sh.sar.basedbank.databinding.ItemDateHeaderBinding import sh.sar.basedbank.databinding.ItemLoadingFooterBinding import sh.sar.basedbank.databinding.ItemTransactionBinding -/** Adapter for Transaction History — date-grouped, shows account name in secondary line. */ +/** Adapter for BankTransaction History — date-grouped, shows account name in secondary line. */ class TransactionAdapter : RecyclerView.Adapter() { private sealed class Item { data class DateHeader(val label: String) : Item() - data class Trx(val transaction: Transaction) : Item() + data class Trx(val transaction: BankTransaction) : Item() } private val displayItems = mutableListOf() @@ -67,7 +67,7 @@ class TransactionAdapter : RecyclerView.Adapter() { } /** Replace the full sorted transaction list and rebuild date groups. */ - fun setTransactions(transactions: List) { + fun setTransactions(transactions: List) { _showLoadingFooter = false displayItems.clear() var lastDateKey = "" @@ -114,7 +114,7 @@ class TransactionAdapter : RecyclerView.Adapter() { inner class TransactionVH(private val b: ItemTransactionBinding) : RecyclerView.ViewHolder(b.root) { - fun bind(trx: Transaction) { + fun bind(trx: BankTransaction) { val isCredit = trx.amount >= 0 val color = AccountHistoryAdapter.sourceColor(trx.source) val name = trx.counterpartyName ?: trx.description @@ -141,7 +141,7 @@ class TransactionAdapter : RecyclerView.Adapter() { } b.tvDescription.text = trx.description - // Show account name in secondary line for Transaction History + // Show account name in secondary line for BankTransaction History b.tvCounterparty.text = trx.accountDisplayName b.tvCounterparty.visibility = View.VISIBLE @@ -162,7 +162,7 @@ class TransactionAdapter : RecyclerView.Adapter() { b.root.setOnClickListener { showDetail(trx) } } - private fun showDetail(trx: Transaction) { + private fun showDetail(trx: BankTransaction) { val ctx = b.root.context val title = trx.counterpartyName?.takeIf { it.isNotBlank() } ?: trx.description val details = buildString { 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 02c4b90..c855808 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 @@ -36,12 +36,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R -import sh.sar.basedbank.api.bml.BmlLoginFlow +import sh.sar.basedbank.api.bml.BmlTransferClient +import sh.sar.basedbank.api.bml.BmlTransferResult +import sh.sar.basedbank.api.bml.BmlValidateClient import sh.sar.basedbank.api.dhiraagu.DhiraaguClient import sh.sar.basedbank.api.fahipay.OoredooClient -import sh.sar.basedbank.api.bml.BmlTransferResult -import sh.sar.basedbank.api.mib.MibAccount -import sh.sar.basedbank.api.mib.MibBeneficiary +import sh.sar.basedbank.api.models.BankAccount +import sh.sar.basedbank.api.models.BankContact import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.api.mib.MibIpsAccountInfo import sh.sar.basedbank.api.mib.MibLookupException @@ -65,11 +66,11 @@ class TransferFragment : Fragment() { private val binding get() = _binding!! private val viewModel: HomeViewModel by activityViewModels() - private var selectedAccount: MibAccount? = null + private var selectedAccount: BankAccount? = null private val session get() = selectedAccount ?.let { (requireActivity().application as BasedBankApp).mibSessionFor(it) } ?: (requireActivity().application as BasedBankApp).anyMibSession() - private fun bmlSessionFor(account: MibAccount?) = + private fun bmlSessionFor(account: BankAccount?) = account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) } ?: (requireActivity().application as BasedBankApp).anyBmlSession() @@ -105,7 +106,7 @@ class TransferFragment : Fragment() { private const val ARG_AMOUNT_PREFILL = "amount_prefill" private const val ARG_REMARKS_PREFILL = "remarks_prefill" - fun newInstanceFrom(account: MibAccount) = TransferFragment().apply { + fun newInstanceFrom(account: BankAccount) = TransferFragment().apply { arguments = Bundle().apply { putString(ARG_FROM_ACCOUNT, account.accountNumber) } } @@ -239,7 +240,7 @@ class TransferFragment : Fragment() { } } - private fun showFromCard(account: MibAccount) { + private fun showFromCard(account: BankAccount) { val colorHex = when (account.bank) { "BML" -> "#0066A1" "FAHIPAY" -> "#15BEA7" @@ -284,7 +285,7 @@ class TransferFragment : Fragment() { } } - private fun updateAmountPrefix(account: MibAccount) { + private fun updateAmountPrefix(account: BankAccount) { binding.tilAmount.prefixText = if (account.currencyName == "USD") "USD " else "MVR " } @@ -369,10 +370,9 @@ class TransferFragment : Fragment() { val info = withContext(Dispatchers.IO) { if (isBmlSource && bmlSess != null) { val inputType = AccountInputParser.detect(accountNumber) - val bmlFlow = BmlLoginFlow() val bmlResult = try { - if (inputType == AccountInputParser.InputType.MIB_ACCOUNT) bmlFlow.verifyMibAccount(bmlSess, accountNumber) - else bmlFlow.validateAccount(bmlSess, accountNumber) + if (inputType == AccountInputParser.InputType.MIB_ACCOUNT) BmlValidateClient().verifyMibAccount(bmlSess, accountNumber) + else BmlValidateClient().validateAccount(bmlSess, accountNumber) } catch (_: Exception) { null } if (bmlResult != null) { val bankId = when (bmlResult.trnType) { @@ -393,7 +393,7 @@ class TransferFragment : Fragment() { catch (e: MibLookupException) { errorMsg = e.message; null } catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null } } else { - val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null } + val bmlResult = try { BmlValidateClient().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null } if (bmlResult != null) { val bankId = when (bmlResult.trnType) { "IAT" -> "MALBMVMV" @@ -731,7 +731,7 @@ class TransferFragment : Fragment() { } private fun doMibTransfer( - src: MibAccount, + src: BankAccount, destAccount: String, destName: String, destDisplay: String, @@ -790,7 +790,7 @@ class TransferFragment : Fragment() { mibReferenceNo = result.trxId, mibTransactionDate = result.date ) - Triple(true, "Transaction ID: ${result.trxId}\n${result.date}", receipt) + Triple(true, "BankTransaction ID: ${result.trxId}\n${result.date}", receipt) } else { Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null) } @@ -800,7 +800,7 @@ class TransferFragment : Fragment() { } private fun doBmlTransfer( - src: MibAccount, + src: BankAccount, destAccount: String, destDisplay: String, amount: Double, @@ -809,8 +809,8 @@ class TransferFragment : Fragment() { isSrcCard: Boolean, isDestMib: Boolean, currency: String, - allAccounts: List, - allContacts: List + allAccounts: List, + allContacts: List ): Triple { val loginId = src.loginTag.removePrefix("bml_") val sess = bmlSessionFor(src) ?: return Triple(false, getString(R.string.transfer_session_unavailable), null) @@ -845,10 +845,9 @@ class TransferFragment : Fragment() { } val toBank = bank ?: if (isDestMib) "MIB" else "BML" - val bmlFlow = BmlLoginFlow() // Step 1: initiate val initiated = try { - bmlFlow.initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank) + BmlTransferClient().initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank) } catch (e: Exception) { return Triple(false, e.message ?: "Initiation failed", null) } if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null) @@ -858,7 +857,7 @@ class TransferFragment : Fragment() { ?.let { Totp.generate(it) } ?: otp return try { - val result = bmlFlow.confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank) + val result = BmlTransferClient().confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank) if (result.success) { val receipt = TransferReceiptData( bank = "BML", @@ -1000,12 +999,12 @@ class TransferFragment : Fragment() { _binding = null } - private fun MibAccount.toDisplayString() = "$accountBriefName · $accountNumber" + private fun BankAccount.toDisplayString() = "$accountBriefName · $accountNumber" - // items: String = section header, MibAccount = selectable row + // items: String = section header, BankAccount = selectable row private inner class AccountDropdownAdapter( private val context: Context, - accounts: List + accounts: List ) : BaseAdapter(), Filterable { private val items: List = buildList { @@ -1018,7 +1017,7 @@ class TransferFragment : Fragment() { } } - fun getAccount(position: Int): MibAccount? = (items.getOrNull(position) as? MibAccount) + fun getAccount(position: Int): BankAccount? = (items.getOrNull(position) as? BankAccount) ?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && !it.statusDesc.equals("Active", ignoreCase = true) } override fun getCount() = items.size @@ -1043,7 +1042,7 @@ class TransferFragment : Fragment() { b.tvHeader.text = item b.root } else { - val acc = item as MibAccount + val acc = item as BankAccount val b = if (convertView?.tag is ItemAccountDropdownBinding) { convertView.tag as ItemAccountDropdownBinding } else { diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt index 48feb4f..f4958e1 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt @@ -25,12 +25,12 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R -import sh.sar.basedbank.api.bml.BmlLoginFlow -import sh.sar.basedbank.api.fahipay.FahipayLoginFlow -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.bml.BmlHistoryClient +import sh.sar.basedbank.api.fahipay.FahipayHistoryClient +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.api.mib.MibHistoryClient -import sh.sar.basedbank.api.mib.Transaction +import sh.sar.basedbank.api.models.BankTransaction import sh.sar.basedbank.api.mib.TransactionCache import sh.sar.basedbank.databinding.FragmentTransferHistoryBinding import sh.sar.basedbank.util.ContactImageCache @@ -48,12 +48,12 @@ class TransferHistoryFragment : Fragment() { private lateinit var adapter: TransactionAdapter // All merged transactions sorted by date desc - private val allTransactions = mutableListOf() + private val allTransactions = mutableListOf() private var searchQuery = "" // Per-account pagination state private data class AccountState( - val account: MibAccount, + val account: BankAccount, var mibNextStart: Int = 1, var mibTotalCount: Int = -1, var bmlNextPage: Int = 1, @@ -158,7 +158,7 @@ class TransferHistoryFragment : Fragment() { lifecycleScope.launch { val newTransactions = withContext(Dispatchers.IO) { - val results = mutableListOf() + val results = mutableListOf() // BML accounts: fetch in parallel val bmlStates = activeStates.filter { it.account.bank == "BML" } @@ -172,7 +172,7 @@ class TransferHistoryFragment : Fragment() { cal.add(Calendar.MONTH, -state.cardMonthOffset) val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time) state.cardMonthOffset++ - BmlLoginFlow().fetchCardHistory( + BmlHistoryClient().fetchCardHistory( session = session, cardId = state.account.internalId, accountDisplayName = state.account.accountBriefName, @@ -182,7 +182,7 @@ class TransferHistoryFragment : Fragment() { } else -> { val session = app.bmlSessionFor(state.account) ?: return@async emptyList() - val (list, totalPages) = BmlLoginFlow().fetchAccountHistory( + val (list, totalPages) = BmlHistoryClient().fetchAccountHistory( session = session, accountId = state.account.internalId, accountDisplayName = state.account.accountBriefName, @@ -194,7 +194,7 @@ class TransferHistoryFragment : Fragment() { list } } - } catch (_: Exception) { emptyList() } + } catch (_: Exception) { emptyList() } } }.awaitAll().flatten()) @@ -203,9 +203,7 @@ class TransferHistoryFragment : Fragment() { for (state in fahipayStates) { val session = app.fahipaySessionFor(state.account) ?: continue try { - val flow = FahipayLoginFlow() - flow.setSessionCookie(session.sessionCookie) - val (list, total) = flow.fetchHistory( + val (list, total) = FahipayHistoryClient().fetchHistory( session = session, accountDisplayName = state.account.accountBriefName, accountNumber = state.account.accountNumber, diff --git a/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt index 20273c3..1260af5 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt @@ -18,14 +18,17 @@ import kotlinx.coroutines.withContext import sh.sar.basedbank.util.Totp import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R +import sh.sar.basedbank.api.bml.BmlAccountClient import sh.sar.basedbank.api.bml.BmlActivationResult import sh.sar.basedbank.api.bml.BmlLoginFlow import sh.sar.basedbank.api.bml.BmlOtpChannel import sh.sar.basedbank.api.bml.BmlProfile +import sh.sar.basedbank.api.fahipay.FahipayAccountClient import sh.sar.basedbank.api.fahipay.FahipayLoginFlow import sh.sar.basedbank.api.fahipay.FahipaySession -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibLoginFlow +import sh.sar.basedbank.api.mib.MibProfileClient import sh.sar.basedbank.util.AccountCache import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.databinding.FragmentCredentialsBinding @@ -56,7 +59,7 @@ class CredentialsFragment : Fragment() { // BML multi-profile state private var bmlFlow: BmlLoginFlow? = null private var bmlLoginId: String = "" - private var bmlAccumulatedAccounts = mutableListOf() + private var bmlAccumulatedAccounts = mutableListOf() private var bmlPendingBusinessProfiles = ArrayDeque>>() private var bmlCurrentBusinessProfile: BmlProfile? = null private var bmlSelectedChannel: String? = null @@ -202,7 +205,7 @@ class CredentialsFragment : Fragment() { store.saveMibCredentials(loginId, username, passwordHash, otpSeed) withContext(Dispatchers.IO) { flow.lastSession?.let { s -> - val profile = flow.fetchPersonalProfile(s) + val profile = MibProfileClient().fetchPersonalProfile(s) if (profile != null) store.saveMibUserProfile( loginId, CredentialStore.MibUserProfile( @@ -428,7 +431,7 @@ class CredentialsFragment : Fragment() { val anySession = app.anyBmlSessionFor(bmlLoginId) if (anySession != null) { withContext(Dispatchers.IO) { - val info = BmlLoginFlow().fetchUserInfo(anySession) + val info = BmlAccountClient().fetchUserInfo(anySession) if (info != null) { store.saveBmlUserProfile( bmlLoginId, @@ -582,13 +585,13 @@ class CredentialsFragment : Fragment() { store: CredentialStore ) { val (profile, balance) = withContext(Dispatchers.IO) { - val p = flow.fetchProfile(session) - val b = flow.fetchBalance(session) + val p = FahipayAccountClient().fetchProfile(session) + val b = FahipayAccountClient().fetchBalance(session) Pair(p, b) } val loginId = profile.profileId val loginTag = "fahipay_$loginId" - val account = flow.buildAccount(profile, balance, loginTag) + val account = FahipayAccountClient().buildAccount(profile, balance, loginTag) store.saveFahipayCredentials(loginId, idCard, password) store.saveFahipaySession(loginId, session.authId, session.sessionCookie) store.saveFahipayUserProfile( diff --git a/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt b/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt index 789eb24..31d11df 100644 --- a/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt +++ b/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt @@ -3,7 +3,7 @@ package sh.sar.basedbank.util import android.content.Context import org.json.JSONArray import org.json.JSONObject -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount object AccountCache { @@ -12,7 +12,7 @@ object AccountCache { private fun bmlKey(loginId: String) = "bml_accounts_$loginId" private fun fahipayKey(loginId: String) = "fahipay_accounts_$loginId" - fun save(context: Context, accounts: List) { + fun save(context: Context, accounts: List) { val arr = JSONArray() for (acc in accounts) { arr.put(JSONObject().apply { @@ -38,7 +38,7 @@ object AccountCache { .edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply() } - fun saveBml(context: Context, loginId: String, accounts: List) { + fun saveBml(context: Context, loginId: String, accounts: List) { val arr = JSONArray() for (acc in accounts) { arr.put(JSONObject().apply { @@ -61,14 +61,14 @@ object AccountCache { .edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply() } - fun loadBml(context: Context, loginId: String): List { + fun loadBml(context: Context, loginId: String): List { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString(bmlKey(loginId), null) ?: return emptyList() return try { val arr = JSONArray(CacheEncryption.decrypt(raw)) (0 until arr.length()).map { i -> val o = arr.getJSONObject(i) - MibAccount( + BankAccount( bank = "BML", profileName = o.optString("profileName"), profileType = o.optString("profileType"), @@ -89,10 +89,10 @@ object AccountCache { } catch (_: Exception) { emptyList() } } - fun loadBml(context: Context, loginIds: List): List = + fun loadBml(context: Context, loginIds: List): List = loginIds.flatMap { loadBml(context, it) } - fun saveFahipay(context: Context, loginId: String, accounts: List) { + fun saveFahipay(context: Context, loginId: String, accounts: List) { val arr = JSONArray() for (acc in accounts) { arr.put(JSONObject().apply { @@ -115,14 +115,14 @@ object AccountCache { .edit().putString(fahipayKey(loginId), CacheEncryption.encrypt(arr.toString())).apply() } - fun loadFahipay(context: Context, loginId: String): List { + fun loadFahipay(context: Context, loginId: String): List { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString(fahipayKey(loginId), null) ?: return emptyList() return try { val arr = JSONArray(CacheEncryption.decrypt(raw)) (0 until arr.length()).map { i -> val o = arr.getJSONObject(i) - MibAccount( + BankAccount( bank = "FAHIPAY", profileName = o.optString("profileName"), profileType = o.optString("profileType"), @@ -143,14 +143,14 @@ object AccountCache { } catch (_: Exception) { emptyList() } } - fun loadFahipay(context: Context, loginIds: List): List = + fun loadFahipay(context: Context, loginIds: List): List = loginIds.flatMap { loadFahipay(context, it) } fun clear(context: Context) { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply() } - fun load(context: Context): List { + fun load(context: Context): List { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString(KEY_MIB, null) ?: return emptyList() return try { @@ -158,7 +158,7 @@ object AccountCache { val arr = JSONArray(json) (0 until arr.length()).map { i -> val o = arr.getJSONObject(i) - MibAccount( + BankAccount( bank = o.optString("bank", "MIB"), profileName = o.optString("profileName"), profileType = o.optString("profileType"), diff --git a/app/src/main/java/sh/sar/basedbank/util/AccountHistoryParser.kt b/app/src/main/java/sh/sar/basedbank/util/AccountHistoryParser.kt index 6d7402a..f30e12d 100644 --- a/app/src/main/java/sh/sar/basedbank/util/AccountHistoryParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/AccountHistoryParser.kt @@ -1,13 +1,13 @@ package sh.sar.basedbank.util -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.util.bmlapi.BmlHistoryParser import sh.sar.basedbank.util.fahipayapi.FahipayHistoryParser import sh.sar.basedbank.util.mibapi.MibHistoryParser object AccountHistoryParser { - fun from(account: MibAccount): AccountHistoryDisplay? = when (account.bank) { + fun from(account: BankAccount): AccountHistoryDisplay? = when (account.bank) { "BML" -> BmlHistoryParser.displayData(account) "FAHIPAY" -> FahipayHistoryParser.displayData(account) "MIB" -> MibHistoryParser.displayData(account) diff --git a/app/src/main/java/sh/sar/basedbank/util/AccountListParser.kt b/app/src/main/java/sh/sar/basedbank/util/AccountListParser.kt index 13e1646..6283a51 100644 --- a/app/src/main/java/sh/sar/basedbank/util/AccountListParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/AccountListParser.kt @@ -1,13 +1,13 @@ package sh.sar.basedbank.util -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.util.bmlapi.BmlDashboardParser import sh.sar.basedbank.util.fahipayapi.FahipayAccountParser import sh.sar.basedbank.util.mibapi.MibAccountParser object AccountListParser { - fun from(account: MibAccount): AccountListDisplay? = when (account.bank) { + fun from(account: BankAccount): AccountListDisplay? = when (account.bank) { "BML" -> BmlDashboardParser.displayData(account) "FAHIPAY" -> FahipayAccountParser.displayData(account) "MIB" -> MibAccountParser.displayData(account) diff --git a/app/src/main/java/sh/sar/basedbank/util/ContactListParser.kt b/app/src/main/java/sh/sar/basedbank/util/ContactListParser.kt index 224b383..d550ee6 100644 --- a/app/src/main/java/sh/sar/basedbank/util/ContactListParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/ContactListParser.kt @@ -1,18 +1,18 @@ package sh.sar.basedbank.util -import sh.sar.basedbank.api.mib.MibBeneficiary +import sh.sar.basedbank.api.models.BankContact import sh.sar.basedbank.util.bmlapi.BmlContactParser import sh.sar.basedbank.util.fahipayapi.FahipayContactParser import sh.sar.basedbank.util.mibapi.MibContactParser object ContactListParser { - fun from(contact: MibBeneficiary): ContactDisplay? = when { + fun from(contact: BankContact): ContactDisplay? = when { contact.benefCategoryId == "BML" -> BmlContactParser.displayData(contact) contact.benefType == "FAHIPAY" -> FahipayContactParser.displayData(contact) contact.benefType in setOf("I", "L", "S") -> MibContactParser.displayData(contact) else -> null } - fun fromList(contacts: List): List = contacts.mapNotNull { from(it) } + fun fromList(contacts: List): List = contacts.mapNotNull { from(it) } } diff --git a/app/src/main/java/sh/sar/basedbank/util/ContactManager.kt b/app/src/main/java/sh/sar/basedbank/util/ContactManager.kt index 0392069..5cc3e1d 100644 --- a/app/src/main/java/sh/sar/basedbank/util/ContactManager.kt +++ b/app/src/main/java/sh/sar/basedbank/util/ContactManager.kt @@ -1,7 +1,7 @@ package sh.sar.basedbank.util import sh.sar.basedbank.BasedBankApp -import sh.sar.basedbank.api.bml.BmlLoginFlow +import sh.sar.basedbank.api.bml.BmlContactsClient import sh.sar.basedbank.api.mib.MibContactsClient /** @@ -21,7 +21,7 @@ object ContactManager { private fun deleteBml(contact: ContactDisplay, app: BasedBankApp): Boolean { val sess = app.bmlSessions[contact.profileId] ?: app.anyBmlSession() ?: return false val contactId = contact.id.removePrefix("bml_") - return try { BmlLoginFlow().deleteContact(sess, contactId) } catch (_: Exception) { false } + return try { BmlContactsClient().deleteContact(sess, contactId) } catch (_: Exception) { false } } private fun deleteMib(contact: ContactDisplay, app: BasedBankApp): Boolean { diff --git a/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt index 621bc54..148cd38 100644 --- a/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt +++ b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt @@ -3,8 +3,8 @@ package sh.sar.basedbank.util import android.content.Context import org.json.JSONArray import org.json.JSONObject -import sh.sar.basedbank.api.mib.MibBeneficiary -import sh.sar.basedbank.api.mib.MibBeneficiaryCategory +import sh.sar.basedbank.api.models.BankContact +import sh.sar.basedbank.api.models.BankContactCategory object ContactsCache { @@ -14,8 +14,8 @@ object ContactsCache { fun save( context: Context, - contacts: List, - categories: List + contacts: List, + categories: List ) { val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() @@ -55,7 +55,7 @@ object ContactsCache { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply() } - fun loadContacts(context: Context): List { + fun loadContacts(context: Context): List { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString(KEY_CONTACTS, null) ?: return emptyList() return try { @@ -63,7 +63,7 @@ object ContactsCache { val arr = JSONArray(json) (0 until arr.length()).map { i -> val o = arr.getJSONObject(i) - MibBeneficiary( + BankContact( benefNo = o.optString("benefNo"), benefName = o.optString("benefName"), benefNickName = o.optString("benefNickName"), @@ -86,7 +86,7 @@ object ContactsCache { private fun bmlKey(loginId: String) = "bml_contacts_$loginId" - fun saveBml(context: Context, loginId: String, contacts: List) { + fun saveBml(context: Context, loginId: String, contacts: List) { val arr = JSONArray() for (c in contacts) { arr.put(JSONObject().apply { @@ -108,14 +108,14 @@ object ContactsCache { .edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply() } - fun loadBml(context: Context, loginId: String): List { + fun loadBml(context: Context, loginId: String): List { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString(bmlKey(loginId), null) ?: return emptyList() return try { val arr = JSONArray(CacheEncryption.decrypt(raw)) (0 until arr.length()).map { i -> val o = arr.getJSONObject(i) - MibBeneficiary( + BankContact( benefNo = o.optString("benefNo"), benefName = o.optString("benefName"), benefNickName = o.optString("benefNickName"), @@ -134,10 +134,10 @@ object ContactsCache { } catch (_: Exception) { emptyList() } } - fun loadBml(context: Context, loginIds: List): List = + fun loadBml(context: Context, loginIds: List): List = loginIds.flatMap { loadBml(context, it) } - fun saveFahipay(context: Context, contacts: List, categories: List) { + fun saveFahipay(context: Context, contacts: List, categories: List) { val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() val arr = JSONArray() for (c in contacts) arr.put(JSONObject().apply { @@ -159,14 +159,14 @@ object ContactsCache { prefs.apply() } - fun loadFahipay(context: Context): List { + fun loadFahipay(context: Context): List { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString("fahipay_contacts", null) ?: return emptyList() return try { val arr = JSONArray(CacheEncryption.decrypt(raw)) (0 until arr.length()).map { i -> val o = arr.getJSONObject(i) - MibBeneficiary( + BankContact( benefNo = o.optString("benefNo"), benefName = "", benefNickName = o.optString("benefNickName"), @@ -185,19 +185,19 @@ object ContactsCache { } catch (_: Exception) { emptyList() } } - fun loadFahipayCategories(context: Context): List { + fun loadFahipayCategories(context: Context): List { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString("fahipay_categories", null) ?: return emptyList() return try { val arr = JSONArray(CacheEncryption.decrypt(raw)) (0 until arr.length()).map { i -> val o = arr.getJSONObject(i) - MibBeneficiaryCategory(o.optString("id"), o.optString("categoryName"), o.optInt("numBenef")) + BankContactCategory(o.optString("id"), o.optString("categoryName"), o.optInt("numBenef")) } } catch (_: Exception) { emptyList() } } - fun loadCategories(context: Context): List { + fun loadCategories(context: Context): List { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString(KEY_CATEGORIES, null) ?: return emptyList() return try { @@ -205,7 +205,7 @@ object ContactsCache { val arr = JSONArray(json) (0 until arr.length()).map { i -> val o = arr.getJSONObject(i) - MibBeneficiaryCategory( + BankContactCategory( id = o.optString("id"), categoryName = o.optString("categoryName"), numBenef = o.optInt("numBenef", 0) diff --git a/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt b/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt index 4a94748..61c5f29 100644 --- a/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt +++ b/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt @@ -4,11 +4,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp -import sh.sar.basedbank.api.bml.BmlLoginFlow -import sh.sar.basedbank.api.fahipay.FahipayLoginFlow -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.bml.BmlHistoryClient +import sh.sar.basedbank.api.fahipay.FahipayHistoryClient +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibHistoryClient -import sh.sar.basedbank.api.mib.Transaction +import sh.sar.basedbank.api.models.BankTransaction import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -18,7 +18,7 @@ import java.util.Locale * The fragment holds one instance per account and calls [hasMore] / [fetchNextPage] * without knowing which bank it is talking to. */ -class HistoryFetcher(private val account: MibAccount) { +class HistoryFetcher(private val account: BankAccount) { private val isMib get() = account.bank == "MIB" private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" @@ -46,18 +46,16 @@ class HistoryFetcher(private val account: MibAccount) { else -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages } - suspend fun fetchNextPage(app: BasedBankApp, pageSize: Int = 10): List = when { + suspend fun fetchNextPage(app: BasedBankApp, pageSize: Int = 10): List = when { isFahipay -> withContext(Dispatchers.IO) { fetchFahipay(app) } isMib -> app.mibMutex.withLock { withContext(Dispatchers.IO) { fetchMib(app, pageSize) } } isBmlCard -> withContext(Dispatchers.IO) { fetchBmlCard(app) } else -> withContext(Dispatchers.IO) { fetchBmlCasa(app) } } - private fun fetchFahipay(app: BasedBankApp): List { + private fun fetchFahipay(app: BasedBankApp): List { val session = app.fahipaySessionFor(account) ?: return emptyList() - val flow = FahipayLoginFlow() - flow.setSessionCookie(session.sessionCookie) - val (list, total) = flow.fetchHistory( + val (list, total) = FahipayHistoryClient().fetchHistory( session = session, accountDisplayName = account.accountBriefName, accountNumber = account.accountNumber, @@ -68,7 +66,7 @@ class HistoryFetcher(private val account: MibAccount) { return list } - private fun fetchMib(app: BasedBankApp, pageSize: Int): List { + private fun fetchMib(app: BasedBankApp, pageSize: Int): List { val loginId = account.loginTag.removePrefix("mib_") val session = app.mibSessions[loginId] ?: return emptyList() val profiles = app.mibProfilesMap[loginId] ?: emptyList() @@ -86,13 +84,13 @@ class HistoryFetcher(private val account: MibAccount) { return list } - private fun fetchBmlCard(app: BasedBankApp): List { + private fun fetchBmlCard(app: BasedBankApp): List { val session = app.bmlSessionFor(account) ?: return emptyList() val cal = Calendar.getInstance() cal.add(Calendar.MONTH, -cardMonthOffset) val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time) cardMonthOffset++ - return BmlLoginFlow().fetchCardHistory( + return BmlHistoryClient().fetchCardHistory( session = session, cardId = account.internalId, accountDisplayName = account.accountBriefName, @@ -101,9 +99,9 @@ class HistoryFetcher(private val account: MibAccount) { ) } - private fun fetchBmlCasa(app: BasedBankApp): List { + private fun fetchBmlCasa(app: BasedBankApp): List { val session = app.bmlSessionFor(account) ?: return emptyList() - val (list, totalPages) = BmlLoginFlow().fetchAccountHistory( + val (list, totalPages) = BmlHistoryClient().fetchAccountHistory( session = session, accountId = account.internalId, accountDisplayName = account.accountBriefName, diff --git a/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlContactParser.kt b/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlContactParser.kt index 99a7cb3..ef1734a 100644 --- a/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlContactParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlContactParser.kt @@ -1,12 +1,12 @@ package sh.sar.basedbank.util.bmlapi -import sh.sar.basedbank.api.mib.MibBeneficiary +import sh.sar.basedbank.api.models.BankContact import sh.sar.basedbank.util.ContactDisplay import sh.sar.basedbank.util.TransferNetwork object BmlContactParser { - fun displayData(contact: MibBeneficiary) = ContactDisplay( + fun displayData(contact: BankContact) = ContactDisplay( id = contact.benefNo, name = contact.benefNickName, realName = contact.benefName, diff --git a/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlDashboardParser.kt b/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlDashboardParser.kt index cf2739a..2f0d8b9 100644 --- a/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlDashboardParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlDashboardParser.kt @@ -1,7 +1,7 @@ package sh.sar.basedbank.util.bmlapi import sh.sar.basedbank.R -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.util.AccountListDisplay object BmlDashboardParser { @@ -10,7 +10,7 @@ object BmlDashboardParser { * Returns all display fields for an account/card row in the accounts list. * Handles both BML CASA accounts and BML prepaid/credit cards. */ - fun displayData(account: MibAccount): AccountListDisplay { + fun displayData(account: BankAccount): AccountListDisplay { val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" return if (isCard) { val isActive = account.statusDesc.equals("Active", ignoreCase = true) @@ -51,7 +51,7 @@ object BmlDashboardParser { } /** Balance shown in the accounts list — ledger (working) balance for BML CASA. */ - fun listBalance(account: MibAccount): String = + fun listBalance(account: BankAccount): String = "${account.currencyName} ${account.currentBalance}" fun cardBrandIcon(productName: String): Int = when { diff --git a/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlHistoryParser.kt b/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlHistoryParser.kt index 0ed8539..b601f77 100644 --- a/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlHistoryParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/bmlapi/BmlHistoryParser.kt @@ -1,11 +1,11 @@ package sh.sar.basedbank.util.bmlapi -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.util.AccountHistoryDisplay object BmlHistoryParser { - fun displayData(account: MibAccount): AccountHistoryDisplay { + fun displayData(account: BankAccount): AccountHistoryDisplay { val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0 return AccountHistoryDisplay( name = account.accountBriefName, diff --git a/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayAccountParser.kt b/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayAccountParser.kt index a47510d..c7b7365 100644 --- a/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayAccountParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayAccountParser.kt @@ -1,11 +1,11 @@ package sh.sar.basedbank.util.fahipayapi -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.util.AccountListDisplay object FahipayAccountParser { - fun displayData(account: MibAccount) = AccountListDisplay( + fun displayData(account: BankAccount) = AccountListDisplay( name = account.accountBriefName, number = account.accountNumber, typeLabel = account.accountTypeName, diff --git a/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayContactParser.kt b/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayContactParser.kt index 422d721..3078094 100644 --- a/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayContactParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayContactParser.kt @@ -1,12 +1,12 @@ package sh.sar.basedbank.util.fahipayapi -import sh.sar.basedbank.api.mib.MibBeneficiary +import sh.sar.basedbank.api.models.BankContact import sh.sar.basedbank.util.ContactDisplay import sh.sar.basedbank.util.TransferNetwork object FahipayContactParser { - fun displayData(contact: MibBeneficiary) = ContactDisplay( + fun displayData(contact: BankContact) = ContactDisplay( id = contact.benefNo, name = contact.benefNickName, realName = contact.benefName, diff --git a/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayHistoryParser.kt b/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayHistoryParser.kt index b21218f..8f6d431 100644 --- a/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayHistoryParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/fahipayapi/FahipayHistoryParser.kt @@ -1,11 +1,11 @@ package sh.sar.basedbank.util.fahipayapi -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.util.AccountHistoryDisplay object FahipayHistoryParser { - fun displayData(account: MibAccount) = AccountHistoryDisplay( + fun displayData(account: BankAccount) = AccountHistoryDisplay( name = account.accountBriefName, number = account.accountNumber, bankPill = "FP", diff --git a/app/src/main/java/sh/sar/basedbank/util/mibapi/MibAccountParser.kt b/app/src/main/java/sh/sar/basedbank/util/mibapi/MibAccountParser.kt index 7b04b45..717bcf5 100644 --- a/app/src/main/java/sh/sar/basedbank/util/mibapi/MibAccountParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/mibapi/MibAccountParser.kt @@ -1,11 +1,11 @@ package sh.sar.basedbank.util.mibapi -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.util.AccountListDisplay object MibAccountParser { - fun displayData(account: MibAccount) = AccountListDisplay( + fun displayData(account: BankAccount) = AccountListDisplay( name = account.accountBriefName, number = account.accountNumber, typeLabel = productLabel(account.accountTypeName), diff --git a/app/src/main/java/sh/sar/basedbank/util/mibapi/MibContactParser.kt b/app/src/main/java/sh/sar/basedbank/util/mibapi/MibContactParser.kt index 125776b..3f75c1f 100644 --- a/app/src/main/java/sh/sar/basedbank/util/mibapi/MibContactParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/mibapi/MibContactParser.kt @@ -1,12 +1,12 @@ package sh.sar.basedbank.util.mibapi -import sh.sar.basedbank.api.mib.MibBeneficiary +import sh.sar.basedbank.api.models.BankContact import sh.sar.basedbank.util.ContactDisplay import sh.sar.basedbank.util.TransferNetwork object MibContactParser { - fun displayData(contact: MibBeneficiary): ContactDisplay { + fun displayData(contact: BankContact): ContactDisplay { val network = when (contact.benefType) { "I" -> TransferNetwork.MIB "S" -> TransferNetwork.SWIFT diff --git a/app/src/main/java/sh/sar/basedbank/util/mibapi/MibHistoryParser.kt b/app/src/main/java/sh/sar/basedbank/util/mibapi/MibHistoryParser.kt index 59b675d..b7197c5 100644 --- a/app/src/main/java/sh/sar/basedbank/util/mibapi/MibHistoryParser.kt +++ b/app/src/main/java/sh/sar/basedbank/util/mibapi/MibHistoryParser.kt @@ -1,11 +1,11 @@ package sh.sar.basedbank.util.mibapi -import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.util.AccountHistoryDisplay object MibHistoryParser { - fun displayData(account: MibAccount) = AccountHistoryDisplay( + fun displayData(account: BankAccount) = AccountHistoryDisplay( name = account.accountBriefName, number = account.accountNumber, bankPill = null, // MIB has no bank pill