fix/accounts-list-balance-consistency #5
@@ -9,6 +9,7 @@ import org.json.JSONObject
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
class SessionExpiredException : Exception("MIB session expired")
|
||||
@@ -168,7 +169,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
accountTypeName = a.optString("accountTypeName"),
|
||||
availableBalance = a.optString("availableBalance"),
|
||||
currentBalance = a.optString("currentBalance"),
|
||||
blockedAmount = a.optString("blockedAmount"),
|
||||
blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
|
||||
mvrBalance = a.optString("mvrBalance"),
|
||||
statusDesc = a.optString("statusDesc"),
|
||||
profileImageHash = profile.customerImage,
|
||||
@@ -188,6 +189,13 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
|
||||
// ─── 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(
|
||||
appId: String, encKey: String, sfunc: String, key2: String? = null
|
||||
): Pair<MibSession, String> {
|
||||
@@ -325,7 +333,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
accountTypeName = a.optString("accountTypeName"),
|
||||
availableBalance = a.optString("availableBalance"),
|
||||
currentBalance = a.optString("currentBalance"),
|
||||
blockedAmount = a.optString("blockedAmount"),
|
||||
blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
|
||||
mvrBalance = a.optString("mvrBalance"),
|
||||
statusDesc = a.optString("statusDesc"),
|
||||
profileImageHash = profile.customerImage,
|
||||
|
||||
@@ -57,14 +57,24 @@ class DashboardFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
|
||||
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances() }
|
||||
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { updatePendingFinances() }
|
||||
viewModel.accounts.observe(viewLifecycleOwner) {
|
||||
updateBalances(it)
|
||||
updateAttentionRow()
|
||||
}
|
||||
viewModel.financing.observe(viewLifecycleOwner) {
|
||||
updatePendingFinances()
|
||||
updateAttentionRow()
|
||||
}
|
||||
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) {
|
||||
updatePendingFinances()
|
||||
updateAttentionRow()
|
||||
}
|
||||
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
||||
viewModel.hideAmounts.observe(viewLifecycleOwner) {
|
||||
updateBalances(viewModel.accounts.value ?: emptyList())
|
||||
updatePendingFinances()
|
||||
updateForeignLimits(viewModel.bmlLimits.value ?: emptyList())
|
||||
updateAttentionRow()
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
@@ -76,6 +86,10 @@ class DashboardFragment : Fragment() {
|
||||
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
|
||||
}
|
||||
|
||||
binding.cardOverdue.setOnClickListener {
|
||||
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
|
||||
}
|
||||
|
||||
val cardAdapter = DashboardCardAdapter()
|
||||
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||
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() {
|
||||
val hide = viewModel.hideAmounts.value ?: false
|
||||
val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount }
|
||||
|
||||
@@ -5,12 +5,16 @@ import sh.sar.basedbank.util.AccountListDisplay
|
||||
|
||||
object MibAccountParser {
|
||||
|
||||
fun displayData(account: BankAccount) = AccountListDisplay(
|
||||
name = account.accountBriefName,
|
||||
number = account.accountNumber,
|
||||
typeLabel = productLabel(account.accountTypeName),
|
||||
balance = "${account.currencyName} ${account.availableBalance}"
|
||||
)
|
||||
fun displayData(account: BankAccount): AccountListDisplay {
|
||||
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
|
||||
return AccountListDisplay(
|
||||
name = account.accountBriefName,
|
||||
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.
|
||||
|
||||
@@ -5,13 +5,16 @@ import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||
|
||||
object MibHistoryParser {
|
||||
|
||||
fun displayData(account: BankAccount) = AccountHistoryDisplay(
|
||||
name = account.accountBriefName,
|
||||
number = account.accountNumber,
|
||||
bankPill = null, // MIB has no bank pill
|
||||
typeLabel = MibAccountParser.productLabel(account.accountTypeName),
|
||||
availableBalance = "${account.currencyName} ${account.availableBalance}",
|
||||
workingBalance = "${account.currencyName} ${account.currentBalance}",
|
||||
blockedBalance = null
|
||||
)
|
||||
fun displayData(account: BankAccount): AccountHistoryDisplay {
|
||||
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
|
||||
return AccountHistoryDisplay(
|
||||
name = account.accountBriefName,
|
||||
number = account.accountNumber,
|
||||
bankPill = null, // MIB has no bank pill
|
||||
typeLabel = MibAccountParser.productLabel(account.accountTypeName),
|
||||
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>
|
||||
|
||||
<!-- 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 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardPendingFinances"
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:textColor="?attr/colorError"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageButton
|
||||
|
||||
@@ -210,6 +210,8 @@
|
||||
<string name="cards">Cards</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 -->
|
||||
<string name="transfer_tab_quick">Quick Transfer</string>
|
||||
|
||||
Reference in New Issue
Block a user