From e82218e89726ab7b8e990f9b1d54c12aa8a4aa64 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Thu, 21 May 2026 22:58:58 +0500 Subject: [PATCH] added support for BML loans --- .../sar/basedbank/api/bml/BmlAccountClient.kt | 47 ++- .../sh/sar/basedbank/api/bml/BmlModels.kt | 12 + .../basedbank/ui/home/DashboardFragment.kt | 14 +- .../sar/basedbank/ui/home/FinancingAdapter.kt | 174 ++++++++-- .../basedbank/ui/home/FinancingFragment.kt | 39 ++- .../sh/sar/basedbank/ui/home/HomeActivity.kt | 34 ++ .../sh/sar/basedbank/ui/home/HomeViewModel.kt | 3 + .../sh/sar/basedbank/util/FinancingCache.kt | 48 +++ .../sh/sar/basedbank/util/HistoryFetcher.kt | 3 + .../util/bmlapi/BmlDashboardParser.kt | 3 +- app/src/main/res/layout/item_bml_loan.xml | 304 ++++++++++++++++++ app/src/main/res/values/strings.xml | 9 + 12 files changed, 644 insertions(+), 46 deletions(-) create mode 100644 app/src/main/res/layout/item_bml_loan.xml 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 index 1047472..1e1c3e0 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlAccountClient.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlAccountClient.kt @@ -49,6 +49,30 @@ class BmlAccountClient { } catch (_: Exception) { null } } + fun fetchLoanDetail(session: BmlSession, internalId: String): BmlLoanDetail? { + val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/account/$internalId")).execute() + val code = resp.code + val json = resp.body?.string() ?: return null + resp.close() + if (code == 401 || code == 419) throw AuthExpiredException() + return try { + val root = JSONObject(json) + if (!root.optBoolean("success")) return null + val p = root.optJSONObject("payload") ?: return null + BmlLoanDetail( + loanAmount = p.optDouble("loanAmount", 0.0), + outstandingAmt = p.optDouble("outstandingAmt", 0.0), + repayAmount = p.optDouble("repayAmount", 0.0), + intRate = p.optDouble("intRate", 0.0), + loanStatus = p.optString("loanStatus"), + startDate = p.optString("startDate"), + endDate = p.optString("endDate"), + noOfRepayOverdue = p.optInt("noOfRepayOverdue", 0), + overdueAmount = p.optDouble("overdueAmount", 0.0) + ) + } catch (_: Exception) { null } + } + private fun parseDashboard( json: String, loginTag: String, @@ -61,6 +85,7 @@ class BmlAccountClient { val casaAccounts = mutableListOf() val prepaidCards = mutableListOf() + val loanAccounts = mutableListOf() for (i in 0 until dashboard.length()) { val item = dashboard.getJSONObject(i) @@ -91,6 +116,26 @@ class BmlAccountClient { profileId = profileId, internalId = internalId )) + } else if (accountType == "Loan") { + val outstanding = Math.abs(item.optDouble("availableBalance", 0.0)) + loanAccounts.add(BankAccount( + bank = "BML", + profileName = profileName, + profileType = "BML_LOAN", + accountNumber = accountNumber, + accountBriefName = item.optString("alias"), + currencyName = currency, + accountTypeName = product, + availableBalance = "%.2f".format(outstanding), + currentBalance = "%.2f".format(outstanding), + blockedAmount = "0.00", + mvrBalance = "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 @@ -119,6 +164,6 @@ class BmlAccountClient { } } - return casaAccounts + prepaidCards + return casaAccounts + prepaidCards + loanAccounts } } 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 aabcaef..da25541 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 @@ -50,6 +50,18 @@ data class BmlTransferResult( val errorMessage: String = "" ) +data class BmlLoanDetail( + val loanAmount: Double, + val outstandingAmt: Double, // negative as returned by API + val repayAmount: Double, + val intRate: Double, + val loanStatus: String, + val startDate: String, // ISO8601 e.g. "2023-10-26T00:00:00+05:00" + val endDate: String, + val noOfRepayOverdue: Int, + val overdueAmount: Double +) + data class BmlForeignLimit( val type: String, val used: Double, 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 bf3b4e8..83c5844 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 @@ -14,6 +14,7 @@ import sh.sar.basedbank.R import sh.sar.basedbank.api.bml.BmlForeignLimit import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibFinanceDeal +import kotlin.math.abs import sh.sar.basedbank.databinding.FragmentDashboardBinding import sh.sar.basedbank.databinding.ItemForeignLimitBinding @@ -30,11 +31,12 @@ class DashboardFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) } - viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) } + viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances() } + viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { updatePendingFinances() } viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) } viewModel.hideAmounts.observe(viewLifecycleOwner) { updateBalances(viewModel.accounts.value ?: emptyList()) - updatePendingFinances(viewModel.financing.value ?: emptyList()) + updatePendingFinances() updateForeignLimits(viewModel.bmlLimits.value ?: emptyList()) } @@ -123,9 +125,13 @@ class DashboardFragment : Fragment() { } } - private fun updatePendingFinances(deals: List) { + private fun updatePendingFinances() { val hide = viewModel.hideAmounts.value ?: false - binding.tvPendingFinances.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(deals.sumOf { it.outstandingAmount }) + val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount } + val bmlLoanDetails = viewModel.bmlLoanDetails.value ?: emptyMap() + val bmlTotal = bmlLoanDetails.values.sumOf { abs(it.outstandingAmt) } + val total = mibTotal + bmlTotal + binding.tvPendingFinances.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(total) } override fun onDestroyView() { diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/FinancingAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/FinancingAdapter.kt index 629c15e..08d130c 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/FinancingAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/FinancingAdapter.kt @@ -5,56 +5,97 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import sh.sar.basedbank.R +import sh.sar.basedbank.api.bml.BmlLoanDetail +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibFinanceDeal import sh.sar.basedbank.api.mib.MibFinancingClient +import sh.sar.basedbank.databinding.ItemBmlLoanBinding import sh.sar.basedbank.databinding.ItemFinanceDealBinding import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale +import kotlin.math.abs +import kotlin.math.ceil -class FinancingAdapter(private var deals: List) : - RecyclerView.Adapter() { +class FinancingAdapter(mibDeals: List) : + RecyclerView.Adapter() { + private sealed class Item { + data class Mib(val deal: MibFinanceDeal) : Item() + data class Bml(val account: BankAccount, val detail: BmlLoanDetail?) : Item() + } + + private var items: List = mibDeals.map { Item.Mib(it) } private var hideAmounts: Boolean = false + private val expandedPositions = mutableSetOf() + private val amountFmt = NumberFormat.getNumberInstance(Locale.US).apply { + minimumFractionDigits = 2 + maximumFractionDigits = 2 + } + private val mibDateFmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) + private val isoDateFmt = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US) + private val outputDateFmt = SimpleDateFormat("d MMM yyyy", Locale.US) + fun setHideAmounts(hide: Boolean) { if (hideAmounts == hide) return hideAmounts = hide notifyDataSetChanged() } - private val expandedPositions = mutableSetOf() - private val amountFmt = NumberFormat.getNumberInstance(Locale.US).apply { - minimumFractionDigits = 2 - maximumFractionDigits = 2 - } - private val inputDateFmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) - private val outputDateFmt = SimpleDateFormat("d MMM yyyy", Locale.US) - - fun updateDeals(newDeals: List) { - deals = newDeals + fun update(mibDeals: List, bmlLoans: List>) { expandedPositions.clear() + items = mibDeals.map { Item.Mib(it) } + bmlLoans.map { (acc, detail) -> Item.Bml(acc, detail) } notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val binding = ItemFinanceDealBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return ViewHolder(binding) + // Legacy compatibility — used on initial empty construction + fun updateDeals(newDeals: List) { + expandedPositions.clear() + val bmlItems = items.filterIsInstance() + items = newDeals.map { Item.Mib(it) } + bmlItems + notifyDataSetChanged() } - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(deals[position], position in expandedPositions) - holder.binding.root.setOnClickListener { + fun updateBmlLoans(loans: List>) { + expandedPositions.clear() + val mibItems = items.filterIsInstance() + items = mibItems + loans.map { (acc, detail) -> Item.Bml(acc, detail) } + notifyDataSetChanged() + } + + override fun getItemViewType(position: Int) = when (items[position]) { + is Item.Mib -> TYPE_MIB + is Item.Bml -> TYPE_BML + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + TYPE_BML -> BmlViewHolder(ItemBmlLoanBinding.inflate(inflater, parent, false)) + else -> MibViewHolder(ItemFinanceDealBinding.inflate(inflater, parent, false)) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val expanded = position in expandedPositions + when (val item = items[position]) { + is Item.Mib -> (holder as MibViewHolder).bind(item.deal, expanded) + is Item.Bml -> (holder as BmlViewHolder).bind(item.account, item.detail, expanded) + } + holder.itemView.setOnClickListener { val pos = holder.bindingAdapterPosition if (pos in expandedPositions) expandedPositions.remove(pos) else expandedPositions.add(pos) notifyItemChanged(pos) } } - override fun getItemCount() = deals.size + override fun getItemCount() = items.size - inner class ViewHolder(val binding: ItemFinanceDealBinding) : + // ── MIB ViewHolder ──────────────────────────────────────────────────────── + + inner class MibViewHolder(val binding: ItemFinanceDealBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(deal: MibFinanceDeal, expanded: Boolean) { @@ -69,25 +110,22 @@ class FinancingAdapter(private var deals: List) : binding.tvPaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.paidAmount)}" binding.tvUnpaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.outstandingAmount)}" - // Progress bar val progress = if (deal.dealAmount > 0) ((deal.paidAmount / deal.dealAmount) * 100).toInt().coerceIn(0, 100) else 0 binding.progressBar.progress = if (hide) 0 else progress - // Completion estimate - binding.tvCompletion.text = completionText(deal, ctx) + binding.tvCompletion.text = mibCompletionText(deal, ctx) - // Expanded details val detailsVisible = if (expanded) View.VISIBLE else View.GONE binding.dividerDetails.visibility = detailsVisible binding.detailsGroup.visibility = detailsVisible if (expanded) { - binding.tvDealDate.text = formatDate(deal.dealDate) + binding.tvDealDate.text = formatMibDate(deal.dealDate) binding.tvInstallment.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.installmentAmount)}" binding.tvNumInstallments.text = deal.noOfInstallments.toString() - binding.tvLastPaidDate.text = formatDate(deal.lastPaidDate) + binding.tvLastPaidDate.text = formatMibDate(deal.lastPaidDate) binding.tvLastPayAmount.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.lastPayAmount)}" if (deal.overdueAmount > 0) { @@ -99,7 +137,7 @@ class FinancingAdapter(private var deals: List) : } } - private fun completionText(deal: MibFinanceDeal, ctx: android.content.Context): String { + private fun mibCompletionText(deal: MibFinanceDeal, ctx: android.content.Context): String { if (deal.outstandingAmount <= 0.0) return ctx.getString(R.string.financing_completion_done) val remaining = MibFinancingClient.remainingMonths(deal) if (remaining <= 0) return ctx.getString(R.string.financing_completion_done) @@ -109,12 +147,84 @@ class FinancingAdapter(private var deals: List) : return ctx.getString(R.string.financing_completion_fmt, month) } - private fun formatDate(raw: String): String { + private fun formatMibDate(raw: String): String { return try { - outputDateFmt.format(inputDateFmt.parse(raw)!!) - } catch (_: Exception) { - raw.take(10) - } + outputDateFmt.format(mibDateFmt.parse(raw)!!) + } catch (_: Exception) { raw.take(10) } } } + + // ── BML ViewHolder ──────────────────────────────────────────────────────── + + inner class BmlViewHolder(val binding: ItemBmlLoanBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(account: BankAccount, detail: BmlLoanDetail?, expanded: Boolean) { + val ctx = binding.root.context + val currency = account.currencyName + val hide = hideAmounts + + binding.tvLoanProduct.text = account.accountTypeName + .trim().lowercase().split(" ") + .joinToString(" ") { it.replaceFirstChar { c -> c.uppercaseChar() } } + binding.tvLoanAccount.text = account.accountNumber + binding.tvLoanStatus.text = detail?.loanStatus?.ifBlank { account.statusDesc } ?: account.statusDesc + + val loanAmt = detail?.loanAmount ?: 0.0 + val outstanding = if (detail != null) abs(detail.outstandingAmt) else account.availableBalance.toDoubleOrNull() ?: 0.0 + val paid = (loanAmt - outstanding).coerceAtLeast(0.0) + + binding.tvLoanTotal.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(loanAmt)}" + binding.tvLoanPaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(paid)}" + binding.tvLoanOutstanding.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(outstanding)}" + + val progress = if (loanAmt > 0) ((paid / loanAmt) * 100).toInt().coerceIn(0, 100) else 0 + binding.loanProgressBar.progress = if (hide) 0 else progress + + binding.tvLoanCompletion.text = bmlCompletionText(detail, ctx) + + val detailsVisible = if (expanded) View.VISIBLE else View.GONE + binding.loanDividerDetails.visibility = detailsVisible + binding.loanDetailsGroup.visibility = detailsVisible + + if (expanded && detail != null) { + binding.tvLoanRepayment.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(detail.repayAmount)}" + binding.tvLoanIntRate.text = ctx.getString(R.string.loan_rate_fmt, detail.intRate) + binding.tvLoanStartDate.text = formatIsoDate(detail.startDate) + binding.tvLoanEndDate.text = formatIsoDate(detail.endDate) + + if (detail.overdueAmount > 0) { + binding.loanRowOverdue.visibility = View.VISIBLE + binding.tvLoanOverdue.text = if (hide) "$currency ••••••" + else "$currency ${amountFmt.format(detail.overdueAmount)} (${detail.noOfRepayOverdue})" + } else { + binding.loanRowOverdue.visibility = View.GONE + } + } + } + + private fun bmlCompletionText(detail: BmlLoanDetail?, ctx: android.content.Context): String { + if (detail == null) return "" + val outstanding = abs(detail.outstandingAmt) + if (outstanding <= 0.0 || detail.repayAmount <= 0.0) + return ctx.getString(R.string.financing_completion_done) + val remaining = ceil(outstanding / detail.repayAmount).toInt() + if (remaining <= 0) return ctx.getString(R.string.financing_completion_done) + val cal = Calendar.getInstance() + cal.add(Calendar.MONTH, remaining) + val month = SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(cal.time) + return ctx.getString(R.string.financing_completion_fmt, month) + } + + private fun formatIsoDate(raw: String): String { + return try { + outputDateFmt.format(isoDateFmt.parse(raw)!!) + } catch (_: Exception) { raw.take(10) } + } + } + + companion object { + private const val TYPE_MIB = 0 + private const val TYPE_BML = 1 + } } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/FinancingFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/FinancingFragment.kt index 7ab358d..6883bb0 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/FinancingFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/FinancingFragment.kt @@ -10,6 +10,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import sh.sar.basedbank.R +import sh.sar.basedbank.api.bml.BmlLoanDetail +import sh.sar.basedbank.api.mib.MibFinanceDeal +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.databinding.FragmentFinancingBinding class FinancingFragment : Fragment() { @@ -20,6 +23,9 @@ class FinancingFragment : Fragment() { private lateinit var adapter: FinancingAdapter private var financingRefreshing = false + private var latestMibDeals: List = emptyList() + private var latestBmlLoanDetails: Map = emptyMap() + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentFinancingBinding.inflate(inflater, container, false) return binding.root @@ -44,19 +50,36 @@ class FinancingFragment : Fragment() { (activity as? HomeActivity)?.triggerRefreshFinancing() } + viewModel.accounts.observe(viewLifecycleOwner) { rebuildAdapter() } viewModel.financing.observe(viewLifecycleOwner) { deals -> - adapter.updateDeals(deals) - binding.recyclerView.visibility = if (deals.isEmpty()) View.GONE else View.VISIBLE - binding.emptyView.visibility = if (deals.isEmpty()) View.VISIBLE else View.GONE - binding.loadingView.visibility = View.GONE - if (financingRefreshing) { - financingRefreshing = false - binding.swipeRefresh.isRefreshing = false - } + latestMibDeals = deals + rebuildAdapter() + } + viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { details -> + latestBmlLoanDetails = details + rebuildAdapter() } viewModel.hideAmounts.observe(viewLifecycleOwner) { adapter.setHideAmounts(it) } } + private fun rebuildAdapter() { + val accounts = viewModel.accounts.value ?: emptyList() + val loanAccounts = accounts.filter { it.profileType == "BML_LOAN" } + val bmlLoans: List> = + loanAccounts.map { acc -> acc to latestBmlLoanDetails[acc.internalId] } + + adapter.update(latestMibDeals, bmlLoans) + + val isEmpty = latestMibDeals.isEmpty() && bmlLoans.isEmpty() + binding.recyclerView.visibility = if (isEmpty) View.GONE else View.VISIBLE + binding.emptyView.visibility = if (isEmpty) View.VISIBLE else View.GONE + binding.loadingView.visibility = View.GONE + if (financingRefreshing) { + financingRefreshing = false + binding.swipeRefresh.isRefreshing = false + } + } + override fun onResume() { super.onResume() requireActivity().title = getString(R.string.nav_finances) 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 4a9863d..e174323 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 @@ -40,6 +40,7 @@ import sh.sar.basedbank.api.bml.BmlAccountClient import sh.sar.basedbank.api.bml.BmlActivationResult import sh.sar.basedbank.api.bml.BmlContactsClient import sh.sar.basedbank.api.bml.BmlForeignLimitsClient +import sh.sar.basedbank.api.bml.BmlLoanDetail import sh.sar.basedbank.api.bml.BmlProfile import sh.sar.basedbank.api.bml.BmlSession import sh.sar.basedbank.api.fahipay.FahipayAccountClient @@ -170,6 +171,8 @@ class HomeActivity : AppCompatActivity() { val cachedFinancing = FinancingCache.load(this) if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing + val cachedBmlLoans = FinancingCache.loadBmlLoans(this) + if (cachedBmlLoans.isNotEmpty()) viewModel.bmlLoanDetails.value = cachedBmlLoans val cachedLimits = ForeignLimitsCache.load(this) if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits @@ -178,6 +181,7 @@ class HomeActivity : AppCompatActivity() { refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId)) } for ((_, session) in app.bmlSessions) refreshBmlLimits(session) + refreshBmlLoanDetails() } else { // Came from lock screen — show caches immediately, refresh everything in background val store = CredentialStore(this) @@ -188,6 +192,8 @@ class HomeActivity : AppCompatActivity() { if (merged.isNotEmpty()) viewModel.accounts.value = merged val cachedFinancing = FinancingCache.load(this) if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing + val cachedBmlLoans = FinancingCache.loadBmlLoans(this) + if (cachedBmlLoans.isNotEmpty()) viewModel.bmlLoanDetails.value = cachedBmlLoans val cachedLimits = ForeignLimitsCache.load(this) if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits @@ -334,6 +340,7 @@ fun applyNavLabelVisibility() { val profiles = app.mibProfilesMap[loginId] ?: emptyList() refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId)) } + refreshBmlLoanDetails() } fun setRefreshing(visible: Boolean) { @@ -661,6 +668,7 @@ fun applyNavLabelVisibility() { val profiles = app.mibProfilesMap[loginId] ?: emptyList() refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId)) } + refreshBmlLoanDetails() } } @@ -898,6 +906,32 @@ fun applyNavLabelVisibility() { } } + private fun refreshBmlLoanDetails() { + val app = application as BasedBankApp + val loanAccounts = app.bmlAccounts.filter { it.profileType == "BML_LOAN" } + if (loanAccounts.isEmpty()) return + lifecycleScope.launch { + try { + val details = withContext(Dispatchers.IO) { + val map = mutableMapOf() + for (acc in loanAccounts) { + val session = app.bmlSessionFor(acc) ?: continue + try { + val detail = BmlAccountClient().fetchLoanDetail(session, acc.internalId) + if (detail != null) map[acc.internalId] = detail + } catch (_: Exception) { /* keep existing */ } + } + map + } + if (details.isNotEmpty()) { + val merged = (viewModel.bmlLoanDetails.value ?: emptyMap()) + details + FinancingCache.saveBmlLoans(this@HomeActivity, merged) + viewModel.bmlLoanDetails.postValue(merged) + } + } catch (_: Exception) { /* keep cached data */ } + } + } + private fun refreshFinancing(loginId: String, session: MibSession, profiles: List) { if (profiles.isEmpty()) return val flow = (application as BasedBankApp).mibFlowFor(loginId) 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 bee384f..6ae96fd 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,6 +3,7 @@ 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.bml.BmlLoanDetail import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.models.BankContact import sh.sar.basedbank.api.models.BankContactCategory @@ -11,6 +12,8 @@ import sh.sar.basedbank.api.mib.MibFinanceDeal class HomeViewModel : ViewModel() { val accounts = MutableLiveData>(emptyList()) val financing = MutableLiveData>(emptyList()) + /** BML loan details keyed by account internalId. */ + val bmlLoanDetails = MutableLiveData>(emptyMap()) val contacts = MutableLiveData>(emptyList()) val contactCategories = MutableLiveData>(emptyList()) diff --git a/app/src/main/java/sh/sar/basedbank/util/FinancingCache.kt b/app/src/main/java/sh/sar/basedbank/util/FinancingCache.kt index 8d01ac5..93b540e 100644 --- a/app/src/main/java/sh/sar/basedbank/util/FinancingCache.kt +++ b/app/src/main/java/sh/sar/basedbank/util/FinancingCache.kt @@ -3,12 +3,14 @@ package sh.sar.basedbank.util import android.content.Context import org.json.JSONArray import org.json.JSONObject +import sh.sar.basedbank.api.bml.BmlLoanDetail import sh.sar.basedbank.api.mib.MibFinanceDeal object FinancingCache { private const val PREFS = "financing_cache" private const val KEY_MIB = "mib_financing" + private const val KEY_BML_LOANS = "bml_loans" fun save(context: Context, deals: List) { val arr = JSONArray() @@ -34,6 +36,52 @@ object FinancingCache { .edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply() } + fun saveBmlLoans(context: Context, loans: Map) { + val arr = JSONArray() + for ((internalId, d) in loans) { + arr.put(JSONObject().apply { + put("internalId", internalId) + put("loanAmount", d.loanAmount) + put("outstandingAmt", d.outstandingAmt) + put("repayAmount", d.repayAmount) + put("intRate", d.intRate) + put("loanStatus", d.loanStatus) + put("startDate", d.startDate) + put("endDate", d.endDate) + put("noOfRepayOverdue", d.noOfRepayOverdue) + put("overdueAmount", d.overdueAmount) + }) + } + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .edit().putString(KEY_BML_LOANS, CacheEncryption.encrypt(arr.toString())).apply() + } + + fun loadBmlLoans(context: Context): Map { + val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getString(KEY_BML_LOANS, null) ?: return emptyMap() + return try { + val json = CacheEncryption.decrypt(raw) + val arr = JSONArray(json) + buildMap { + for (i in 0 until arr.length()) { + val o = arr.getJSONObject(i) + val id = o.optString("internalId") + if (id.isNotBlank()) put(id, BmlLoanDetail( + loanAmount = o.optDouble("loanAmount", 0.0), + outstandingAmt = o.optDouble("outstandingAmt", 0.0), + repayAmount = o.optDouble("repayAmount", 0.0), + intRate = o.optDouble("intRate", 0.0), + loanStatus = o.optString("loanStatus"), + startDate = o.optString("startDate"), + endDate = o.optString("endDate"), + noOfRepayOverdue = o.optInt("noOfRepayOverdue", 0), + overdueAmount = o.optDouble("overdueAmount", 0.0) + )) + } + } + } catch (_: Exception) { emptyMap() } + } + fun clear(context: Context) { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply() } 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 61c5f29..e8676fa 100644 --- a/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt +++ b/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt @@ -22,6 +22,7 @@ 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" + private val isBmlLoan get() = account.profileType == "BML_LOAN" private val isFahipay get() = account.bank == "FAHIPAY" // MIB pagination @@ -40,6 +41,7 @@ class HistoryFetcher(private val account: BankAccount) { private var fahipayTotal = -1 fun hasMore(): Boolean = when { + isBmlLoan -> false isFahipay -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal isMib -> mibTotalCount < 0 || mibNextStart <= mibTotalCount isBmlCard -> cardMonthOffset < 3 @@ -47,6 +49,7 @@ class HistoryFetcher(private val account: BankAccount) { } suspend fun fetchNextPage(app: BasedBankApp, pageSize: Int = 10): List = when { + isBmlLoan -> emptyList() isFahipay -> withContext(Dispatchers.IO) { fetchFahipay(app) } isMib -> app.mibMutex.withLock { withContext(Dispatchers.IO) { fetchMib(app, pageSize) } } isBmlCard -> withContext(Dispatchers.IO) { fetchBmlCard(app) } 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 2f0d8b9..fbf0f4f 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 @@ -10,7 +10,8 @@ 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: BankAccount): AccountListDisplay { + fun displayData(account: BankAccount): AccountListDisplay? { + if (account.profileType == "BML_LOAN") return null // Loans shown on financing page only val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" return if (isCard) { val isActive = account.statusDesc.equals("Active", ignoreCase = true) diff --git a/app/src/main/res/layout/item_bml_loan.xml b/app/src/main/res/layout/item_bml_loan.xml new file mode 100644 index 0000000..5f40222 --- /dev/null +++ b/app/src/main/res/layout/item_bml_loan.xml @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b9a806..05ca83c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -261,4 +261,13 @@ Fully paid Deal #%s Completes %s + + + Outstanding + Monthly Repayment + Interest Rate + Start Date + End Date + Overdue Payments + %.2f%%