fix/accounts-list-balance-consistency #5
@@ -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,
|
||||||
|
|||||||
@@ -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,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"
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
android:textColor="?attr/colorOnSurfaceVariant"
|
android:textColor="?attr/colorError"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
|||||||
@@ -210,6 +210,8 @@
|
|||||||
<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="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>
|
||||||
|
|||||||
Reference in New Issue
Block a user