display outstanding and unbilled credit card values and also fix 00.00 values on credit card statement

This commit is contained in:
2026-06-13 22:55:54 +05:00
parent ae18a8c6c8
commit 52d2eb235b
6 changed files with 93 additions and 69 deletions
+2 -2
View File
@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-06-03T08:28:30.389803148Z"> <DropdownSelection timestamp="2026-06-13T17:53:06.478193524Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="Default" identifier="serial=10.0.1.245:5555;connection=d182cf37" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=a703e092" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>
@@ -9,6 +9,12 @@ import sh.sar.basedbank.api.models.BankTransaction
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
data class BmlCardHistoryResult(
val statement: List<BankTransaction>,
val outstanding: List<BankTransaction>,
val unbilled: List<BankTransaction>
)
class BmlHistoryClient { class BmlHistoryClient {
private val client = newBmlApiClient() private val client = newBmlApiClient()
@@ -70,7 +76,7 @@ class BmlHistoryClient {
accountDisplayName: String, accountDisplayName: String,
accountNumber: String, accountNumber: String,
month: String month: String
): List<BankTransaction> { ): BmlCardHistoryResult {
val body = """{"card":"$cardId","month":"$month"}""" val body = """{"card":"$cardId","month":"$month"}"""
.toRequestBody("application/json".toMediaType()) .toRequestBody("application/json".toMediaType())
val resp = client.newCall( val resp = client.newCall(
@@ -81,76 +87,56 @@ class BmlHistoryClient {
.build() .build()
).execute() ).execute()
val code = resp.code val code = resp.code
val json = resp.body?.string() ?: return emptyList() val json = resp.body?.string() ?: return BmlCardHistoryResult(emptyList(), emptyList(), emptyList())
resp.close() resp.close()
if (code == 401 || code == 419) throw AuthExpiredException() if (code == 401 || code == 419) throw AuthExpiredException()
if (code in 500..599) throw BankServerException("BML") if (code in 500..599) throw BankServerException("BML")
return try { return try {
val root = JSONObject(json) val root = JSONObject(json)
if (!root.optBoolean("success")) return emptyList() if (!root.optBoolean("success")) return BmlCardHistoryResult(emptyList(), emptyList(), emptyList())
val payload = root.optJSONObject("payload") ?: return emptyList() val payload = root.optJSONObject("payload")
val result = mutableListOf<BankTransaction>() ?: return BmlCardHistoryResult(emptyList(), emptyList(), emptyList())
val authDetails = payload.optJSONObject("outstanding") val outstanding = parseCardArray(
?.optJSONArray("CardOutStdAuthDetails") payload.optJSONObject("outstanding")?.optJSONArray("CardOutStdAuthDetails"),
if (authDetails != null) { idPrefix = "auth", accountNumber, accountDisplayName
for (i in 0 until authDetails.length()) { )
val item = authDetails.getJSONObject(i) val unbilled = parseCardArray(
result.add(BankTransaction( payload.optJSONObject("unbilled")?.optJSONArray("CardUnbillTxnDetails"),
id = "auth_${item.optString("TranApprCode")}_$i", idPrefix = "unbilled", accountNumber, accountDisplayName
date = item.optString("DateTime"), )
description = item.optString("TranDesc").trim(), val statement = parseCardArray(
amount = item.optDouble("BillingAmount", 0.0), payload.optJSONArray("cardstatement"),
currency = item.optString("BillingCcy", "MVR"), idPrefix = "stmt", accountNumber, accountDisplayName
counterpartyName = null, )
reference = item.optString("TranApprCode").takeIf { it.isNotBlank() },
accountNumber = accountNumber,
accountDisplayName = accountDisplayName,
source = "BML_CARD"
))
}
}
val unbilled = payload.optJSONObject("unbilled") BmlCardHistoryResult(statement, outstanding, unbilled)
?.optJSONArray("CardUnbillTxnDetails") } catch (_: Exception) { BmlCardHistoryResult(emptyList(), emptyList(), emptyList()) }
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") private fun parseCardArray(
if (statement != null) { arr: org.json.JSONArray?,
for (i in 0 until statement.length()) { idPrefix: String,
val item = statement.getJSONObject(i) accountNumber: String,
result.add(BankTransaction( accountDisplayName: String
id = "stmt_${item.optString("TranRef", i.toString())}", ): List<BankTransaction> {
date = item.optString("TransDate", item.optString("TranDate", "")), if (arr == null) return emptyList()
description = item.optString("TranDesc", item.optString("Description", "")).trim(), return (0 until arr.length()).map { i ->
amount = -item.optDouble("TranAmount", 0.0), val item = arr.getJSONObject(i)
currency = item.optString("TranCcy", "MVR"), val ref = item.optString("TranApprCode")
counterpartyName = null, BankTransaction(
reference = item.optString("TranRef").takeIf { it.isNotBlank() }, id = "${idPrefix}_${ref.ifBlank { i.toString() }}",
accountNumber = accountNumber, date = item.optString("DateTime"),
accountDisplayName = accountDisplayName, description = item.optString("TranDesc").trim(),
source = "BML_CARD" amount = item.optDouble("BillingAmount", 0.0),
)) currency = item.optString("BillingCcy", "MVR"),
} counterpartyName = null,
} reference = ref.takeIf { it.isNotBlank() },
result accountNumber = accountNumber,
} catch (_: Exception) { emptyList() } accountDisplayName = accountDisplayName,
source = "BML_CARD"
)
}
} }
fun fetchPendingHistory( fun fetchPendingHistory(
@@ -74,9 +74,18 @@ class AccountHistoryAdapter(
} }
fun setPendingTransactions(transactions: List<BankTransaction>) { fun setPendingTransactions(transactions: List<BankTransaction>) {
setLeadingSections(listOf("Pending" to transactions))
}
/**
* Sets one or more labeled sections that render above the main statement list
* (e.g. card "Outstanding" + "Unbilled"). Empty sections are skipped.
*/
fun setLeadingSections(sections: List<Pair<String, List<BankTransaction>>>) {
pendingItems.clear() pendingItems.clear()
if (transactions.isNotEmpty()) { for ((label, transactions) in sections) {
pendingItems.add(Item.DateHeader("Pending")) if (transactions.isEmpty()) continue
pendingItems.add(Item.DateHeader(label))
for (trx in transactions) pendingItems.add(Item.Trx(trx, showDate = true)) for (trx in transactions) pendingItems.add(Item.Trx(trx, showDate = true))
} }
notifyDataSetChanged() notifyDataSetChanged()
@@ -228,6 +228,13 @@ class AccountHistoryFragment : Fragment() {
} }
(activity as? HomeActivity)?.hideConnectivityBanner() (activity as? HomeActivity)?.hideConnectivityBanner()
fetcher.takeCardPendingSections()?.let { (outstanding, unbilled) ->
adapter.setLeadingSections(listOf(
"Outstanding" to outstanding,
"Unbilled" to unbilled
))
}
if (transactions.isNotEmpty()) { if (transactions.isNotEmpty()) {
val existingIds = allTransactions.map { it.id }.toHashSet() val existingIds = allTransactions.map { it.id }.toHashSet()
val newOnes = transactions.filter { it.id !in existingIds } val newOnes = transactions.filter { it.id !in existingIds }
@@ -209,13 +209,14 @@ class TransferHistoryFragment : Fragment() {
cal.add(Calendar.MONTH, -state.cardMonthOffset) cal.add(Calendar.MONTH, -state.cardMonthOffset)
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time) val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
state.cardMonthOffset++ state.cardMonthOffset++
BmlHistoryClient().fetchCardHistory( val cardResult = BmlHistoryClient().fetchCardHistory(
session = session, session = session,
cardId = state.account.internalId, cardId = state.account.internalId,
accountDisplayName = state.account.accountBriefName, accountDisplayName = state.account.accountBriefName,
accountNumber = state.account.accountNumber, accountNumber = state.account.accountNumber,
month = month month = month
) )
cardResult.statement + cardResult.outstanding + cardResult.unbilled
} }
else -> { else -> {
val session = app.bmlSessionFor(state.account) ?: return@async emptyList() val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
@@ -35,6 +35,21 @@ class HistoryFetcher(private val account: BankAccount) {
// BML card pagination (month-based) // BML card pagination (month-based)
private var cardMonthOffset = 0 private var cardMonthOffset = 0
private var pendingCardOutstanding: List<BankTransaction>? = null
private var pendingCardUnbilled: List<BankTransaction>? = null
/**
* Returns and clears the card outstanding + unbilled lists captured on the first card
* fetch. Each list is only ever returned once.
*/
fun takeCardPendingSections(): Pair<List<BankTransaction>, List<BankTransaction>>? {
val o = pendingCardOutstanding
val u = pendingCardUnbilled
if (o == null && u == null) return null
pendingCardOutstanding = null
pendingCardUnbilled = null
return Pair(o ?: emptyList(), u ?: emptyList())
}
// Fahipay pagination // Fahipay pagination
private var fahipayNextStart = 0 private var fahipayNextStart = 0
@@ -90,16 +105,22 @@ class HistoryFetcher(private val account: BankAccount) {
private fun fetchBmlCard(app: BasedBankApp): List<BankTransaction> { private fun fetchBmlCard(app: BasedBankApp): List<BankTransaction> {
val session = app.bmlSessionFor(account) ?: return emptyList() val session = app.bmlSessionFor(account) ?: return emptyList()
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
val isFirstFetch = cardMonthOffset == 0
cal.add(Calendar.MONTH, -cardMonthOffset) cal.add(Calendar.MONTH, -cardMonthOffset)
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time) val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
cardMonthOffset++ cardMonthOffset++
return BmlHistoryClient().fetchCardHistory( val result = BmlHistoryClient().fetchCardHistory(
session = session, session = session,
cardId = account.internalId, cardId = account.internalId,
accountDisplayName = account.accountBriefName, accountDisplayName = account.accountBriefName,
accountNumber = account.accountNumber, accountNumber = account.accountNumber,
month = month month = month
) )
if (isFirstFetch) {
pendingCardOutstanding = result.outstanding
pendingCardUnbilled = result.unbilled
}
return result.statement
} }
private fun fetchBmlCasa(app: BasedBankApp): List<BankTransaction> { private fun fetchBmlCasa(app: BasedBankApp): List<BankTransaction> {