Merge pull request 'fix/accounts-list-balance-consistency' (#5) from ahusan/fksar:fix/accounts-list-balance-consistency into main

Reviewed-on: shihaam/thijooree#5
This commit is contained in:
2026-05-28 16:23:33 +05:00
committed by shihaam
11 changed files with 229 additions and 26 deletions
+4
View File
@@ -27,6 +27,10 @@ android {
} }
buildTypes { buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
release { release {
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false isMinifyEnabled = false
@@ -9,6 +9,7 @@ import org.json.JSONObject
import java.security.MessageDigest import java.security.MessageDigest
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.abs
import kotlin.random.Random import kotlin.random.Random
class SessionExpiredException : Exception("MIB session expired") class SessionExpiredException : Exception("MIB session expired")
@@ -168,7 +169,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
accountTypeName = a.optString("accountTypeName"), accountTypeName = a.optString("accountTypeName"),
availableBalance = a.optString("availableBalance"), availableBalance = a.optString("availableBalance"),
currentBalance = a.optString("currentBalance"), currentBalance = a.optString("currentBalance"),
blockedAmount = a.optString("blockedAmount"), blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
mvrBalance = a.optString("mvrBalance"), mvrBalance = a.optString("mvrBalance"),
statusDesc = a.optString("statusDesc"), statusDesc = a.optString("statusDesc"),
profileImageHash = profile.customerImage, profileImageHash = profile.customerImage,
@@ -188,6 +189,13 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
// ─── Helpers ───────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────
/** MIB returns blockedAmount as a signed decimal where negative = funds held.
* Normalize to a positive magnitude so downstream code can treat it uniformly. */
private fun absBlockedAmount(raw: String): String {
val v = raw.toDoubleOrNull() ?: return raw
return "%.2f".format(abs(v))
}
private fun initialKeyExchange( private fun initialKeyExchange(
appId: String, encKey: String, sfunc: String, key2: String? = null appId: String, encKey: String, sfunc: String, key2: String? = null
): Pair<MibSession, String> { ): Pair<MibSession, String> {
@@ -325,7 +333,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
accountTypeName = a.optString("accountTypeName"), accountTypeName = a.optString("accountTypeName"),
availableBalance = a.optString("availableBalance"), availableBalance = a.optString("availableBalance"),
currentBalance = a.optString("currentBalance"), currentBalance = a.optString("currentBalance"),
blockedAmount = a.optString("blockedAmount"), blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
mvrBalance = a.optString("mvrBalance"), mvrBalance = a.optString("mvrBalance"),
statusDesc = a.optString("statusDesc"), statusDesc = a.optString("statusDesc"),
profileImageHash = profile.customerImage, profileImageHash = profile.customerImage,
@@ -125,6 +125,14 @@ class AccountsAdapter(
binding.tvAccountNumber.text = display.number binding.tvAccountNumber.text = display.number
binding.tvAccountType.text = display.typeLabel binding.tvAccountType.text = display.typeLabel
binding.tvBalance.text = if (hideAmounts) maskAmount(display.balance) else display.balance binding.tvBalance.text = if (hideAmounts) maskAmount(display.balance) else display.balance
val blocked = display.blockedBalance
if (blocked != null) {
val shown = if (hideAmounts) maskAmount(blocked) else blocked
binding.tvBlocked.text = binding.root.context.getString(R.string.account_blocked_label, shown)
binding.tvBlocked.visibility = View.VISIBLE
} else {
binding.tvBlocked.visibility = View.GONE
}
binding.btnTransfer.setOnClickListener { onTransferClick?.invoke(account) } binding.btnTransfer.setOnClickListener { onTransferClick?.invoke(account) }
binding.root.setOnClickListener { onAccountClick(account) } binding.root.setOnClickListener { onAccountClick(account) }
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {
@@ -57,14 +57,24 @@ class DashboardFragment : Fragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) } viewModel.accounts.observe(viewLifecycleOwner) {
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances() } updateBalances(it)
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { updatePendingFinances() } updateAttentionRow()
}
viewModel.financing.observe(viewLifecycleOwner) {
updatePendingFinances()
updateAttentionRow()
}
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) {
updatePendingFinances()
updateAttentionRow()
}
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) } viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
viewModel.hideAmounts.observe(viewLifecycleOwner) { viewModel.hideAmounts.observe(viewLifecycleOwner) {
updateBalances(viewModel.accounts.value ?: emptyList()) updateBalances(viewModel.accounts.value ?: emptyList())
updatePendingFinances() updatePendingFinances()
updateForeignLimits(viewModel.bmlLimits.value ?: emptyList()) updateForeignLimits(viewModel.bmlLimits.value ?: emptyList())
updateAttentionRow()
} }
binding.swipeRefresh.setOnRefreshListener { binding.swipeRefresh.setOnRefreshListener {
@@ -76,6 +86,10 @@ class DashboardFragment : Fragment() {
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances) (activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
} }
binding.cardOverdue.setOnClickListener {
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
}
val cardAdapter = DashboardCardAdapter() val cardAdapter = DashboardCardAdapter()
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false) binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.rvCards.adapter = cardAdapter binding.rvCards.adapter = cardAdapter
@@ -244,6 +258,56 @@ class DashboardFragment : Fragment() {
} }
} }
private fun updateAttentionRow() {
val hide = viewModel.hideAmounts.value ?: false
val accounts = viewModel.accounts.value ?: emptyList()
// Blocked: sum across CASA-style accounts (exclude cards and loans) per currency.
val blockedByCurrency = accounts
.filter { it.profileType != "BML_CREDIT" && it.profileType != "BML_PREPAID" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN" }
.mapNotNull { acc ->
val v = acc.blockedAmount.replace(",", "").toDoubleOrNull() ?: 0.0
if (v > 0.0) acc.currencyName.uppercase() to v else null
}
.groupBy({ it.first }, { it.second })
.mapValues { (_, vs) -> vs.sum() }
val blockedTotal = blockedByCurrency.values.sum()
if (blockedTotal > 0.0) {
// Primary line: prefer MVR if present, otherwise the first currency.
val primaryCcy = if ("MVR" in blockedByCurrency) "MVR" else blockedByCurrency.keys.first()
val primaryAmt = blockedByCurrency.getValue(primaryCcy)
binding.tvBlockedTotal.text = if (hide) "$primaryCcy ••••••" else "$primaryCcy %,.2f".format(primaryAmt)
val secondary = blockedByCurrency.filterKeys { it != primaryCcy }
if (secondary.isNotEmpty()) {
binding.tvBlockedSecondary.text = secondary.entries.joinToString(" · ") { (ccy, amt) ->
if (hide) "$ccy ••••••" else "$ccy %,.2f".format(amt)
}
binding.tvBlockedSecondary.visibility = View.VISIBLE
} else {
binding.tvBlockedSecondary.visibility = View.GONE
}
binding.cardBlocked.visibility = View.VISIBLE
} else {
binding.cardBlocked.visibility = View.GONE
}
// Overdue: MIB finance deals + BML loan details (assumed MVR — matches existing Pending Finances).
val mibOverdue = (viewModel.financing.value ?: emptyList()).sumOf { it.overdueAmount }
val bmlOverdue = (viewModel.bmlLoanDetails.value ?: emptyMap()).values.sumOf { it.overdueAmount }
val overdueTotal = mibOverdue + bmlOverdue
if (overdueTotal > 0.0) {
binding.tvOverdueTotal.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(overdueTotal)
binding.cardOverdue.visibility = View.VISIBLE
} else {
binding.cardOverdue.visibility = View.GONE
}
binding.rowAttention.visibility =
if (blockedTotal > 0.0 || overdueTotal > 0.0) View.VISIBLE else View.GONE
}
private fun updatePendingFinances() { private fun updatePendingFinances() {
val hide = viewModel.hideAmounts.value ?: false val hide = viewModel.hideAmounts.value ?: false
val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount } val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount }
@@ -5,6 +5,7 @@ data class AccountListDisplay(
val number: String, val number: String,
val typeLabel: String, val typeLabel: String,
val balance: String, val balance: String,
val blockedBalance: String? = null, // null when zero or not applicable
val isCard: Boolean = false, val isCard: Boolean = false,
val cardBrandIcon: Int = 0, // drawable res, only meaningful if isCard val cardBrandIcon: Int = 0, // drawable res, only meaningful if isCard
val statusLabel: String? = null // null = active; shown as status pill if set val statusLabel: String? = null // null = active; shown as status pill if set
@@ -26,11 +26,13 @@ object BmlDashboardParser {
statusLabel = if (isActive) null else account.statusDesc statusLabel = if (isActive) null else account.statusDesc
) )
} else { } else {
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
AccountListDisplay( AccountListDisplay(
name = account.accountBriefName, name = account.accountBriefName,
number = account.accountNumber, number = account.accountNumber,
typeLabel = productLabel(account.accountTypeName), typeLabel = productLabel(account.accountTypeName),
balance = listBalance(account) balance = listBalance(account),
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
) )
} }
} }
@@ -52,9 +54,9 @@ object BmlDashboardParser {
} }
} }
/** Balance shown in the accounts list — ledger (working) balance for BML CASA. */ /** Balance shown in the accounts list — available balance, consistent with transfer/contact picker. */
fun listBalance(account: BankAccount): String = fun listBalance(account: BankAccount): String =
"${account.currencyName} ${account.currentBalance}" "${account.currencyName} ${account.availableBalance}"
fun cardBrandIcon(productName: String): Int = when { fun cardBrandIcon(productName: String): Int = when {
productName.contains("AMEX", ignoreCase = true) || productName.contains("AMEX", ignoreCase = true) ||
@@ -5,12 +5,16 @@ import sh.sar.basedbank.util.AccountListDisplay
object MibAccountParser { object MibAccountParser {
fun displayData(account: BankAccount) = AccountListDisplay( fun displayData(account: BankAccount): AccountListDisplay {
name = account.accountBriefName, val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
number = account.accountNumber, return AccountListDisplay(
typeLabel = productLabel(account.accountTypeName), name = account.accountBriefName,
balance = "${account.currencyName} ${account.availableBalance}" number = account.accountNumber,
) typeLabel = productLabel(account.accountTypeName),
balance = "${account.currencyName} ${account.availableBalance}",
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
)
}
/** /**
* Returns a display-ready product label for a MIB (Faisanet) account type name. * Returns a display-ready product label for a MIB (Faisanet) account type name.
@@ -5,13 +5,16 @@ import sh.sar.basedbank.util.AccountHistoryDisplay
object MibHistoryParser { object MibHistoryParser {
fun displayData(account: BankAccount) = AccountHistoryDisplay( fun displayData(account: BankAccount): AccountHistoryDisplay {
name = account.accountBriefName, val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
number = account.accountNumber, return AccountHistoryDisplay(
bankPill = null, // MIB has no bank pill name = account.accountBriefName,
typeLabel = MibAccountParser.productLabel(account.accountTypeName), number = account.accountNumber,
availableBalance = "${account.currencyName} ${account.availableBalance}", bankPill = null, // MIB has no bank pill
workingBalance = "${account.currencyName} ${account.currentBalance}", typeLabel = MibAccountParser.productLabel(account.accountTypeName),
blockedBalance = null availableBalance = "${account.currencyName} ${account.availableBalance}",
) workingBalance = "${account.currencyName} ${account.currentBalance}",
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
)
}
} }
@@ -176,6 +176,103 @@
</LinearLayout> </LinearLayout>
<!-- Attention row: Blocked + Overdue (hidden when nothing applies) -->
<LinearLayout
android:id="@+id/rowAttention"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="16dp"
android:visibility="gone">
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardBlocked"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="?attr/colorErrorContainer"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dashboard_blocked"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer" />
<TextView
android:id="@+id/tvBlockedTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnErrorContainer" />
<TextView
android:id="@+id/tvBlockedSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer"
android:alpha="0.75"
android:visibility="gone" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardOverdue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="?attr/colorErrorContainer"
android:clickable="true"
android:focusable="true"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dashboard_overdue"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer" />
<TextView
android:id="@+id/tvOverdueTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnErrorContainer" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<!-- Pending Finances card --> <!-- Pending Finances card -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/cardPendingFinances" android:id="@+id/cardPendingFinances"
+9
View File
@@ -72,6 +72,15 @@
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurface" /> android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvBlocked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorError"
android:visibility="gone" />
<ImageButton <ImageButton
android:id="@+id/btnTransfer" android:id="@+id/btnTransfer"
android:layout_width="40dp" android:layout_width="40dp"
+3
View File
@@ -208,6 +208,9 @@
<string name="accounts">Accounts</string> <string name="accounts">Accounts</string>
<string name="cards">Cards</string> <string name="cards">Cards</string>
<string name="available_balance">Available Balance</string> <string name="available_balance">Available Balance</string>
<string name="account_blocked_label">%1$s blocked</string>
<string name="dashboard_blocked">Blocked Funds</string>
<string name="dashboard_overdue">Overdue Financing</string>
<!-- Transfer --> <!-- Transfer -->
<string name="transfer_tab_quick">Quick Transfer</string> <string name="transfer_tab_quick">Quick Transfer</string>