huge refactor.. might need to revert later
Auto Tag on Version Change / check-version (push) Successful in 4s
Auto Tag on Version Change / check-version (push) Successful in 4s
This commit is contained in:
@@ -8,7 +8,7 @@ import sh.sar.basedbank.api.bml.BmlLoginFlow
|
|||||||
import sh.sar.basedbank.api.bml.BmlProfile
|
import sh.sar.basedbank.api.bml.BmlProfile
|
||||||
import sh.sar.basedbank.api.bml.BmlSession
|
import sh.sar.basedbank.api.bml.BmlSession
|
||||||
import sh.sar.basedbank.api.fahipay.FahipaySession
|
import sh.sar.basedbank.api.fahipay.FahipaySession
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||||
import sh.sar.basedbank.api.mib.MibProfile
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
import sh.sar.basedbank.api.mib.MibSession
|
import sh.sar.basedbank.api.mib.MibSession
|
||||||
@@ -17,14 +17,14 @@ import sh.sar.basedbank.util.CredentialStore
|
|||||||
class BasedBankApp : Application() {
|
class BasedBankApp : Application() {
|
||||||
|
|
||||||
// Held in memory after successful login; cleared on logout
|
// Held in memory after successful login; cleared on logout
|
||||||
var accounts: List<MibAccount> = emptyList()
|
var accounts: List<BankAccount> = emptyList()
|
||||||
var fullName: String = ""
|
var fullName: String = ""
|
||||||
|
|
||||||
/** Active MIB sessions keyed by loginId (= MIB username). */
|
/** Active MIB sessions keyed by loginId (= MIB username). */
|
||||||
val mibSessions: MutableMap<String, MibSession> = mutableMapOf()
|
val mibSessions: MutableMap<String, MibSession> = mutableMapOf()
|
||||||
val mibProfilesMap: MutableMap<String, List<MibProfile>> = mutableMapOf()
|
val mibProfilesMap: MutableMap<String, List<MibProfile>> = mutableMapOf()
|
||||||
val mibLoginFlows: MutableMap<String, MibLoginFlow> = mutableMapOf()
|
val mibLoginFlows: MutableMap<String, MibLoginFlow> = mutableMapOf()
|
||||||
var mibAccounts: List<MibAccount> = emptyList()
|
var mibAccounts: List<BankAccount> = emptyList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Active BML sessions keyed by profileId (a globally unique GUID per BML profile).
|
* Active BML sessions keyed by profileId (a globally unique GUID per BML profile).
|
||||||
@@ -35,16 +35,16 @@ class BasedBankApp : Application() {
|
|||||||
val bmlProfilesMap: MutableMap<String, List<BmlProfile>> = mutableMapOf()
|
val bmlProfilesMap: MutableMap<String, List<BmlProfile>> = mutableMapOf()
|
||||||
/** BML login flows per loginId — hold the web session (cookies) needed for profile activation. */
|
/** BML login flows per loginId — hold the web session (cookies) needed for profile activation. */
|
||||||
val bmlLoginFlows: MutableMap<String, BmlLoginFlow> = mutableMapOf()
|
val bmlLoginFlows: MutableMap<String, BmlLoginFlow> = mutableMapOf()
|
||||||
var bmlAccounts: List<MibAccount> = emptyList()
|
var bmlAccounts: List<BankAccount> = emptyList()
|
||||||
|
|
||||||
/** Active Fahipay sessions keyed by loginId (= profileId). */
|
/** Active Fahipay sessions keyed by loginId (= profileId). */
|
||||||
val fahipaySessions: MutableMap<String, FahipaySession> = mutableMapOf()
|
val fahipaySessions: MutableMap<String, FahipaySession> = mutableMapOf()
|
||||||
var fahipayAccounts: List<MibAccount> = emptyList()
|
var fahipayAccounts: List<BankAccount> = emptyList()
|
||||||
|
|
||||||
// ─── MIB helpers ──────────────────────────────────────────────────────────
|
// ─── MIB helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Returns the MIB session for the given account (matched via loginTag). */
|
/** Returns the MIB session for the given account (matched via loginTag). */
|
||||||
fun mibSessionFor(account: MibAccount): MibSession? =
|
fun mibSessionFor(account: BankAccount): MibSession? =
|
||||||
mibSessions[account.loginTag.removePrefix("mib_")]
|
mibSessions[account.loginTag.removePrefix("mib_")]
|
||||||
|
|
||||||
/** Returns any available MIB session. */
|
/** Returns any available MIB session. */
|
||||||
@@ -73,7 +73,7 @@ class BasedBankApp : Application() {
|
|||||||
* Returns the BML session for the given account.
|
* Returns the BML session for the given account.
|
||||||
* Looks up by profileId first (multi-profile), falls back to loginId (legacy single-profile).
|
* Looks up by profileId first (multi-profile), falls back to loginId (legacy single-profile).
|
||||||
*/
|
*/
|
||||||
fun bmlSessionFor(account: MibAccount): BmlSession? {
|
fun bmlSessionFor(account: BankAccount): BmlSession? {
|
||||||
val byProfile = if (account.profileId.isNotBlank()) bmlSessions[account.profileId] else null
|
val byProfile = if (account.profileId.isNotBlank()) bmlSessions[account.profileId] else null
|
||||||
return byProfile ?: bmlSessions[account.loginTag.removePrefix("bml_")]
|
return byProfile ?: bmlSessions[account.loginTag.removePrefix("bml_")]
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ class BasedBankApp : Application() {
|
|||||||
// ─── Fahipay helpers ──────────────────────────────────────────────────────
|
// ─── Fahipay helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Returns the Fahipay session for the given account (matched via loginTag = "fahipay_${profileId}"). */
|
/** Returns the Fahipay session for the given account (matched via loginTag = "fahipay_${profileId}"). */
|
||||||
fun fahipaySessionFor(account: MibAccount): FahipaySession? =
|
fun fahipaySessionFor(account: BankAccount): FahipaySession? =
|
||||||
fahipaySessions[account.loginTag.removePrefix("fahipay_")]
|
fahipaySessions[account.loginTag.removePrefix("fahipay_")]
|
||||||
|
|
||||||
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
|
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
|
||||||
|
data class BmlUserInfo(
|
||||||
|
val fullName: String,
|
||||||
|
val email: String,
|
||||||
|
val mobile: String,
|
||||||
|
val customerId: String,
|
||||||
|
val idCard: String,
|
||||||
|
val birthdate: String
|
||||||
|
)
|
||||||
|
|
||||||
|
class BmlAccountClient {
|
||||||
|
|
||||||
|
private val client = newBmlApiClient()
|
||||||
|
|
||||||
|
fun fetchAccounts(
|
||||||
|
session: BmlSession,
|
||||||
|
loginTag: String,
|
||||||
|
profileName: String = "Personal",
|
||||||
|
profileId: String = ""
|
||||||
|
): List<BankAccount> {
|
||||||
|
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/dashboard")).execute()
|
||||||
|
val code = resp.code
|
||||||
|
val json = resp.body?.string()
|
||||||
|
resp.close()
|
||||||
|
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||||
|
return parseDashboard(json ?: return emptyList(), loginTag, profileName, profileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchUserInfo(session: BmlSession): BmlUserInfo? {
|
||||||
|
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/userinfo")).execute()
|
||||||
|
val json = resp.body?.string() ?: return null
|
||||||
|
resp.close()
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return null
|
||||||
|
val user = root.optJSONObject("payload")?.optJSONObject("user") ?: return null
|
||||||
|
BmlUserInfo(
|
||||||
|
fullName = user.optString("fullname").trim(),
|
||||||
|
email = user.optString("email").trim(),
|
||||||
|
mobile = user.optString("mobile_phone").trim(),
|
||||||
|
customerId = user.optString("customer_number").trim(),
|
||||||
|
idCard = user.optString("idcard").trim(),
|
||||||
|
birthdate = user.optString("birthdate").trim()
|
||||||
|
)
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDashboard(
|
||||||
|
json: String,
|
||||||
|
loginTag: String,
|
||||||
|
profileName: String,
|
||||||
|
profileId: String
|
||||||
|
): List<BankAccount> {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
|
val dashboard = root.optJSONObject("payload")?.optJSONArray("dashboard") ?: return emptyList()
|
||||||
|
|
||||||
|
val casaAccounts = mutableListOf<BankAccount>()
|
||||||
|
val prepaidCards = mutableListOf<BankAccount>()
|
||||||
|
|
||||||
|
for (i in 0 until dashboard.length()) {
|
||||||
|
val item = dashboard.getJSONObject(i)
|
||||||
|
val currency = item.optString("currency", "MVR")
|
||||||
|
val accountType = item.optString("account_type", "CASA")
|
||||||
|
val product = item.optString("product")
|
||||||
|
val accountNumber = item.optString("account")
|
||||||
|
val status = item.optString("account_status", "Active")
|
||||||
|
val internalId = item.optString("id", "")
|
||||||
|
|
||||||
|
if (accountType == "CASA") {
|
||||||
|
val available = item.optDouble("availableBalance", 0.0)
|
||||||
|
casaAccounts.add(BankAccount(
|
||||||
|
bank = "BML",
|
||||||
|
profileName = profileName,
|
||||||
|
profileType = "BML",
|
||||||
|
accountNumber = accountNumber,
|
||||||
|
accountBriefName = item.optString("alias"),
|
||||||
|
currencyName = currency,
|
||||||
|
accountTypeName = product,
|
||||||
|
availableBalance = "%.2f".format(available),
|
||||||
|
currentBalance = "%.2f".format(item.optDouble("ledgerBalance", 0.0)),
|
||||||
|
blockedAmount = "%.2f".format(item.optDouble("lockedAmount", 0.0)),
|
||||||
|
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "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
|
||||||
|
val isPrepaid = item.optBoolean("prepaid_card", false)
|
||||||
|
val cardBalance = item.optJSONObject("cardBalance")
|
||||||
|
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
||||||
|
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
|
||||||
|
prepaidCards.add(BankAccount(
|
||||||
|
bank = "BML",
|
||||||
|
profileName = profileName,
|
||||||
|
profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT",
|
||||||
|
accountNumber = accountNumber,
|
||||||
|
accountBriefName = item.optString("alias").ifBlank { product },
|
||||||
|
currencyName = currency,
|
||||||
|
accountTypeName = product,
|
||||||
|
availableBalance = "%.2f".format(available),
|
||||||
|
currentBalance = "%.2f".format(current),
|
||||||
|
blockedAmount = "0.00",
|
||||||
|
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
||||||
|
statusDesc = status,
|
||||||
|
profileImageHash = null,
|
||||||
|
loginTag = loginTag,
|
||||||
|
profileId = profileId,
|
||||||
|
internalId = internalId
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return casaAccounts + prepaidCards
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
internal const val BML_BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
|
||||||
|
internal const val BML_USER_AGENT = "bml-mobile-banking/348 (POCO; Android 14; 22101320I)"
|
||||||
|
internal const val BML_APP_VERSION = "2.1.44.348"
|
||||||
|
|
||||||
|
internal fun newBmlApiClient(): OkHttpClient = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
internal fun bmlApiRequest(session: BmlSession, url: String): Request =
|
||||||
|
Request.Builder().url(url)
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.build()
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
|
|
||||||
|
class BmlContactsClient {
|
||||||
|
|
||||||
|
private val client = newBmlApiClient()
|
||||||
|
|
||||||
|
fun fetchContacts(session: BmlSession, loginId: String): List<BankContact> {
|
||||||
|
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/contacts")).execute()
|
||||||
|
val json = resp.body?.string() ?: return emptyList()
|
||||||
|
resp.close()
|
||||||
|
return parseContacts(json, loginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveContact(
|
||||||
|
session: BmlSession,
|
||||||
|
contactType: String,
|
||||||
|
account: String,
|
||||||
|
alias: String,
|
||||||
|
currency: String? = null,
|
||||||
|
name: String? = null,
|
||||||
|
swift: String? = null
|
||||||
|
): Boolean {
|
||||||
|
val bodyObj = JSONObject().apply {
|
||||||
|
put("contact_type", contactType)
|
||||||
|
put("account", account)
|
||||||
|
put("alias", alias)
|
||||||
|
if (currency != null) put("currency", currency)
|
||||||
|
if (name != null) put("name", name)
|
||||||
|
if (swift != null) put("swift", swift)
|
||||||
|
}
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder().url("$BML_BASE_URL/api/mobile/contacts")
|
||||||
|
.post(bodyObj.toString().toRequestBody("application/json".toMediaType()))
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val json = resp.body?.string() ?: return false
|
||||||
|
resp.close()
|
||||||
|
return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteContact(session: BmlSession, contactId: String): Boolean {
|
||||||
|
val body = """{"_method":"delete"}""".toRequestBody("application/json".toMediaType())
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder().url("$BML_BASE_URL/api/mobile/contacts/$contactId")
|
||||||
|
.post(body)
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val bodyStr = resp.body?.string() ?: return false
|
||||||
|
resp.close()
|
||||||
|
return try { JSONObject(bodyStr).optBoolean("success") } catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseContacts(json: String, loginId: String): List<BankContact> {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
|
val payload: JSONArray = root.optJSONArray("payload") ?: return emptyList()
|
||||||
|
val result = mutableListOf<BankContact>()
|
||||||
|
for (i in 0 until payload.length()) {
|
||||||
|
val item = payload.getJSONObject(i)
|
||||||
|
val account = item.optString("account", "")
|
||||||
|
if (account.isBlank()) continue
|
||||||
|
result.add(BankContact(
|
||||||
|
benefNo = "bml_${item.optInt("id")}",
|
||||||
|
benefName = item.optString("name"),
|
||||||
|
benefNickName = item.optString("alias", item.optString("name")),
|
||||||
|
benefAccount = account,
|
||||||
|
benefType = "I",
|
||||||
|
bankColor = "#0066A1",
|
||||||
|
benefBankName = "Bank of Maldives",
|
||||||
|
bankCode = "",
|
||||||
|
benefStatus = item.optString("status", "S"),
|
||||||
|
transferCyDesc = item.optString("currency", "MVR"),
|
||||||
|
customerImgHash = null,
|
||||||
|
benefCategoryId = "BML",
|
||||||
|
profileId = loginId
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class BmlForeignLimitsClient {
|
||||||
|
|
||||||
|
// Foreign limits use a different host than the main BML API
|
||||||
|
private val BASE_URL = "https://app.bankofmaldives.com.mv/api/v2"
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun fetchForeignLimits(session: BmlSession): List<BmlForeignLimit> {
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder().url("$BASE_URL/foreign-limits")
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val code = resp.code
|
||||||
|
val json = resp.body?.string()
|
||||||
|
resp.close()
|
||||||
|
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||||
|
return parseForeignLimits(json ?: return emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseForeignLimits(json: String): List<BmlForeignLimit> {
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
|
val payload = root.optJSONArray("payload") ?: return emptyList()
|
||||||
|
(0 until payload.length()).map { i ->
|
||||||
|
val item = payload.getJSONObject(i)
|
||||||
|
val usage = item.optJSONObject("usageByCategory") ?: JSONObject()
|
||||||
|
val atm = usage.optJSONObject("ATM") ?: JSONObject()
|
||||||
|
val ecom = usage.optJSONObject("ECOM") ?: JSONObject()
|
||||||
|
val pos = usage.optJSONObject("POS") ?: JSONObject()
|
||||||
|
BmlForeignLimit(
|
||||||
|
type = item.optString("type", "Debit"),
|
||||||
|
used = item.optDouble("used", 0.0),
|
||||||
|
totalLimit = item.optDouble("totalLimit", 0.0),
|
||||||
|
generalCap = item.optDouble("generalCap", 0.0),
|
||||||
|
generalRemaining = item.optDouble("generalRemaining", 0.0),
|
||||||
|
medicalRemaining = item.optDouble("medicalRemaining", 0.0),
|
||||||
|
isAtmEnabled = item.optBoolean("isAtmEnabled", false),
|
||||||
|
isPosEnabled = item.optBoolean("isPosEnabled", false),
|
||||||
|
atmRemaining = atm.optDouble("remaining", 0.0),
|
||||||
|
atmLimit = atm.optDouble("limit", 0.0),
|
||||||
|
ecomRemaining = ecom.optDouble("remaining", 0.0),
|
||||||
|
ecomLimit = ecom.optDouble("limit", 0.0),
|
||||||
|
posRemaining = pos.optDouble("remaining", 0.0),
|
||||||
|
posLimit = pos.optDouble("limit", 0.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.models.BankTransaction
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class BmlHistoryClient {
|
||||||
|
|
||||||
|
private val client = newBmlApiClient()
|
||||||
|
|
||||||
|
fun fetchAccountHistory(
|
||||||
|
session: BmlSession,
|
||||||
|
accountId: String,
|
||||||
|
accountDisplayName: String,
|
||||||
|
accountNumber: String,
|
||||||
|
page: Int
|
||||||
|
): Pair<List<BankTransaction>, Int> {
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder().url("$BML_BASE_URL/api/mobile/account/$accountId/history/$page")
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val code = resp.code
|
||||||
|
val json = resp.body?.string() ?: return Pair(emptyList(), 0)
|
||||||
|
resp.close()
|
||||||
|
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return Pair(emptyList(), 0)
|
||||||
|
val payload = root.optJSONObject("payload") ?: return Pair(emptyList(), 0)
|
||||||
|
val totalPages = payload.optInt("totalPages", 0)
|
||||||
|
val history = payload.optJSONArray("history") ?: return Pair(emptyList(), totalPages)
|
||||||
|
val transactions = (0 until history.length()).map { i ->
|
||||||
|
val item = history.getJSONObject(i)
|
||||||
|
val desc = item.optString("description").trim()
|
||||||
|
val narrative1 = item.optString("narrative1")
|
||||||
|
val date = when (desc) {
|
||||||
|
"Purchase" -> parsePurchaseNarrative1(narrative1) ?: item.optString("bookingDate")
|
||||||
|
"Transfer Debit", "Transfer Credit" -> parseTransferNarrative1(narrative1) ?: item.optString("bookingDate")
|
||||||
|
else -> item.optString("bookingDate")
|
||||||
|
}
|
||||||
|
BankTransaction(
|
||||||
|
id = item.optString("id"),
|
||||||
|
date = date,
|
||||||
|
description = desc,
|
||||||
|
amount = item.optDouble("amount", 0.0),
|
||||||
|
currency = item.optString("currency"),
|
||||||
|
counterpartyName = item.optString("narrative2").takeIf { it.isNotBlank() },
|
||||||
|
reference = item.optString("reference").takeIf { it.isNotBlank() },
|
||||||
|
accountNumber = accountNumber,
|
||||||
|
accountDisplayName = accountDisplayName,
|
||||||
|
source = "BML"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Pair(transactions, totalPages)
|
||||||
|
} catch (_: Exception) { Pair(emptyList(), 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchCardHistory(
|
||||||
|
session: BmlSession,
|
||||||
|
cardId: String,
|
||||||
|
accountDisplayName: String,
|
||||||
|
accountNumber: String,
|
||||||
|
month: String
|
||||||
|
): List<BankTransaction> {
|
||||||
|
val body = """{"card":"$cardId","month":"$month"}"""
|
||||||
|
.toRequestBody("application/json".toMediaType())
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder().url("$BML_BASE_URL/api/mobile/card/statement").post(body)
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val code = resp.code
|
||||||
|
val json = resp.body?.string() ?: return emptyList()
|
||||||
|
resp.close()
|
||||||
|
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
|
val payload = root.optJSONObject("payload") ?: return emptyList()
|
||||||
|
val result = mutableListOf<BankTransaction>()
|
||||||
|
|
||||||
|
val authDetails = payload.optJSONObject("outstanding")
|
||||||
|
?.optJSONArray("CardOutStdAuthDetails")
|
||||||
|
if (authDetails != null) {
|
||||||
|
for (i in 0 until authDetails.length()) {
|
||||||
|
val item = authDetails.getJSONObject(i)
|
||||||
|
result.add(BankTransaction(
|
||||||
|
id = "auth_${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 unbilled = payload.optJSONObject("unbilled")
|
||||||
|
?.optJSONArray("CardUnbillTxnDetails")
|
||||||
|
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")
|
||||||
|
if (statement != null) {
|
||||||
|
for (i in 0 until statement.length()) {
|
||||||
|
val item = statement.getJSONObject(i)
|
||||||
|
result.add(BankTransaction(
|
||||||
|
id = "stmt_${item.optString("TranRef", i.toString())}",
|
||||||
|
date = item.optString("TransDate", item.optString("TranDate", "")),
|
||||||
|
description = item.optString("TranDesc", item.optString("Description", "")).trim(),
|
||||||
|
amount = -item.optDouble("TranAmount", 0.0),
|
||||||
|
currency = item.optString("TranCcy", "MVR"),
|
||||||
|
counterpartyName = null,
|
||||||
|
reference = item.optString("TranRef").takeIf { it.isNotBlank() },
|
||||||
|
accountNumber = accountNumber,
|
||||||
|
accountDisplayName = accountDisplayName,
|
||||||
|
source = "BML_CARD"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// "12-05-2026 041675" → first 4 digits of time part as HH:mm
|
||||||
|
private fun parsePurchaseNarrative1(narrative1: String): String? {
|
||||||
|
return try {
|
||||||
|
val parts = narrative1.split(" ")
|
||||||
|
if (parts.size < 2) null
|
||||||
|
else {
|
||||||
|
val timePart = parts[1].take(4)
|
||||||
|
val combined = "${parts[0]} ${timePart.take(2)}:${timePart.drop(2)}:00"
|
||||||
|
val date = SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.US).parse(combined)
|
||||||
|
date?.let { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(it) }
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
// "11-04-2026 13-17-08" → yyyy-MM-dd HH:mm:ss
|
||||||
|
private fun parseTransferNarrative1(narrative1: String): String? {
|
||||||
|
return try {
|
||||||
|
val date = SimpleDateFormat("dd-MM-yyyy HH-mm-ss", Locale.US).parse(narrative1)
|
||||||
|
date?.let { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(it) }
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,19 +7,15 @@ import okhttp3.FormBody
|
|||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
|
||||||
import sh.sar.basedbank.api.mib.Transaction
|
|
||||||
import sh.sar.basedbank.util.Totp
|
import sh.sar.basedbank.util.Totp
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class AuthExpiredException : Exception("Session expired")
|
class AuthExpiredException : Exception("Session expired")
|
||||||
@@ -53,11 +49,6 @@ class BmlLoginFlow {
|
|||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val apiClient = OkHttpClient.Builder()
|
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
/** PKCE params — generated once per login and reused across all profile activations. */
|
/** PKCE params — generated once per login and reused across all profile activations. */
|
||||||
private var codeVerifier: String = ""
|
private var codeVerifier: String = ""
|
||||||
private var codeChallenge: String = ""
|
private var codeChallenge: String = ""
|
||||||
@@ -234,7 +225,7 @@ class BmlLoginFlow {
|
|||||||
code: String,
|
code: String,
|
||||||
profile: BmlProfile,
|
profile: BmlProfile,
|
||||||
loginTag: String
|
loginTag: String
|
||||||
): Pair<BmlSession, List<MibAccount>> {
|
): Pair<BmlSession, List<BankAccount>> {
|
||||||
// Refresh XSRF token before submitting
|
// Refresh XSRF token before submitting
|
||||||
client.newCall(
|
client.newCall(
|
||||||
Request.Builder().url("$BASE_URL/web/profile/2fa/business")
|
Request.Builder().url("$BASE_URL/web/profile/2fa/business")
|
||||||
@@ -275,7 +266,7 @@ class BmlLoginFlow {
|
|||||||
loginTag: String,
|
loginTag: String,
|
||||||
profileName: String,
|
profileName: String,
|
||||||
profileId: String
|
profileId: String
|
||||||
): Pair<BmlSession, List<MibAccount>> {
|
): Pair<BmlSession, List<BankAccount>> {
|
||||||
val authorizeUrl = HttpUrl.Builder()
|
val authorizeUrl = HttpUrl.Builder()
|
||||||
.scheme("https").host("www.bankofmaldives.com.mv")
|
.scheme("https").host("www.bankofmaldives.com.mv")
|
||||||
.addPathSegments("internetbanking/oauth/authorize")
|
.addPathSegments("internetbanking/oauth/authorize")
|
||||||
@@ -323,419 +314,9 @@ class BmlLoginFlow {
|
|||||||
.takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed")
|
.takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed")
|
||||||
|
|
||||||
val session = BmlSession(accessToken = accessToken, deviceId = deviceId)
|
val session = BmlSession(accessToken = accessToken, deviceId = deviceId)
|
||||||
val accounts = fetchAccounts(session, loginTag, profileName, profileId)
|
val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profileName, profileId)
|
||||||
return Pair(session, accounts)
|
return Pair(session, accounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── API methods ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
fun fetchAccounts(
|
|
||||||
session: BmlSession,
|
|
||||||
loginTag: String,
|
|
||||||
profileName: String = "Personal",
|
|
||||||
profileId: String = ""
|
|
||||||
): List<MibAccount> {
|
|
||||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
|
|
||||||
val code = resp.code
|
|
||||||
val json = resp.body?.string()
|
|
||||||
resp.close()
|
|
||||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
|
||||||
return parseDashboard(json ?: return emptyList(), loginTag, profileName, profileId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchForeignLimits(session: BmlSession): List<BmlForeignLimit> {
|
|
||||||
val resp = apiClient.newCall(
|
|
||||||
Request.Builder().url("https://app.bankofmaldives.com.mv/api/v2/foreign-limits")
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val code = resp.code
|
|
||||||
val json = resp.body?.string()
|
|
||||||
resp.close()
|
|
||||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
|
||||||
return parseForeignLimits(json ?: return emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
data class BmlUserInfo(
|
|
||||||
val fullName: String,
|
|
||||||
val email: String,
|
|
||||||
val mobile: String,
|
|
||||||
val customerId: String,
|
|
||||||
val idCard: String,
|
|
||||||
val birthdate: String
|
|
||||||
)
|
|
||||||
|
|
||||||
fun fetchUserInfo(session: BmlSession): BmlUserInfo? {
|
|
||||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/userinfo")).execute()
|
|
||||||
val json = resp.body?.string() ?: return null
|
|
||||||
resp.close()
|
|
||||||
return try {
|
|
||||||
val root = JSONObject(json)
|
|
||||||
if (!root.optBoolean("success")) return null
|
|
||||||
val user = root.optJSONObject("payload")?.optJSONObject("user") ?: return null
|
|
||||||
BmlUserInfo(
|
|
||||||
fullName = user.optString("fullname").trim(),
|
|
||||||
email = user.optString("email").trim(),
|
|
||||||
mobile = user.optString("mobile_phone").trim(),
|
|
||||||
customerId = user.optString("customer_number").trim(),
|
|
||||||
idCard = user.optString("idcard").trim(),
|
|
||||||
birthdate = user.optString("birthdate").trim()
|
|
||||||
)
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validateAccount(session: BmlSession, input: String): BmlAccountValidation? {
|
|
||||||
val resp = apiClient.newCall(
|
|
||||||
Request.Builder().url("$BASE_URL/api/mobile/validate/account/$input")
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val json = resp.body?.string() ?: return null
|
|
||||||
resp.close()
|
|
||||||
return try {
|
|
||||||
val root = JSONObject(json)
|
|
||||||
if (!root.optBoolean("success")) return null
|
|
||||||
val payload = root.optJSONObject("payload") ?: return null
|
|
||||||
val trnType = payload.optString("trnType", "")
|
|
||||||
val validationType = payload.optString("validationType", "")
|
|
||||||
if (validationType == "alias") {
|
|
||||||
val cdtrAcct = payload.optJSONObject("CdtrAcct") ?: return null
|
|
||||||
BmlAccountValidation(
|
|
||||||
trnType = trnType,
|
|
||||||
validationType = validationType,
|
|
||||||
account = cdtrAcct.optString("Acct"),
|
|
||||||
originalInput = input,
|
|
||||||
name = payload.optString("contact_name").trim(),
|
|
||||||
alias = null,
|
|
||||||
currency = payload.optString("currency", "MVR"),
|
|
||||||
agnt = cdtrAcct.optString("FinInstnId").takeIf { it.isNotBlank() }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
BmlAccountValidation(
|
|
||||||
trnType = trnType,
|
|
||||||
validationType = validationType,
|
|
||||||
account = payload.optString("account"),
|
|
||||||
originalInput = input,
|
|
||||||
name = payload.optString("name"),
|
|
||||||
alias = payload.optString("alias").takeIf { it.isNotBlank() && it != "null" },
|
|
||||||
currency = payload.optString("currency", "MVR")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyMibAccount(session: BmlSession, account: String): BmlAccountValidation? {
|
|
||||||
val resp = apiClient.newCall(
|
|
||||||
Request.Builder().url("$BASE_URL/api/mobile/favara/account-verification/$account/MIB")
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val json = resp.body?.string() ?: return null
|
|
||||||
resp.close()
|
|
||||||
return try {
|
|
||||||
val root = JSONObject(json)
|
|
||||||
if (!root.optBoolean("success")) return null
|
|
||||||
BmlAccountValidation(
|
|
||||||
trnType = "DOT",
|
|
||||||
validationType = "MIB",
|
|
||||||
account = root.optString("account"),
|
|
||||||
originalInput = account,
|
|
||||||
name = root.optString("name"),
|
|
||||||
alias = null,
|
|
||||||
currency = "MVR",
|
|
||||||
agnt = root.optString("agnt").takeIf { it.isNotBlank() }
|
|
||||||
)
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveContact(
|
|
||||||
session: BmlSession,
|
|
||||||
contactType: String,
|
|
||||||
account: String,
|
|
||||||
alias: String,
|
|
||||||
currency: String? = null,
|
|
||||||
name: String? = null,
|
|
||||||
swift: String? = null
|
|
||||||
): Boolean {
|
|
||||||
val bodyObj = JSONObject().apply {
|
|
||||||
put("contact_type", contactType)
|
|
||||||
put("account", account)
|
|
||||||
put("alias", alias)
|
|
||||||
if (currency != null) put("currency", currency)
|
|
||||||
if (name != null) put("name", name)
|
|
||||||
if (swift != null) put("swift", swift)
|
|
||||||
}
|
|
||||||
val resp = apiClient.newCall(
|
|
||||||
Request.Builder().url("$BASE_URL/api/mobile/contacts")
|
|
||||||
.post(bodyObj.toString().toRequestBody("application/json".toMediaType()))
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val json = resp.body?.string() ?: return false
|
|
||||||
resp.close()
|
|
||||||
return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchContacts(session: BmlSession, loginId: String): List<MibBeneficiary> {
|
|
||||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/contacts")).execute()
|
|
||||||
val json = resp.body?.string() ?: return emptyList()
|
|
||||||
resp.close()
|
|
||||||
return parseContacts(json, loginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initiateTransfer(
|
|
||||||
session: BmlSession,
|
|
||||||
debitAccount: String,
|
|
||||||
creditAccount: String,
|
|
||||||
amount: Double,
|
|
||||||
transferType: String,
|
|
||||||
currency: String,
|
|
||||||
bank: String? = null
|
|
||||||
): Boolean {
|
|
||||||
val jo = JSONObject().apply {
|
|
||||||
put("debitAccount", debitAccount)
|
|
||||||
put("creditAccount", creditAccount)
|
|
||||||
put("debitAmount", amount)
|
|
||||||
put("transfertype", transferType)
|
|
||||||
put("currency", currency)
|
|
||||||
put("channel", "token")
|
|
||||||
if (bank != null) put("bank", bank)
|
|
||||||
}
|
|
||||||
val body = jo.toString().toRequestBody("application/json".toMediaType())
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("$BASE_URL/api/mobile/transfer")
|
|
||||||
.post(body)
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.header("accept", "application/json")
|
|
||||||
.build()
|
|
||||||
return apiClient.newCall(request).execute().use { response ->
|
|
||||||
val bodyStr = response.body?.string() ?: return@use false
|
|
||||||
try {
|
|
||||||
val json = JSONObject(bodyStr)
|
|
||||||
json.optBoolean("success") && json.optInt("code") == 22
|
|
||||||
} catch (_: Exception) { false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun confirmTransfer(
|
|
||||||
session: BmlSession,
|
|
||||||
debitAccount: String,
|
|
||||||
creditAccount: String,
|
|
||||||
amount: Double,
|
|
||||||
transferType: String,
|
|
||||||
currency: String,
|
|
||||||
otp: String,
|
|
||||||
remarks: String = "",
|
|
||||||
bank: String? = null
|
|
||||||
): BmlTransferResult {
|
|
||||||
val jo = JSONObject().apply {
|
|
||||||
put("debitAccount", debitAccount)
|
|
||||||
put("creditAccount", creditAccount)
|
|
||||||
put("debitAmount", amount)
|
|
||||||
put("transfertype", transferType)
|
|
||||||
put("currency", currency)
|
|
||||||
put("channel", "token")
|
|
||||||
put("otp", otp)
|
|
||||||
if (remarks.isNotBlank()) put("remarks", remarks)
|
|
||||||
if (bank != null) put("bank", bank)
|
|
||||||
}
|
|
||||||
val body = jo.toString().toRequestBody("application/json".toMediaType())
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("$BASE_URL/api/mobile/transfer")
|
|
||||||
.post(body)
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.header("accept", "application/json")
|
|
||||||
.build()
|
|
||||||
return apiClient.newCall(request).execute().use { response ->
|
|
||||||
val bodyStr = response.body?.string()
|
|
||||||
?: return@use BmlTransferResult(false, errorMessage = "No response")
|
|
||||||
try {
|
|
||||||
val json = JSONObject(bodyStr)
|
|
||||||
if (!json.optBoolean("success")) {
|
|
||||||
BmlTransferResult(false, errorMessage = json.optString("message").ifBlank { "Transfer failed" })
|
|
||||||
} else {
|
|
||||||
val payload = json.optJSONObject("payload")
|
|
||||||
BmlTransferResult(
|
|
||||||
success = true,
|
|
||||||
reference = payload?.optString("reference") ?: "",
|
|
||||||
timestamp = payload?.optString("timestamp") ?: "",
|
|
||||||
message = json.optString("message")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { BmlTransferResult(false, errorMessage = "Parse error") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteContact(session: BmlSession, contactId: String): Boolean {
|
|
||||||
val body = """{"_method":"delete"}""".toRequestBody("application/json".toMediaType())
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("$BASE_URL/api/mobile/contacts/$contactId")
|
|
||||||
.post(body)
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.header("accept", "application/json")
|
|
||||||
.build()
|
|
||||||
return apiClient.newCall(request).execute().use { response ->
|
|
||||||
val bodyStr = response.body?.string() ?: return@use false
|
|
||||||
try { JSONObject(bodyStr).optBoolean("success") } catch (_: Exception) { false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchAccountHistory(
|
|
||||||
session: BmlSession,
|
|
||||||
accountId: String,
|
|
||||||
accountDisplayName: String,
|
|
||||||
accountNumber: String,
|
|
||||||
page: Int
|
|
||||||
): Pair<List<Transaction>, Int> {
|
|
||||||
val resp = apiClient.newCall(
|
|
||||||
Request.Builder().url("$BASE_URL/api/mobile/account/$accountId/history/$page")
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val code = resp.code
|
|
||||||
val json = resp.body?.string() ?: return Pair(emptyList(), 0)
|
|
||||||
resp.close()
|
|
||||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
|
||||||
return try {
|
|
||||||
val root = JSONObject(json)
|
|
||||||
if (!root.optBoolean("success")) return Pair(emptyList(), 0)
|
|
||||||
val payload = root.optJSONObject("payload") ?: return Pair(emptyList(), 0)
|
|
||||||
val totalPages = payload.optInt("totalPages", 0)
|
|
||||||
val history = payload.optJSONArray("history") ?: return Pair(emptyList(), totalPages)
|
|
||||||
val transactions = (0 until history.length()).map { i ->
|
|
||||||
val item = history.getJSONObject(i)
|
|
||||||
val desc = item.optString("description").trim()
|
|
||||||
val narrative1 = item.optString("narrative1")
|
|
||||||
val date = when (desc) {
|
|
||||||
"Purchase" -> parsePurchaseNarrative1(narrative1) ?: item.optString("bookingDate")
|
|
||||||
"Transfer Debit", "Transfer Credit" -> parseTransferNarrative1(narrative1) ?: item.optString("bookingDate")
|
|
||||||
else -> item.optString("bookingDate")
|
|
||||||
}
|
|
||||||
Transaction(
|
|
||||||
id = item.optString("id"),
|
|
||||||
date = date,
|
|
||||||
description = desc,
|
|
||||||
amount = item.optDouble("amount", 0.0),
|
|
||||||
currency = item.optString("currency"),
|
|
||||||
counterpartyName = item.optString("narrative2").takeIf { it.isNotBlank() },
|
|
||||||
reference = item.optString("reference").takeIf { it.isNotBlank() },
|
|
||||||
accountNumber = accountNumber,
|
|
||||||
accountDisplayName = accountDisplayName,
|
|
||||||
source = "BML"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Pair(transactions, totalPages)
|
|
||||||
} catch (_: Exception) { Pair(emptyList(), 0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchCardHistory(
|
|
||||||
session: BmlSession,
|
|
||||||
cardId: String,
|
|
||||||
accountDisplayName: String,
|
|
||||||
accountNumber: String,
|
|
||||||
month: String
|
|
||||||
): List<Transaction> {
|
|
||||||
val body = """{"card":"$cardId","month":"$month"}"""
|
|
||||||
.toRequestBody("application/json".toMediaType())
|
|
||||||
val resp = apiClient.newCall(
|
|
||||||
Request.Builder().url("$BASE_URL/api/mobile/card/statement").post(body)
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val code = resp.code
|
|
||||||
val json = resp.body?.string() ?: return emptyList()
|
|
||||||
resp.close()
|
|
||||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
|
||||||
return try {
|
|
||||||
val root = JSONObject(json)
|
|
||||||
if (!root.optBoolean("success")) return emptyList()
|
|
||||||
val payload = root.optJSONObject("payload") ?: return emptyList()
|
|
||||||
val result = mutableListOf<Transaction>()
|
|
||||||
|
|
||||||
val authDetails = payload.optJSONObject("outstanding")
|
|
||||||
?.optJSONArray("CardOutStdAuthDetails")
|
|
||||||
if (authDetails != null) {
|
|
||||||
for (i in 0 until authDetails.length()) {
|
|
||||||
val item = authDetails.getJSONObject(i)
|
|
||||||
result.add(Transaction(
|
|
||||||
id = "auth_${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 unbilled = payload.optJSONObject("unbilled")
|
|
||||||
?.optJSONArray("CardUnbillTxnDetails")
|
|
||||||
if (unbilled != null) {
|
|
||||||
for (i in 0 until unbilled.length()) {
|
|
||||||
val item = unbilled.getJSONObject(i)
|
|
||||||
result.add(Transaction(
|
|
||||||
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")
|
|
||||||
if (statement != null) {
|
|
||||||
for (i in 0 until statement.length()) {
|
|
||||||
val item = statement.getJSONObject(i)
|
|
||||||
result.add(Transaction(
|
|
||||||
id = "stmt_${item.optString("TranRef", i.toString())}",
|
|
||||||
date = item.optString("TransDate", item.optString("TranDate", "")),
|
|
||||||
description = item.optString("TranDesc", item.optString("Description", "")).trim(),
|
|
||||||
amount = -item.optDouble("TranAmount", 0.0),
|
|
||||||
currency = item.optString("TranCcy", "MVR"),
|
|
||||||
counterpartyName = null,
|
|
||||||
reference = item.optString("TranRef").takeIf { it.isNotBlank() },
|
|
||||||
accountNumber = accountNumber,
|
|
||||||
accountDisplayName = accountDisplayName,
|
|
||||||
source = "BML_CARD"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
|
||||||
} catch (_: Exception) { emptyList() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Parsing ──────────────────────────────────────────────────────────────
|
// ─── Parsing ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -787,148 +368,6 @@ class BmlLoginFlow {
|
|||||||
}
|
}
|
||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseDashboard(
|
|
||||||
json: String,
|
|
||||||
loginTag: String,
|
|
||||||
profileName: String = "Personal",
|
|
||||||
profileId: String = ""
|
|
||||||
): List<MibAccount> {
|
|
||||||
val root = JSONObject(json)
|
|
||||||
if (!root.optBoolean("success")) return emptyList()
|
|
||||||
val dashboard = root.optJSONObject("payload")?.optJSONArray("dashboard") ?: return emptyList()
|
|
||||||
|
|
||||||
val casaAccounts = mutableListOf<MibAccount>()
|
|
||||||
val prepaidCards = mutableListOf<MibAccount>()
|
|
||||||
|
|
||||||
for (i in 0 until dashboard.length()) {
|
|
||||||
val item = dashboard.getJSONObject(i)
|
|
||||||
val currency = item.optString("currency", "MVR")
|
|
||||||
val accountType = item.optString("account_type", "CASA")
|
|
||||||
val product = item.optString("product")
|
|
||||||
val accountNumber = item.optString("account")
|
|
||||||
val status = item.optString("account_status", "Active")
|
|
||||||
val internalId = item.optString("id", "")
|
|
||||||
|
|
||||||
if (accountType == "CASA") {
|
|
||||||
val available = item.optDouble("availableBalance", 0.0)
|
|
||||||
casaAccounts.add(MibAccount(
|
|
||||||
bank = "BML",
|
|
||||||
profileName = profileName,
|
|
||||||
profileType = "BML",
|
|
||||||
accountNumber = accountNumber,
|
|
||||||
accountBriefName = item.optString("alias"),
|
|
||||||
currencyName = currency,
|
|
||||||
accountTypeName = product,
|
|
||||||
availableBalance = "%.2f".format(available),
|
|
||||||
currentBalance = "%.2f".format(item.optDouble("ledgerBalance", 0.0)),
|
|
||||||
blockedAmount = "%.2f".format(item.optDouble("lockedAmount", 0.0)),
|
|
||||||
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "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
|
|
||||||
val isPrepaid = item.optBoolean("prepaid_card", false)
|
|
||||||
val cardBalance = item.optJSONObject("cardBalance")
|
|
||||||
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
|
||||||
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
|
|
||||||
prepaidCards.add(MibAccount(
|
|
||||||
bank = "BML",
|
|
||||||
profileName = profileName,
|
|
||||||
profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT",
|
|
||||||
accountNumber = accountNumber,
|
|
||||||
accountBriefName = item.optString("alias").ifBlank { product },
|
|
||||||
currencyName = currency,
|
|
||||||
accountTypeName = product,
|
|
||||||
availableBalance = "%.2f".format(available),
|
|
||||||
currentBalance = "%.2f".format(current),
|
|
||||||
blockedAmount = "0.00",
|
|
||||||
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
|
||||||
statusDesc = status,
|
|
||||||
profileImageHash = null,
|
|
||||||
loginTag = loginTag,
|
|
||||||
profileId = profileId,
|
|
||||||
internalId = internalId
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return casaAccounts + prepaidCards
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseForeignLimits(json: String): List<BmlForeignLimit> {
|
|
||||||
return try {
|
|
||||||
val root = JSONObject(json)
|
|
||||||
if (!root.optBoolean("success")) return emptyList()
|
|
||||||
val payload = root.optJSONArray("payload") ?: return emptyList()
|
|
||||||
(0 until payload.length()).map { i ->
|
|
||||||
val item = payload.getJSONObject(i)
|
|
||||||
val usage = item.optJSONObject("usageByCategory") ?: JSONObject()
|
|
||||||
val atm = usage.optJSONObject("ATM") ?: JSONObject()
|
|
||||||
val ecom = usage.optJSONObject("ECOM") ?: JSONObject()
|
|
||||||
val pos = usage.optJSONObject("POS") ?: JSONObject()
|
|
||||||
BmlForeignLimit(
|
|
||||||
type = item.optString("type", "Debit"),
|
|
||||||
used = item.optDouble("used", 0.0),
|
|
||||||
totalLimit = item.optDouble("totalLimit", 0.0),
|
|
||||||
generalCap = item.optDouble("generalCap", 0.0),
|
|
||||||
generalRemaining = item.optDouble("generalRemaining", 0.0),
|
|
||||||
medicalRemaining = item.optDouble("medicalRemaining", 0.0),
|
|
||||||
isAtmEnabled = item.optBoolean("isAtmEnabled", false),
|
|
||||||
isPosEnabled = item.optBoolean("isPosEnabled", false),
|
|
||||||
atmRemaining = atm.optDouble("remaining", 0.0),
|
|
||||||
atmLimit = atm.optDouble("limit", 0.0),
|
|
||||||
ecomRemaining = ecom.optDouble("remaining", 0.0),
|
|
||||||
ecomLimit = ecom.optDouble("limit", 0.0),
|
|
||||||
posRemaining = pos.optDouble("remaining", 0.0),
|
|
||||||
posLimit = pos.optDouble("limit", 0.0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { emptyList() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseContacts(json: String, loginId: String = ""): List<MibBeneficiary> {
|
|
||||||
val root = JSONObject(json)
|
|
||||||
if (!root.optBoolean("success")) return emptyList()
|
|
||||||
val payload: JSONArray = root.optJSONArray("payload") ?: return emptyList()
|
|
||||||
val result = mutableListOf<MibBeneficiary>()
|
|
||||||
for (i in 0 until payload.length()) {
|
|
||||||
val item = payload.getJSONObject(i)
|
|
||||||
val account = item.optString("account", "")
|
|
||||||
if (account.isBlank()) continue
|
|
||||||
result.add(MibBeneficiary(
|
|
||||||
benefNo = "bml_${item.optInt("id")}",
|
|
||||||
benefName = item.optString("name"),
|
|
||||||
benefNickName = item.optString("alias", item.optString("name")),
|
|
||||||
benefAccount = account,
|
|
||||||
benefType = "I",
|
|
||||||
bankColor = "#0066A1",
|
|
||||||
benefBankName = "Bank of Maldives",
|
|
||||||
bankCode = "",
|
|
||||||
benefStatus = item.optString("status", "S"),
|
|
||||||
transferCyDesc = item.optString("currency", "MVR"),
|
|
||||||
customerImgHash = null,
|
|
||||||
benefCategoryId = "BML",
|
|
||||||
profileId = loginId
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private fun apiRequest(session: BmlSession, url: String) =
|
|
||||||
Request.Builder().url(url)
|
|
||||||
.header("Authorization", "Bearer ${session.accessToken}")
|
|
||||||
.header("User-Agent", APP_USER_AGENT)
|
|
||||||
.header("x-app-version", APP_VERSION)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private fun xsrfToken(): String? =
|
private fun xsrfToken(): String? =
|
||||||
cookieStore["www.bankofmaldives.com.mv"]?.firstOrNull { it.name == "XSRF-TOKEN" }?.value
|
cookieStore["www.bankofmaldives.com.mv"]?.firstOrNull { it.name == "XSRF-TOKEN" }?.value
|
||||||
|
|
||||||
@@ -955,25 +394,4 @@ class BmlLoginFlow {
|
|||||||
return bytes.joinToString("") { "%02x".format(it) }
|
return bytes.joinToString("") { "%02x".format(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// "12-05-2026 041675" → first 4 digits of time part as HH:mm
|
|
||||||
private fun parsePurchaseNarrative1(narrative1: String): String? {
|
|
||||||
return try {
|
|
||||||
val parts = narrative1.split(" ")
|
|
||||||
if (parts.size < 2) null
|
|
||||||
else {
|
|
||||||
val timePart = parts[1].take(4)
|
|
||||||
val combined = "${parts[0]} ${timePart.take(2)}:${timePart.drop(2)}:00"
|
|
||||||
val date = SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.US).parse(combined)
|
|
||||||
date?.let { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(it) }
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
// "11-04-2026 13-17-08" → yyyy-MM-dd HH:mm:ss
|
|
||||||
private fun parseTransferNarrative1(narrative1: String): String? {
|
|
||||||
return try {
|
|
||||||
val date = SimpleDateFormat("dd-MM-yyyy HH-mm-ss", Locale.US).parse(narrative1)
|
|
||||||
date?.let { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(it) }
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package sh.sar.basedbank.api.bml
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
|
||||||
data class BmlSession(
|
data class BmlSession(
|
||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
@@ -24,7 +24,7 @@ data class BmlOtpChannel(
|
|||||||
sealed class BmlActivationResult {
|
sealed class BmlActivationResult {
|
||||||
data class Success(
|
data class Success(
|
||||||
val session: BmlSession,
|
val session: BmlSession,
|
||||||
val accounts: List<MibAccount>
|
val accounts: List<BankAccount>
|
||||||
) : BmlActivationResult()
|
) : BmlActivationResult()
|
||||||
data class NeedsBusinessOtp(
|
data class NeedsBusinessOtp(
|
||||||
val channels: List<BmlOtpChannel>
|
val channels: List<BmlOtpChannel>
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class BmlTransferClient {
|
||||||
|
|
||||||
|
private val client = newBmlApiClient()
|
||||||
|
|
||||||
|
/** Step 1: initiate the transfer (triggers OTP). Returns true if the server accepted it. */
|
||||||
|
fun initiateTransfer(
|
||||||
|
session: BmlSession,
|
||||||
|
debitAccount: String,
|
||||||
|
creditAccount: String,
|
||||||
|
amount: Double,
|
||||||
|
transferType: String,
|
||||||
|
currency: String,
|
||||||
|
bank: String? = null
|
||||||
|
): Boolean {
|
||||||
|
val jo = JSONObject().apply {
|
||||||
|
put("debitAccount", debitAccount)
|
||||||
|
put("creditAccount", creditAccount)
|
||||||
|
put("debitAmount", amount)
|
||||||
|
put("transfertype", transferType)
|
||||||
|
put("currency", currency)
|
||||||
|
put("channel", "token")
|
||||||
|
if (bank != null) put("bank", bank)
|
||||||
|
}
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BML_BASE_URL/api/mobile/transfer")
|
||||||
|
.post(jo.toString().toRequestBody("application/json".toMediaType()))
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.build()
|
||||||
|
return client.newCall(request).execute().use { response ->
|
||||||
|
val bodyStr = response.body?.string() ?: return@use false
|
||||||
|
try {
|
||||||
|
val json = JSONObject(bodyStr)
|
||||||
|
json.optBoolean("success") && json.optInt("code") == 22
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Step 2: confirm with OTP. Returns a [BmlTransferResult] with success/reference/error. */
|
||||||
|
fun confirmTransfer(
|
||||||
|
session: BmlSession,
|
||||||
|
debitAccount: String,
|
||||||
|
creditAccount: String,
|
||||||
|
amount: Double,
|
||||||
|
transferType: String,
|
||||||
|
currency: String,
|
||||||
|
otp: String,
|
||||||
|
remarks: String = "",
|
||||||
|
bank: String? = null
|
||||||
|
): BmlTransferResult {
|
||||||
|
val jo = JSONObject().apply {
|
||||||
|
put("debitAccount", debitAccount)
|
||||||
|
put("creditAccount", creditAccount)
|
||||||
|
put("debitAmount", amount)
|
||||||
|
put("transfertype", transferType)
|
||||||
|
put("currency", currency)
|
||||||
|
put("channel", "token")
|
||||||
|
put("otp", otp)
|
||||||
|
if (remarks.isNotBlank()) put("remarks", remarks)
|
||||||
|
if (bank != null) put("bank", bank)
|
||||||
|
}
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BML_BASE_URL/api/mobile/transfer")
|
||||||
|
.post(jo.toString().toRequestBody("application/json".toMediaType()))
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.build()
|
||||||
|
return client.newCall(request).execute().use { response ->
|
||||||
|
val bodyStr = response.body?.string()
|
||||||
|
?: return@use BmlTransferResult(false, errorMessage = "No response")
|
||||||
|
try {
|
||||||
|
val json = JSONObject(bodyStr)
|
||||||
|
if (!json.optBoolean("success")) {
|
||||||
|
BmlTransferResult(false, errorMessage = json.optString("message").ifBlank { "Transfer failed" })
|
||||||
|
} else {
|
||||||
|
val payload = json.optJSONObject("payload")
|
||||||
|
BmlTransferResult(
|
||||||
|
success = true,
|
||||||
|
reference = payload?.optString("reference") ?: "",
|
||||||
|
timestamp = payload?.optString("timestamp") ?: "",
|
||||||
|
message = json.optString("message")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { BmlTransferResult(false, errorMessage = "Parse error") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class BmlValidateClient {
|
||||||
|
|
||||||
|
private val client = newBmlApiClient()
|
||||||
|
|
||||||
|
fun validateAccount(session: BmlSession, input: String): BmlAccountValidation? {
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder().url("$BML_BASE_URL/api/mobile/validate/account/$input")
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val json = resp.body?.string() ?: return null
|
||||||
|
resp.close()
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return null
|
||||||
|
val payload = root.optJSONObject("payload") ?: return null
|
||||||
|
val trnType = payload.optString("trnType", "")
|
||||||
|
val validationType = payload.optString("validationType", "")
|
||||||
|
if (validationType == "alias") {
|
||||||
|
val cdtrAcct = payload.optJSONObject("CdtrAcct") ?: return null
|
||||||
|
BmlAccountValidation(
|
||||||
|
trnType = trnType,
|
||||||
|
validationType = validationType,
|
||||||
|
account = cdtrAcct.optString("Acct"),
|
||||||
|
originalInput = input,
|
||||||
|
name = payload.optString("contact_name").trim(),
|
||||||
|
alias = null,
|
||||||
|
currency = payload.optString("currency", "MVR"),
|
||||||
|
agnt = cdtrAcct.optString("FinInstnId").takeIf { it.isNotBlank() }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BmlAccountValidation(
|
||||||
|
trnType = trnType,
|
||||||
|
validationType = validationType,
|
||||||
|
account = payload.optString("account"),
|
||||||
|
originalInput = input,
|
||||||
|
name = payload.optString("name"),
|
||||||
|
alias = payload.optString("alias").takeIf { it.isNotBlank() && it != "null" },
|
||||||
|
currency = payload.optString("currency", "MVR")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyMibAccount(session: BmlSession, account: String): BmlAccountValidation? {
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder().url("$BML_BASE_URL/api/mobile/favara/account-verification/$account/MIB")
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", BML_USER_AGENT)
|
||||||
|
.header("x-app-version", BML_APP_VERSION)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val json = resp.body?.string() ?: return null
|
||||||
|
resp.close()
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return null
|
||||||
|
BmlAccountValidation(
|
||||||
|
trnType = "DOT",
|
||||||
|
validationType = "MIB",
|
||||||
|
account = root.optString("account"),
|
||||||
|
originalInput = account,
|
||||||
|
name = root.optString("name"),
|
||||||
|
alias = null,
|
||||||
|
currency = "MVR",
|
||||||
|
agnt = root.optString("agnt").takeIf { it.isNotBlank() }
|
||||||
|
)
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package sh.sar.basedbank.api.fahipay
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class FahipayAccountClient {
|
||||||
|
|
||||||
|
private val BASE_URL = "https://fahipay.mv"
|
||||||
|
private val UA = "okhttp/4.12.0"
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun Request.Builder.auth(session: FahipaySession): Request.Builder = this
|
||||||
|
.header("authid", session.authId)
|
||||||
|
.header("Cookie", "__Secure-sess=${session.sessionCookie}")
|
||||||
|
.header("content-type", "multipart/form-data")
|
||||||
|
.header("User-Agent", UA)
|
||||||
|
|
||||||
|
fun fetchProfile(session: FahipaySession): FahipayUserProfile {
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder().url("$BASE_URL/actions/getprofile/?lang=en")
|
||||||
|
.auth(session).build()
|
||||||
|
).execute()
|
||||||
|
val json = resp.body?.string() ?: throw Exception("Empty profile response")
|
||||||
|
resp.close()
|
||||||
|
val obj = JSONObject(json)
|
||||||
|
val props = obj.optJSONObject("props") ?: JSONObject()
|
||||||
|
return FahipayUserProfile(
|
||||||
|
fullName = obj.optString("fullname").trim(),
|
||||||
|
email = obj.optString("email").trim(),
|
||||||
|
mobile = obj.optString("mobile").trim(),
|
||||||
|
nid = obj.optString("nid").trim(),
|
||||||
|
profileId = obj.optString("profileID").trim(),
|
||||||
|
walletAccount = props.optString("acc", ""),
|
||||||
|
linkedAccounts = props.optJSONObject("accs")?.toString() ?: "{}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchBalance(session: FahipaySession): Double {
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder().url("$BASE_URL/actions/getbalance/?lang=en")
|
||||||
|
.auth(session).build()
|
||||||
|
).execute()
|
||||||
|
val json = resp.body?.string() ?: return 0.0
|
||||||
|
resp.close()
|
||||||
|
return try {
|
||||||
|
val obj = JSONObject(json)
|
||||||
|
if (obj.optBoolean("error")) 0.0 else obj.optDouble("balance", 0.0)
|
||||||
|
} catch (_: Exception) { 0.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildAccount(profile: FahipayUserProfile, balance: Double, loginTag: String): BankAccount =
|
||||||
|
BankAccount(
|
||||||
|
bank = "FAHIPAY",
|
||||||
|
profileName = profile.fullName.ifBlank { "Fahipay" },
|
||||||
|
profileType = "FAHIPAY",
|
||||||
|
accountNumber = profile.walletAccount,
|
||||||
|
accountBriefName = "Fahipay Wallet",
|
||||||
|
currencyName = "MVR",
|
||||||
|
accountTypeName = "Digital Wallet",
|
||||||
|
availableBalance = "%.2f".format(balance),
|
||||||
|
currentBalance = "%.2f".format(balance),
|
||||||
|
blockedAmount = "0.00",
|
||||||
|
mvrBalance = "%.2f".format(balance),
|
||||||
|
statusDesc = "Active",
|
||||||
|
profileImageHash = null,
|
||||||
|
loginTag = loginTag,
|
||||||
|
internalId = profile.profileId
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package sh.sar.basedbank.api.fahipay
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
|
import sh.sar.basedbank.util.AccountInputParser
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class FahipayContactsClient {
|
||||||
|
|
||||||
|
private val BASE_URL = "https://fahipay.mv"
|
||||||
|
private val UA = "okhttp/4.12.0"
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun fetchContacts(session: FahipaySession): List<FahipayContactGroup> {
|
||||||
|
val endpoints = listOf(
|
||||||
|
Triple("FAHIPAY_RAASTAS", "Raastas", "ooredooraastas"),
|
||||||
|
Triple("FAHIPAY_RELOAD", "Reload", "dhiraagureload"),
|
||||||
|
Triple("FAHIPAY_OOREDOO_BILL", "Ooredoo Bill", "ooredoobillpay"),
|
||||||
|
Triple("FAHIPAY_DHIRAAGU_BILL", "Dhiraagu Bill", "dhiraagubillpay")
|
||||||
|
)
|
||||||
|
val result = mutableListOf<FahipayContactGroup>()
|
||||||
|
for ((catId, label, page) in endpoints) {
|
||||||
|
try {
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder()
|
||||||
|
.url("$BASE_URL/api/app/favs/?page=$page&lang=en")
|
||||||
|
.header("authid", session.authId)
|
||||||
|
.header("Cookie", "__Secure-sess=${session.sessionCookie}")
|
||||||
|
.header("User-Agent", UA)
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val json = resp.body?.string() ?: continue
|
||||||
|
resp.close()
|
||||||
|
val obj = JSONObject(json)
|
||||||
|
val groupObj = obj.optJSONObject(page) ?: continue
|
||||||
|
val contacts = mutableListOf<BankContact>()
|
||||||
|
for (key in groupObj.keys()) {
|
||||||
|
val entry = groupObj.getJSONObject(key)
|
||||||
|
val number = entry.optString("number")
|
||||||
|
val name = entry.optString("name").trim().ifBlank { number }
|
||||||
|
if (AccountInputParser.detect(number) != AccountInputParser.InputType.PHONE) continue
|
||||||
|
contacts.add(BankContact(
|
||||||
|
benefNo = "fp_${page}_$number",
|
||||||
|
benefName = "",
|
||||||
|
benefNickName = name,
|
||||||
|
benefAccount = number,
|
||||||
|
benefType = "FAHIPAY",
|
||||||
|
bankColor = "#FF6B00",
|
||||||
|
benefBankName = label,
|
||||||
|
bankCode = "",
|
||||||
|
benefStatus = "",
|
||||||
|
transferCyDesc = "",
|
||||||
|
customerImgHash = null,
|
||||||
|
benefCategoryId = catId,
|
||||||
|
profileId = ""
|
||||||
|
))
|
||||||
|
}
|
||||||
|
if (contacts.isNotEmpty()) result.add(FahipayContactGroup(catId, label, contacts))
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package sh.sar.basedbank.api.fahipay
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.models.BankTransaction
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class FahipayHistoryClient {
|
||||||
|
|
||||||
|
private val BASE_URL = "https://fahipay.mv"
|
||||||
|
private val UA = "okhttp/4.12.0"
|
||||||
|
private val PAGE_SIZE = 15
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun fetchHistory(
|
||||||
|
session: FahipaySession,
|
||||||
|
accountDisplayName: String,
|
||||||
|
accountNumber: String,
|
||||||
|
start: Int
|
||||||
|
): Pair<List<BankTransaction>, Int> {
|
||||||
|
val resp = client.newCall(
|
||||||
|
Request.Builder()
|
||||||
|
.url("$BASE_URL/actions/activity/?s=$start&l=$PAGE_SIZE&lang=en")
|
||||||
|
.header("authid", session.authId)
|
||||||
|
.header("Cookie", "__Secure-sess=${session.sessionCookie}")
|
||||||
|
.header("content-type", "multipart/form-data")
|
||||||
|
.header("User-Agent", UA)
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val json = resp.body?.string() ?: return Pair(emptyList(), 0)
|
||||||
|
resp.close()
|
||||||
|
return try {
|
||||||
|
val obj = JSONObject(json)
|
||||||
|
val total = obj.optInt("total", 0)
|
||||||
|
val entries = obj.optJSONArray("entries") ?: return Pair(emptyList(), total)
|
||||||
|
val list = (0 until entries.length()).map { i ->
|
||||||
|
val e = entries.getJSONObject(i)
|
||||||
|
BankTransaction(
|
||||||
|
id = e.optString("transaction"),
|
||||||
|
date = e.optString("date"),
|
||||||
|
description = e.optString("name").trim(),
|
||||||
|
amount = e.optDouble("amount", 0.0),
|
||||||
|
currency = "MVR",
|
||||||
|
counterpartyName = e.optString("details").takeIf { it.isNotBlank() },
|
||||||
|
reference = e.optString("transaction").takeIf { it.isNotBlank() },
|
||||||
|
accountNumber = accountNumber,
|
||||||
|
accountDisplayName = accountDisplayName,
|
||||||
|
source = "FAHIPAY",
|
||||||
|
iconUrl = e.optString("icon").takeIf { it.isNotBlank() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Pair(list, total)
|
||||||
|
} catch (_: Exception) { Pair(emptyList(), 0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,10 +10,6 @@ import okhttp3.Request
|
|||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
|
||||||
import sh.sar.basedbank.api.mib.Transaction
|
|
||||||
import sh.sar.basedbank.util.AccountInputParser
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@@ -21,8 +17,6 @@ class FahipayLoginFlow {
|
|||||||
|
|
||||||
private val BASE_URL = "https://fahipay.mv"
|
private val BASE_URL = "https://fahipay.mv"
|
||||||
private val UA_WEBVIEW = "Mozilla/5.0 (Linux; Android 14; ${Build.MODEL} Build/AP2A.240905.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
|
private val UA_WEBVIEW = "Mozilla/5.0 (Linux; Android 14; ${Build.MODEL} Build/AP2A.240905.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
|
||||||
private val UA_OKHTTP = "okhttp/4.12.0"
|
|
||||||
private val PAGE_SIZE = 15
|
|
||||||
|
|
||||||
private val cookieStore = mutableMapOf<String, MutableList<Cookie>>()
|
private val cookieStore = mutableMapOf<String, MutableList<Cookie>>()
|
||||||
private val cookieJar = object : CookieJar {
|
private val cookieJar = object : CookieJar {
|
||||||
@@ -144,165 +138,6 @@ class FahipayLoginFlow {
|
|||||||
?: throw Exception("No authID in OTP response")
|
?: throw Exception("No authID in OTP response")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchProfile(session: FahipaySession): FahipayUserProfile {
|
|
||||||
val resp = client.newCall(
|
|
||||||
Request.Builder().url("$BASE_URL/actions/getprofile/?lang=en")
|
|
||||||
.header("authid", session.authId)
|
|
||||||
.header("content-type", "multipart/form-data")
|
|
||||||
.header("User-Agent", UA_OKHTTP)
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val json = resp.body?.string() ?: throw Exception("Empty profile response")
|
|
||||||
resp.close()
|
|
||||||
|
|
||||||
val obj = JSONObject(json)
|
|
||||||
val props = obj.optJSONObject("props") ?: JSONObject()
|
|
||||||
return FahipayUserProfile(
|
|
||||||
fullName = obj.optString("fullname").trim(),
|
|
||||||
email = obj.optString("email").trim(),
|
|
||||||
mobile = obj.optString("mobile").trim(),
|
|
||||||
nid = obj.optString("nid").trim(),
|
|
||||||
profileId = obj.optString("profileID").trim(),
|
|
||||||
walletAccount = props.optString("acc", ""),
|
|
||||||
linkedAccounts = props.optJSONObject("accs")?.toString() ?: "{}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchBalance(session: FahipaySession): Double {
|
|
||||||
val resp = client.newCall(
|
|
||||||
Request.Builder().url("$BASE_URL/actions/getbalance/?lang=en")
|
|
||||||
.header("authid", session.authId)
|
|
||||||
.header("content-type", "multipart/form-data")
|
|
||||||
.header("User-Agent", UA_OKHTTP)
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val json = resp.body?.string() ?: return 0.0
|
|
||||||
resp.close()
|
|
||||||
return try {
|
|
||||||
val obj = JSONObject(json)
|
|
||||||
if (obj.optBoolean("error")) 0.0 else obj.optDouble("balance", 0.0)
|
|
||||||
} catch (_: Exception) { 0.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildAccount(profile: FahipayUserProfile, balance: Double, loginTag: String): MibAccount =
|
|
||||||
MibAccount(
|
|
||||||
bank = "FAHIPAY",
|
|
||||||
profileName = profile.fullName.ifBlank { "Fahipay" },
|
|
||||||
profileType = "FAHIPAY",
|
|
||||||
accountNumber = profile.walletAccount,
|
|
||||||
accountBriefName = "Fahipay Wallet",
|
|
||||||
currencyName = "MVR",
|
|
||||||
accountTypeName = "Digital Wallet",
|
|
||||||
availableBalance = "%.2f".format(balance),
|
|
||||||
currentBalance = "%.2f".format(balance),
|
|
||||||
blockedAmount = "0.00",
|
|
||||||
mvrBalance = "%.2f".format(balance),
|
|
||||||
statusDesc = "Active",
|
|
||||||
profileImageHash = null,
|
|
||||||
loginTag = loginTag,
|
|
||||||
internalId = profile.profileId
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches paginated activity history.
|
|
||||||
* @param start offset (0-based)
|
|
||||||
* @return Pair of (transactions, total count)
|
|
||||||
*/
|
|
||||||
fun fetchHistory(
|
|
||||||
session: FahipaySession,
|
|
||||||
accountDisplayName: String,
|
|
||||||
accountNumber: String,
|
|
||||||
start: Int
|
|
||||||
): Pair<List<Transaction>, Int> {
|
|
||||||
val resp = client.newCall(
|
|
||||||
Request.Builder()
|
|
||||||
.url("$BASE_URL/actions/activity/?s=$start&l=$PAGE_SIZE&lang=en")
|
|
||||||
.header("authid", session.authId)
|
|
||||||
.header("content-type", "multipart/form-data")
|
|
||||||
.header("User-Agent", UA_OKHTTP)
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val json = resp.body?.string() ?: return Pair(emptyList(), 0)
|
|
||||||
resp.close()
|
|
||||||
return try {
|
|
||||||
val obj = JSONObject(json)
|
|
||||||
val total = obj.optInt("total", 0)
|
|
||||||
val entries = obj.optJSONArray("entries") ?: return Pair(emptyList(), total)
|
|
||||||
val list = (0 until entries.length()).map { i ->
|
|
||||||
val e = entries.getJSONObject(i)
|
|
||||||
Transaction(
|
|
||||||
id = e.optString("transaction"),
|
|
||||||
date = e.optString("date"),
|
|
||||||
description = e.optString("name").trim(),
|
|
||||||
amount = e.optDouble("amount", 0.0),
|
|
||||||
currency = "MVR",
|
|
||||||
counterpartyName = e.optString("details").takeIf { it.isNotBlank() },
|
|
||||||
reference = e.optString("transaction").takeIf { it.isNotBlank() },
|
|
||||||
accountNumber = accountNumber,
|
|
||||||
accountDisplayName = accountDisplayName,
|
|
||||||
source = "FAHIPAY",
|
|
||||||
iconUrl = e.optString("icon").takeIf { it.isNotBlank() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Pair(list, total)
|
|
||||||
} catch (_: Exception) { Pair(emptyList(), 0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches Fahipay saved favourites for the 4 service groups.
|
|
||||||
* Only includes entries whose number is a valid 7-digit Maldivian phone number (starts with 7 or 9).
|
|
||||||
* Groups with no valid entries are omitted.
|
|
||||||
*/
|
|
||||||
fun fetchContacts(session: FahipaySession): List<FahipayContactGroup> {
|
|
||||||
val endpoints = listOf(
|
|
||||||
Triple("FAHIPAY_RAASTAS", "Raastas", "ooredooraastas"),
|
|
||||||
Triple("FAHIPAY_RELOAD", "Reload", "dhiraagureload"),
|
|
||||||
Triple("FAHIPAY_OOREDOO_BILL", "Ooredoo Bill", "ooredoobillpay"),
|
|
||||||
Triple("FAHIPAY_DHIRAAGU_BILL", "Dhiraagu Bill", "dhiraagubillpay")
|
|
||||||
)
|
|
||||||
val result = mutableListOf<FahipayContactGroup>()
|
|
||||||
for ((catId, label, page) in endpoints) {
|
|
||||||
try {
|
|
||||||
val resp = client.newCall(
|
|
||||||
Request.Builder()
|
|
||||||
.url("$BASE_URL/api/app/favs/?page=$page&lang=en")
|
|
||||||
.header("authid", session.authId)
|
|
||||||
.header("User-Agent", UA_OKHTTP)
|
|
||||||
.build()
|
|
||||||
).execute()
|
|
||||||
val json = resp.body?.string() ?: continue
|
|
||||||
resp.close()
|
|
||||||
val obj = JSONObject(json)
|
|
||||||
// Empty group comes back as a JSON array [], not an object — optJSONObject returns null
|
|
||||||
val groupObj = obj.optJSONObject(page) ?: continue
|
|
||||||
val contacts = mutableListOf<MibBeneficiary>()
|
|
||||||
for (key in groupObj.keys()) {
|
|
||||||
val entry = groupObj.getJSONObject(key)
|
|
||||||
val number = entry.optString("number")
|
|
||||||
val name = entry.optString("name").trim().ifBlank { number }
|
|
||||||
if (AccountInputParser.detect(number) != AccountInputParser.InputType.PHONE) continue
|
|
||||||
contacts.add(MibBeneficiary(
|
|
||||||
benefNo = "fp_${page}_$number",
|
|
||||||
benefName = "",
|
|
||||||
benefNickName = name,
|
|
||||||
benefAccount = number,
|
|
||||||
benefType = "FAHIPAY",
|
|
||||||
bankColor = "#FF6B00",
|
|
||||||
benefBankName = label,
|
|
||||||
bankCode = "",
|
|
||||||
benefStatus = "",
|
|
||||||
transferCyDesc = "",
|
|
||||||
customerImgHash = null,
|
|
||||||
benefCategoryId = catId,
|
|
||||||
profileId = ""
|
|
||||||
))
|
|
||||||
}
|
|
||||||
if (contacts.isNotEmpty()) result.add(FahipayContactGroup(catId, label, contacts))
|
|
||||||
} catch (_: Exception) {}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deviceParts(deviceUuid: String): Array<Pair<String, String>> = arrayOf(
|
private fun deviceParts(deviceUuid: String): Array<Pair<String, String>> = arrayOf(
|
||||||
"device[available]" to "true",
|
"device[available]" to "true",
|
||||||
"device[platform]" to "Android",
|
"device[platform]" to "Android",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package sh.sar.basedbank.api.fahipay
|
package sh.sar.basedbank.api.fahipay
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
|
|
||||||
data class FahipaySession(
|
data class FahipaySession(
|
||||||
val authId: String,
|
val authId: String,
|
||||||
val sessionCookie: String
|
val sessionCookie: String
|
||||||
@@ -23,5 +25,5 @@ data class FahipayLoginStep(
|
|||||||
data class FahipayContactGroup(
|
data class FahipayContactGroup(
|
||||||
val categoryId: String,
|
val categoryId: String,
|
||||||
val label: String,
|
val label: String,
|
||||||
val contacts: List<sh.sar.basedbank.api.mib.MibBeneficiary>
|
val contacts: List<BankContact>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -356,44 +356,6 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MibPersonalProfile(
|
|
||||||
val fullName: String,
|
|
||||||
val username: String,
|
|
||||||
val email: String,
|
|
||||||
val mobile: String,
|
|
||||||
val enrolled: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Fetches the customer's profile info from the Faisanet personal profile page. */
|
|
||||||
fun fetchPersonalProfile(session: MibSession): MibPersonalProfile? {
|
|
||||||
val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " +
|
|
||||||
"IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597"
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("https://faisamobilex-wv.mib.com.mv/personalProfile")
|
|
||||||
.get()
|
|
||||||
.header("Cookie", cookieHeader)
|
|
||||||
.build()
|
|
||||||
return try {
|
|
||||||
val resp = client.newCall(request).execute()
|
|
||||||
val html = resp.body?.string() ?: return null
|
|
||||||
resp.close()
|
|
||||||
fun scrape(label: String): String {
|
|
||||||
val r = Regex("""<span[^>]*>\s*<b[^>]*>\s*$label\s*</b[^>]*>.*?<span[^>]*>([^<]+)</span>""",
|
|
||||||
setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
|
|
||||||
return r.find(html)?.groupValues?.get(1)?.trim() ?: ""
|
|
||||||
}
|
|
||||||
val nameRegex = Regex("""<h5 class="mb-1 text-dark fw-semibold">\s*([^<]+)\s*</h5>""")
|
|
||||||
val fullName = nameRegex.find(html)?.groupValues?.get(1)?.trim() ?: return null
|
|
||||||
MibPersonalProfile(
|
|
||||||
fullName = fullName,
|
|
||||||
username = scrape("Username:"),
|
|
||||||
email = scrape("Email:"),
|
|
||||||
mobile = scrape("Mobile no:"),
|
|
||||||
enrolled = scrape("Enrolled:")
|
|
||||||
)
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Fetches a profile image via P41. Returns base64 JPEG string, or null if not found. */
|
/** Fetches a profile image via P41. Returns base64 JPEG string, or null if not found. */
|
||||||
fun fetchProfileImage(session: MibSession, imageHash: String): String? {
|
fun fetchProfileImage(session: MibSession, imageHash: String): String? {
|
||||||
val payload = baseData(session, "P41").apply {
|
val payload = baseData(session, "P41").apply {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
package sh.sar.basedbank.api.mib
|
package sh.sar.basedbank.api.mib
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
|
import sh.sar.basedbank.api.models.BankContactCategory
|
||||||
|
import sh.sar.basedbank.api.models.BankTransaction
|
||||||
|
|
||||||
|
// Kept for source compatibility within the mib package
|
||||||
|
typealias MibAccount = BankAccount
|
||||||
|
typealias MibBeneficiary = BankContact
|
||||||
|
typealias Transaction = BankTransaction
|
||||||
|
typealias MibBeneficiaryCategory = BankContactCategory
|
||||||
|
|
||||||
data class MibSession(
|
data class MibSession(
|
||||||
val appId: String,
|
val appId: String,
|
||||||
val xxid: String,
|
val xxid: String,
|
||||||
@@ -19,25 +30,6 @@ data class MibProfile(
|
|||||||
val customerImage: String?
|
val customerImage: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MibAccount(
|
|
||||||
val bank: String, // "MIB", "BML", "FAHIPAY" — set by the login flow
|
|
||||||
val profileName: String,
|
|
||||||
val profileType: String,
|
|
||||||
val cifType: String = "", // MIB: human-readable profile category (e.g. "Individual", "Sole Propr"); empty for other banks
|
|
||||||
val accountNumber: String,
|
|
||||||
val accountBriefName: String,
|
|
||||||
val currencyName: String,
|
|
||||||
val accountTypeName: String,
|
|
||||||
val availableBalance: String,
|
|
||||||
val currentBalance: String,
|
|
||||||
val blockedAmount: String,
|
|
||||||
val mvrBalance: String,
|
|
||||||
val statusDesc: String,
|
|
||||||
val profileImageHash: String?,
|
|
||||||
val loginTag: String = "",
|
|
||||||
val profileId: String = "", // MIB profile ID; empty for BML accounts
|
|
||||||
val internalId: String = "" // BML internal UUID; empty for MIB accounts
|
|
||||||
)
|
|
||||||
|
|
||||||
data class MibTransferResult(
|
data class MibTransferResult(
|
||||||
val success: Boolean,
|
val success: Boolean,
|
||||||
@@ -46,27 +38,6 @@ data class MibTransferResult(
|
|||||||
val errorMessage: String = ""
|
val errorMessage: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MibBeneficiaryCategory(
|
|
||||||
val id: String,
|
|
||||||
val categoryName: String,
|
|
||||||
val numBenef: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
data class MibBeneficiary(
|
|
||||||
val benefNo: String,
|
|
||||||
val benefName: String,
|
|
||||||
val benefNickName: String,
|
|
||||||
val benefAccount: String,
|
|
||||||
val benefType: String, // L=Local, I=Internal(MIB), S=Swift
|
|
||||||
val bankColor: String,
|
|
||||||
val benefBankName: String,
|
|
||||||
val bankCode: String,
|
|
||||||
val benefStatus: String,
|
|
||||||
val transferCyDesc: String,
|
|
||||||
val customerImgHash: String?,
|
|
||||||
val benefCategoryId: String, // "0" = uncategorized
|
|
||||||
val profileId: String = "" // MIB profile ID; empty for BML contacts
|
|
||||||
)
|
|
||||||
|
|
||||||
data class MibIpsAccountInfo(
|
data class MibIpsAccountInfo(
|
||||||
val accountName: String,
|
val accountName: String,
|
||||||
@@ -74,19 +45,6 @@ data class MibIpsAccountInfo(
|
|||||||
val bankId: String
|
val bankId: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Transaction(
|
|
||||||
val id: String,
|
|
||||||
val date: String, // "YYYY-MM-DD HH:mm:ss" for MIB, ISO8601 for BML
|
|
||||||
val description: String,
|
|
||||||
val amount: Double, // negative = debit, positive = credit
|
|
||||||
val currency: String,
|
|
||||||
val counterpartyName: String?,
|
|
||||||
val reference: String?,
|
|
||||||
val accountNumber: String,
|
|
||||||
val accountDisplayName: String,
|
|
||||||
val source: String, // "MIB", "BML", "BML_CARD", "FAHIPAY"
|
|
||||||
val iconUrl: String? = null // merchant icon URL (Fahipay only)
|
|
||||||
)
|
|
||||||
|
|
||||||
data class MibFinanceDeal(
|
data class MibFinanceDeal(
|
||||||
val dealNo: String,
|
val dealNo: String,
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package sh.sar.basedbank.api.mib
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
data class MibPersonalProfile(
|
||||||
|
val fullName: String,
|
||||||
|
val username: String,
|
||||||
|
val email: String,
|
||||||
|
val mobile: String,
|
||||||
|
val enrolled: String
|
||||||
|
)
|
||||||
|
|
||||||
|
class MibProfileClient {
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun fetchPersonalProfile(session: MibSession): MibPersonalProfile? {
|
||||||
|
val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " +
|
||||||
|
"IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597"
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("https://faisamobilex-wv.mib.com.mv/personalProfile")
|
||||||
|
.get()
|
||||||
|
.header("Cookie", cookieHeader)
|
||||||
|
.build()
|
||||||
|
return try {
|
||||||
|
val resp = client.newCall(request).execute()
|
||||||
|
val html = resp.body?.string() ?: return null
|
||||||
|
resp.close()
|
||||||
|
fun scrape(label: String): String {
|
||||||
|
val r = Regex("""<span[^>]*>\s*<b[^>]*>\s*$label\s*</b[^>]*>.*?<span[^>]*>([^<]+)</span>""",
|
||||||
|
setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
|
||||||
|
return r.find(html)?.groupValues?.get(1)?.trim() ?: ""
|
||||||
|
}
|
||||||
|
val nameRegex = Regex("""<h5 class="mb-1 text-dark fw-semibold">\s*([^<]+)\s*</h5>""")
|
||||||
|
val fullName = nameRegex.find(html)?.groupValues?.get(1)?.trim() ?: return null
|
||||||
|
MibPersonalProfile(
|
||||||
|
fullName = fullName,
|
||||||
|
username = scrape("Username:"),
|
||||||
|
email = scrape("Email:"),
|
||||||
|
mobile = scrape("Mobile no:"),
|
||||||
|
enrolled = scrape("Enrolled:")
|
||||||
|
)
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package sh.sar.basedbank.api.models
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified account model used across all banks (MIB, BML, Fahipay, ...).
|
||||||
|
* The [bank] field identifies which bank owns this account.
|
||||||
|
*/
|
||||||
|
data class BankAccount(
|
||||||
|
val bank: String, // "MIB", "BML", "FAHIPAY" — set by the login flow
|
||||||
|
val profileName: String,
|
||||||
|
val profileType: String,
|
||||||
|
val cifType: String = "", // MIB: human-readable profile category (e.g. "Individual", "Sole Propr"); empty for other banks
|
||||||
|
val accountNumber: String,
|
||||||
|
val accountBriefName: String,
|
||||||
|
val currencyName: String,
|
||||||
|
val accountTypeName: String,
|
||||||
|
val availableBalance: String,
|
||||||
|
val currentBalance: String,
|
||||||
|
val blockedAmount: String,
|
||||||
|
val mvrBalance: String,
|
||||||
|
val statusDesc: String,
|
||||||
|
val profileImageHash: String?,
|
||||||
|
val loginTag: String = "",
|
||||||
|
val profileId: String = "", // profile ID used by the bank; empty if not applicable
|
||||||
|
val internalId: String = "" // bank-internal UUID or ID; empty if not applicable
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified contact/beneficiary model used across all banks.
|
||||||
|
* Each bank may interpret fields differently; see per-bank notes below.
|
||||||
|
*/
|
||||||
|
data class BankContact(
|
||||||
|
val benefNo: String,
|
||||||
|
val benefName: String,
|
||||||
|
val benefNickName: String,
|
||||||
|
val benefAccount: String,
|
||||||
|
val benefType: String, // MIB: L=Local, I=Internal, S=Swift; BML: "I"; Fahipay: "FAHIPAY"
|
||||||
|
val bankColor: String,
|
||||||
|
val benefBankName: String,
|
||||||
|
val bankCode: String,
|
||||||
|
val benefStatus: String,
|
||||||
|
val transferCyDesc: String,
|
||||||
|
val customerImgHash: String?,
|
||||||
|
val benefCategoryId: String, // MIB: numeric category ID or "0"; BML: "BML"; Fahipay: "FAHIPAY"
|
||||||
|
val profileId: String = "" // owning profile ID; empty where not applicable
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact category (group) used across MIB and Fahipay.
|
||||||
|
*/
|
||||||
|
data class BankContactCategory(
|
||||||
|
val id: String,
|
||||||
|
val categoryName: String,
|
||||||
|
val numBenef: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified transaction model used across all banks.
|
||||||
|
* [source] identifies the originating bank/account type.
|
||||||
|
*/
|
||||||
|
data class BankTransaction(
|
||||||
|
val id: String,
|
||||||
|
val date: String, // "YYYY-MM-DD HH:mm:ss" (MIB/BML normalised) or ISO8601
|
||||||
|
val description: String,
|
||||||
|
val amount: Double, // negative = debit, positive = credit
|
||||||
|
val currency: String,
|
||||||
|
val counterpartyName: String?,
|
||||||
|
val reference: String?,
|
||||||
|
val accountNumber: String,
|
||||||
|
val accountDisplayName: String,
|
||||||
|
val source: String, // "MIB", "BML", "BML_CARD", "FAHIPAY"
|
||||||
|
val iconUrl: String? = null // merchant icon URL (Fahipay only)
|
||||||
|
)
|
||||||
@@ -9,8 +9,8 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.Transaction
|
import sh.sar.basedbank.api.models.BankTransaction
|
||||||
import sh.sar.basedbank.util.AccountHistoryDisplay
|
import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||||
import sh.sar.basedbank.databinding.ItemAccountHistoryHeaderBinding
|
import sh.sar.basedbank.databinding.ItemAccountHistoryHeaderBinding
|
||||||
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||||
@@ -21,13 +21,13 @@ import java.util.Date
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class AccountHistoryAdapter(
|
class AccountHistoryAdapter(
|
||||||
private val account: MibAccount,
|
private val account: BankAccount,
|
||||||
private val display: AccountHistoryDisplay
|
private val display: AccountHistoryDisplay
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private sealed class Item {
|
private sealed class Item {
|
||||||
data class DateHeader(val label: String) : Item()
|
data class DateHeader(val label: String) : Item()
|
||||||
data class Trx(val transaction: Transaction) : Item()
|
data class Trx(val transaction: BankTransaction) : Item()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val displayItems = mutableListOf<Item>()
|
private val displayItems = mutableListOf<Item>()
|
||||||
@@ -36,7 +36,7 @@ class AccountHistoryAdapter(
|
|||||||
private val iconUrlCache = mutableMapOf<String, Bitmap>()
|
private val iconUrlCache = mutableMapOf<String, Bitmap>()
|
||||||
var onImageNeeded: ((counterpartyName: String) -> Unit)? = null
|
var onImageNeeded: ((counterpartyName: String) -> Unit)? = null
|
||||||
var onIconUrlNeeded: ((url: String) -> Unit)? = null
|
var onIconUrlNeeded: ((url: String) -> Unit)? = null
|
||||||
var onTransferClick: ((MibAccount) -> Unit)? = null
|
var onTransferClick: ((BankAccount) -> Unit)? = null
|
||||||
private var hideAmounts: Boolean = false
|
private var hideAmounts: Boolean = false
|
||||||
|
|
||||||
fun setHideAmounts(hide: Boolean) {
|
fun setHideAmounts(hide: Boolean) {
|
||||||
@@ -84,7 +84,7 @@ class AccountHistoryAdapter(
|
|||||||
* Display the given (already sorted + filtered) list with date group headers.
|
* Display the given (already sorted + filtered) list with date group headers.
|
||||||
* Silently resets the loading footer so notifyDataSetChanged covers everything.
|
* Silently resets the loading footer so notifyDataSetChanged covers everything.
|
||||||
*/
|
*/
|
||||||
fun setTransactions(transactions: List<Transaction>) {
|
fun setTransactions(transactions: List<BankTransaction>) {
|
||||||
_showLoadingFooter = false
|
_showLoadingFooter = false
|
||||||
displayItems.clear()
|
displayItems.clear()
|
||||||
lastInsertedDateKey = ""
|
lastInsertedDateKey = ""
|
||||||
@@ -105,7 +105,7 @@ class AccountHistoryAdapter(
|
|||||||
* Appends [newTransactions] (assumed to be older than all existing items) using incremental
|
* Appends [newTransactions] (assumed to be older than all existing items) using incremental
|
||||||
* notifications, so the RecyclerView doesn't reset scroll position.
|
* notifications, so the RecyclerView doesn't reset scroll position.
|
||||||
*/
|
*/
|
||||||
fun appendTransactions(newTransactions: List<Transaction>) {
|
fun appendTransactions(newTransactions: List<BankTransaction>) {
|
||||||
if (newTransactions.isEmpty()) return
|
if (newTransactions.isEmpty()) return
|
||||||
if (_showLoadingFooter) {
|
if (_showLoadingFooter) {
|
||||||
val pos = itemCount - 1
|
val pos = itemCount - 1
|
||||||
@@ -184,7 +184,7 @@ class AccountHistoryAdapter(
|
|||||||
|
|
||||||
inner class TransactionVH(private val b: ItemTransactionBinding) :
|
inner class TransactionVH(private val b: ItemTransactionBinding) :
|
||||||
RecyclerView.ViewHolder(b.root) {
|
RecyclerView.ViewHolder(b.root) {
|
||||||
fun bind(trx: Transaction) {
|
fun bind(trx: BankTransaction) {
|
||||||
val isCredit = trx.amount >= 0
|
val isCredit = trx.amount >= 0
|
||||||
val color = sourceColor(trx.source)
|
val color = sourceColor(trx.source)
|
||||||
val name = trx.counterpartyName ?: trx.description
|
val name = trx.counterpartyName ?: trx.description
|
||||||
@@ -237,7 +237,7 @@ class AccountHistoryAdapter(
|
|||||||
b.root.setOnClickListener { showDetail(trx) }
|
b.root.setOnClickListener { showDetail(trx) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showDetail(trx: Transaction) {
|
private fun showDetail(trx: BankTransaction) {
|
||||||
val ctx = b.root.context
|
val ctx = b.root.context
|
||||||
val title = trx.counterpartyName?.takeIf { it.isNotBlank() } ?: trx.description
|
val title = trx.counterpartyName?.takeIf { it.isNotBlank() } ?: trx.description
|
||||||
val details = buildString {
|
val details = buildString {
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.api.mib.Transaction
|
import sh.sar.basedbank.api.models.BankTransaction
|
||||||
import sh.sar.basedbank.api.mib.TransactionCache
|
import sh.sar.basedbank.api.mib.TransactionCache
|
||||||
import sh.sar.basedbank.databinding.FragmentAccountHistoryBinding
|
import sh.sar.basedbank.databinding.FragmentAccountHistoryBinding
|
||||||
import sh.sar.basedbank.util.AccountHistoryParser
|
import sh.sar.basedbank.util.AccountHistoryParser
|
||||||
@@ -39,10 +39,10 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
private val viewModel: HomeViewModel by activityViewModels()
|
private val viewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
private lateinit var adapter: AccountHistoryAdapter
|
private lateinit var adapter: AccountHistoryAdapter
|
||||||
private lateinit var account: MibAccount
|
private lateinit var account: BankAccount
|
||||||
private lateinit var fetcher: HistoryFetcher
|
private lateinit var fetcher: HistoryFetcher
|
||||||
|
|
||||||
private val allTransactions = mutableListOf<Transaction>()
|
private val allTransactions = mutableListOf<BankTransaction>()
|
||||||
private var searchQuery = ""
|
private var searchQuery = ""
|
||||||
private var firstPageDone = false
|
private var firstPageDone = false
|
||||||
private val pendingImageNames = mutableSetOf<String>()
|
private val pendingImageNames = mutableSetOf<String>()
|
||||||
@@ -53,7 +53,7 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val ARG_ACCOUNT_NUMBER = "account_number"
|
private const val ARG_ACCOUNT_NUMBER = "account_number"
|
||||||
|
|
||||||
fun newInstance(account: MibAccount) = AccountHistoryFragment().apply {
|
fun newInstance(account: BankAccount) = AccountHistoryFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putString(ARG_ACCOUNT_NUMBER, account.accountNumber)
|
putString(ARG_ACCOUNT_NUMBER, account.accountNumber)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.databinding.ItemAccountBinding
|
import sh.sar.basedbank.databinding.ItemAccountBinding
|
||||||
import sh.sar.basedbank.databinding.ItemCardBinding
|
import sh.sar.basedbank.databinding.ItemCardBinding
|
||||||
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||||
@@ -16,22 +16,22 @@ import sh.sar.basedbank.util.AccountListDisplay
|
|||||||
import sh.sar.basedbank.util.AccountListParser
|
import sh.sar.basedbank.util.AccountListParser
|
||||||
|
|
||||||
class AccountsAdapter(
|
class AccountsAdapter(
|
||||||
accounts: List<MibAccount>,
|
accounts: List<BankAccount>,
|
||||||
private val onAccountClick: (MibAccount) -> Unit = {}
|
private val onAccountClick: (BankAccount) -> Unit = {}
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
var onTransferClick: ((MibAccount) -> Unit)? = null
|
var onTransferClick: ((BankAccount) -> Unit)? = null
|
||||||
private var hideAmounts: Boolean = false
|
private var hideAmounts: Boolean = false
|
||||||
|
|
||||||
private sealed class Item {
|
private sealed class Item {
|
||||||
data class SectionTitle(val label: String) : Item()
|
data class SectionTitle(val label: String) : Item()
|
||||||
data class Account(val account: MibAccount, val display: AccountListDisplay) : Item()
|
data class Account(val account: BankAccount, val display: AccountListDisplay) : Item()
|
||||||
data class Card(val account: MibAccount, val display: AccountListDisplay) : Item()
|
data class Card(val account: BankAccount, val display: AccountListDisplay) : Item()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val items: MutableList<Item> = buildItems(accounts).toMutableList()
|
private val items: MutableList<Item> = buildItems(accounts).toMutableList()
|
||||||
|
|
||||||
fun updateAccounts(accounts: List<MibAccount>) {
|
fun updateAccounts(accounts: List<BankAccount>) {
|
||||||
items.clear()
|
items.clear()
|
||||||
items.addAll(buildItems(accounts))
|
items.addAll(buildItems(accounts))
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
@@ -43,12 +43,12 @@ class AccountsAdapter(
|
|||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
|
private fun buildItems(accounts: List<BankAccount>): List<Item> = buildList {
|
||||||
val displayed = accounts.mapNotNull { acc -> AccountListParser.from(acc)?.let { acc to it } }
|
val displayed = accounts.mapNotNull { acc -> AccountListParser.from(acc)?.let { acc to it } }
|
||||||
val nonCards = displayed.filter { !it.second.isCard }
|
val nonCards = displayed.filter { !it.second.isCard }
|
||||||
val cards = displayed.filter { it.second.isCard }
|
val cards = displayed.filter { it.second.isCard }
|
||||||
|
|
||||||
val groups = LinkedHashMap<String, MutableList<Pair<MibAccount, AccountListDisplay>>>()
|
val groups = LinkedHashMap<String, MutableList<Pair<BankAccount, AccountListDisplay>>>()
|
||||||
for ((acc, display) in nonCards) {
|
for ((acc, display) in nonCards) {
|
||||||
val title = sectionTitle(acc)
|
val title = sectionTitle(acc)
|
||||||
groups.getOrPut(title) { mutableListOf() }.add(acc to display)
|
groups.getOrPut(title) { mutableListOf() }.add(acc to display)
|
||||||
@@ -64,7 +64,7 @@ class AccountsAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sectionTitle(account: MibAccount): String {
|
private fun sectionTitle(account: BankAccount): String {
|
||||||
val bankName = when (account.bank) {
|
val bankName = when (account.bank) {
|
||||||
"BML" -> "Bank of Maldives"
|
"BML" -> "Bank of Maldives"
|
||||||
"FAHIPAY" -> "Fahipay"
|
"FAHIPAY" -> "Fahipay"
|
||||||
@@ -112,7 +112,7 @@ class AccountsAdapter(
|
|||||||
|
|
||||||
private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
|
private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(account: MibAccount, display: AccountListDisplay) {
|
fun bind(account: BankAccount, display: AccountListDisplay) {
|
||||||
binding.tvAccountName.text = display.name
|
binding.tvAccountName.text = display.name
|
||||||
binding.tvAccountNumber.text = display.number
|
binding.tvAccountNumber.text = display.number
|
||||||
binding.tvAccountType.text = display.typeLabel
|
binding.tvAccountType.text = display.typeLabel
|
||||||
@@ -128,7 +128,7 @@ class AccountsAdapter(
|
|||||||
|
|
||||||
private inner class CardViewHolder(private val binding: ItemCardBinding) :
|
private inner class CardViewHolder(private val binding: ItemCardBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(account: MibAccount, display: AccountListDisplay) {
|
fun bind(account: BankAccount, display: AccountListDisplay) {
|
||||||
binding.ivCardBrand.setImageResource(display.cardBrandIcon)
|
binding.ivCardBrand.setImageResource(display.cardBrandIcon)
|
||||||
binding.tvCardName.text = display.name
|
binding.tvCardName.text = display.name
|
||||||
binding.tvCardNumber.text = display.number
|
binding.tvCardNumber.text = display.number
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ import kotlinx.coroutines.withContext
|
|||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlAccountValidation
|
import sh.sar.basedbank.api.bml.BmlAccountValidation
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
import sh.sar.basedbank.api.bml.BmlValidateClient
|
||||||
|
import sh.sar.basedbank.api.models.BankContactCategory
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibProfile
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
import sh.sar.basedbank.api.mib.MibTransferClient
|
import sh.sar.basedbank.api.mib.MibTransferClient
|
||||||
@@ -64,7 +65,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
private var selectedImageBase64: String = ""
|
private var selectedImageBase64: String = ""
|
||||||
private var selectedCategoryId: String = "0"
|
private var selectedCategoryId: String = "0"
|
||||||
private var categories: List<MibBeneficiaryCategory> = emptyList()
|
private var categories: List<BankContactCategory> = emptyList()
|
||||||
|
|
||||||
private val imagePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
private val imagePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||||
uri ?: return@registerForActivityResult
|
uri ?: return@registerForActivityResult
|
||||||
@@ -244,14 +245,12 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
private fun lookupForBml(input: String): BmlAccountValidation? {
|
private fun lookupForBml(input: String): BmlAccountValidation? {
|
||||||
val loginId = selectedDest?.bmlLoginId ?: return null
|
val loginId = selectedDest?.bmlLoginId ?: return null
|
||||||
val bmlSess = app.bmlSessions[loginId] ?: return null
|
val bmlSess = app.bmlSessions[loginId] ?: return null
|
||||||
val bmlFlow = BmlLoginFlow()
|
|
||||||
|
|
||||||
// 1) Try BML validate
|
// 1) Try BML validate
|
||||||
val validated = try { bmlFlow.validateAccount(bmlSess, input) } catch (_: Exception) { null }
|
val validated = try { BmlValidateClient().validateAccount(bmlSess, input) } catch (_: Exception) { null }
|
||||||
if (validated != null) return validated
|
if (validated != null) return validated
|
||||||
|
|
||||||
// 2) Try BML MIB verify
|
// 2) Try BML MIB verify
|
||||||
val mibVerified = try { bmlFlow.verifyMibAccount(bmlSess, input) } catch (_: Exception) { null }
|
val mibVerified = try { BmlValidateClient().verifyMibAccount(bmlSess, input) } catch (_: Exception) { null }
|
||||||
if (mibVerified != null) return mibVerified
|
if (mibVerified != null) return mibVerified
|
||||||
|
|
||||||
// 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML)
|
// 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML)
|
||||||
@@ -295,7 +294,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
// MIB lookup failed (e.g. BML USD account) — fall back to BML validate
|
// MIB lookup failed (e.g. BML USD account) — fall back to BML validate
|
||||||
val bmlSess = app.anyBmlSession() ?: return null
|
val bmlSess = app.anyBmlSession() ?: return null
|
||||||
return try { BmlLoginFlow().validateAccount(bmlSess, input) } catch (_: Exception) { null }
|
return try { BmlValidateClient().validateAccount(bmlSess, input) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLookupResult(validation: BmlAccountValidation, input: String) {
|
private fun showLookupResult(validation: BmlAccountValidation, input: String) {
|
||||||
@@ -421,15 +420,14 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
val loginId = selectedDest?.bmlLoginId ?: return false
|
val loginId = selectedDest?.bmlLoginId ?: return false
|
||||||
val bmlSess = app.bmlSessions[loginId] ?: return false
|
val bmlSess = app.bmlSessions[loginId] ?: return false
|
||||||
val lookup = bmlLookup ?: return false
|
val lookup = bmlLookup ?: return false
|
||||||
val bmlFlow = BmlLoginFlow()
|
|
||||||
val account = lookup.account
|
val account = lookup.account
|
||||||
return when {
|
return when {
|
||||||
account.matches(Regex("^7\\d{12}$")) ->
|
account.matches(Regex("^7\\d{12}$")) ->
|
||||||
// BML account → IAT
|
// BML account → IAT
|
||||||
bmlFlow.saveContact(bmlSess, "IAT", account, alias)
|
BmlContactsClient().saveContact(bmlSess, "IAT", account, alias)
|
||||||
account.matches(Regex("^9\\d{16}$")) ->
|
account.matches(Regex("^9\\d{16}$")) ->
|
||||||
// MIB internal → DOT; swift is BML's internal UUID for MIB bank
|
// MIB internal → DOT; swift is BML's internal UUID for MIB bank
|
||||||
bmlFlow.saveContact(bmlSess, "DOT", account, alias,
|
BmlContactsClient().saveContact(bmlSess, "DOT", account, alias,
|
||||||
currency = lookup.currency, name = lookup.name, swift = MIB_SWIFT_ON_BML)
|
currency = lookup.currency, name = lookup.name, swift = MIB_SWIFT_ON_BML)
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
@@ -489,7 +487,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
if (dest.isBml) {
|
if (dest.isBml) {
|
||||||
val loginId = dest.bmlLoginId ?: return@launch
|
val loginId = dest.bmlLoginId ?: return@launch
|
||||||
val bmlSess = app.bmlSessions[loginId] ?: return@launch
|
val bmlSess = app.bmlSessions[loginId] ?: return@launch
|
||||||
val fresh = BmlLoginFlow().fetchContacts(bmlSess, loginId)
|
val fresh = BmlContactsClient().fetchContacts(bmlSess, loginId)
|
||||||
val existing = viewModel.contacts.value ?: emptyList()
|
val existing = viewModel.contacts.value ?: emptyList()
|
||||||
val merged = existing.filter { it.benefCategoryId != "BML" } + fresh
|
val merged = existing.filter { it.benefCategoryId != "BML" } + fresh
|
||||||
viewModel.contacts.postValue(merged)
|
viewModel.contacts.postValue(merged)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
import sh.sar.basedbank.api.models.BankContactCategory
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.databinding.FragmentContactsBinding
|
import sh.sar.basedbank.databinding.FragmentContactsBinding
|
||||||
import sh.sar.basedbank.util.ContactDisplay
|
import sh.sar.basedbank.util.ContactDisplay
|
||||||
@@ -153,7 +153,7 @@ class ContactsFragment : Fragment() {
|
|||||||
}.also { it.attach() }
|
}.also { it.attach() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rebuildPager(cats: List<MibBeneficiaryCategory>) {
|
private fun rebuildPager(cats: List<BankContactCategory>) {
|
||||||
val pages = buildList {
|
val pages = buildList {
|
||||||
add(TabPage(null, getString(R.string.contacts_tab_all)))
|
add(TabPage(null, getString(R.string.contacts_tab_all)))
|
||||||
cats.forEach { add(TabPage(it.id, it.categoryName)) }
|
cats.forEach { add(TabPage(it.id, it.categoryName)) }
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||||
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
||||||
@@ -68,7 +68,7 @@ class DashboardFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateBalances(accounts: List<MibAccount>) {
|
private fun updateBalances(accounts: List<BankAccount>) {
|
||||||
val hide = viewModel.hideAmounts.value ?: false
|
val hide = viewModel.hideAmounts.value ?: false
|
||||||
if (hide) {
|
if (hide) {
|
||||||
binding.tvMvrBalance.text = "MVR ••••••"
|
binding.tvMvrBalance.text = "MVR ••••••"
|
||||||
|
|||||||
@@ -36,18 +36,22 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.AuthExpiredException
|
import sh.sar.basedbank.api.bml.AuthExpiredException
|
||||||
|
import sh.sar.basedbank.api.bml.BmlAccountClient
|
||||||
import sh.sar.basedbank.api.bml.BmlActivationResult
|
import sh.sar.basedbank.api.bml.BmlActivationResult
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlContactsClient
|
||||||
|
import sh.sar.basedbank.api.bml.BmlForeignLimitsClient
|
||||||
import sh.sar.basedbank.api.bml.BmlProfile
|
import sh.sar.basedbank.api.bml.BmlProfile
|
||||||
import sh.sar.basedbank.api.bml.BmlSession
|
import sh.sar.basedbank.api.bml.BmlSession
|
||||||
|
import sh.sar.basedbank.api.fahipay.FahipayAccountClient
|
||||||
|
import sh.sar.basedbank.api.fahipay.FahipayContactsClient
|
||||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||||
import sh.sar.basedbank.api.fahipay.FahipaySession
|
import sh.sar.basedbank.api.fahipay.FahipaySession
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||||
import sh.sar.basedbank.databinding.ActivityHomeBinding
|
import sh.sar.basedbank.databinding.ActivityHomeBinding
|
||||||
import sh.sar.basedbank.ui.login.LoginActivity
|
import sh.sar.basedbank.ui.login.LoginActivity
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
import sh.sar.basedbank.api.models.BankContactCategory
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibFinancingClient
|
import sh.sar.basedbank.api.mib.MibFinancingClient
|
||||||
import sh.sar.basedbank.api.mib.MibProfile
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
@@ -490,7 +494,7 @@ fun applyNavLabelVisibility() {
|
|||||||
val loginTag = "bml_$loginId"
|
val loginTag = "bml_$loginId"
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
val savedProfiles = store.loadBmlProfiles(loginId)
|
val savedProfiles = store.loadBmlProfiles(loginId)
|
||||||
val allAccounts = mutableListOf<MibAccount>()
|
val allAccounts = mutableListOf<BankAccount>()
|
||||||
var anyExpired = savedProfiles.isEmpty()
|
var anyExpired = savedProfiles.isEmpty()
|
||||||
|
|
||||||
// Try each saved profile's cached session
|
// Try each saved profile's cached session
|
||||||
@@ -499,7 +503,7 @@ fun applyNavLabelVisibility() {
|
|||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
try {
|
try {
|
||||||
val session = BmlSession(saved.first, saved.second)
|
val session = BmlSession(saved.first, saved.second)
|
||||||
val accounts = BmlLoginFlow().fetchAccounts(session, loginTag, profile.name, profile.profileId)
|
val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profile.name, profile.profileId)
|
||||||
app.bmlSessions[profile.profileId] = session
|
app.bmlSessions[profile.profileId] = session
|
||||||
allAccounts += accounts
|
allAccounts += accounts
|
||||||
} catch (_: AuthExpiredException) { anyExpired = true
|
} catch (_: AuthExpiredException) { anyExpired = true
|
||||||
@@ -517,7 +521,7 @@ fun applyNavLabelVisibility() {
|
|||||||
if (legacyToken != null) {
|
if (legacyToken != null) {
|
||||||
try {
|
try {
|
||||||
val session = BmlSession(legacyToken.first, legacyToken.second)
|
val session = BmlSession(legacyToken.first, legacyToken.second)
|
||||||
val accounts = BmlLoginFlow().fetchAccounts(session, loginTag)
|
val accounts = BmlAccountClient().fetchAccounts(session, loginTag)
|
||||||
app.bmlSessions[loginId] = session
|
app.bmlSessions[loginId] = session
|
||||||
allAccounts += accounts
|
allAccounts += accounts
|
||||||
anyExpired = false
|
anyExpired = false
|
||||||
@@ -565,7 +569,7 @@ fun applyNavLabelVisibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (allAccounts.isNotEmpty()) AccountCache.saveBml(this@HomeActivity, loginId, allAccounts)
|
if (allAccounts.isNotEmpty()) AccountCache.saveBml(this@HomeActivity, loginId, allAccounts)
|
||||||
allAccounts as List<MibAccount>
|
allAccounts as List<BankAccount>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,10 +585,9 @@ fun applyNavLabelVisibility() {
|
|||||||
if (savedSession != null) {
|
if (savedSession != null) {
|
||||||
try {
|
try {
|
||||||
val session = FahipaySession(savedSession.first, savedSession.second)
|
val session = FahipaySession(savedSession.first, savedSession.second)
|
||||||
fahipayFlow.setSessionCookie(session.sessionCookie)
|
val balance = FahipayAccountClient().fetchBalance(session)
|
||||||
val balance = fahipayFlow.fetchBalance(session)
|
val profile = FahipayAccountClient().fetchProfile(session)
|
||||||
val profile = fahipayFlow.fetchProfile(session)
|
val accounts = listOf(FahipayAccountClient().buildAccount(profile, balance, loginTag))
|
||||||
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.fahipaySessions[loginId] = session
|
app.fahipaySessions[loginId] = session
|
||||||
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
@@ -601,9 +604,9 @@ fun applyNavLabelVisibility() {
|
|||||||
val cookieValue = fahipayFlow.getSessionCookieValue() ?: ""
|
val cookieValue = fahipayFlow.getSessionCookieValue() ?: ""
|
||||||
val session = FahipaySession(authId, cookieValue)
|
val session = FahipaySession(authId, cookieValue)
|
||||||
store.saveFahipaySession(loginId, authId, cookieValue)
|
store.saveFahipaySession(loginId, authId, cookieValue)
|
||||||
val profile = fahipayFlow.fetchProfile(session)
|
val profile = FahipayAccountClient().fetchProfile(session)
|
||||||
val balance = fahipayFlow.fetchBalance(session)
|
val balance = FahipayAccountClient().fetchBalance(session)
|
||||||
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
val accounts = listOf(FahipayAccountClient().buildAccount(profile, balance, loginTag))
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.fahipaySessions[loginId] = session
|
app.fahipaySessions[loginId] = session
|
||||||
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
@@ -636,7 +639,7 @@ fun applyNavLabelVisibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Filters accounts whose profileId the user has hidden in settings. */
|
/** Filters accounts whose profileId the user has hidden in settings. */
|
||||||
private fun List<MibAccount>.filterVisibleAccounts(): List<MibAccount> {
|
private fun List<BankAccount>.filterVisibleAccounts(): List<BankAccount> {
|
||||||
val store = CredentialStore(this@HomeActivity)
|
val store = CredentialStore(this@HomeActivity)
|
||||||
return filter { acc ->
|
return filter { acc ->
|
||||||
when (acc.bank) {
|
when (acc.bank) {
|
||||||
@@ -669,11 +672,10 @@ fun applyNavLabelVisibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshBmlLimits(session: BmlSession) {
|
private fun refreshBmlLimits(session: BmlSession) {
|
||||||
val bmlFlow = BmlLoginFlow()
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val (userName, limits) = withContext(Dispatchers.IO) {
|
val (userName, limits) = withContext(Dispatchers.IO) {
|
||||||
Pair(bmlFlow.fetchUserInfo(session)?.fullName ?: "", bmlFlow.fetchForeignLimits(session))
|
Pair(BmlAccountClient().fetchUserInfo(session)?.fullName ?: "", BmlForeignLimitsClient().fetchForeignLimits(session))
|
||||||
}
|
}
|
||||||
val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf()
|
val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf()
|
||||||
val idx = existing.indexOfFirst { it.userName == userName }
|
val idx = existing.indexOfFirst { it.userName == userName }
|
||||||
@@ -693,7 +695,7 @@ fun applyNavLabelVisibility() {
|
|||||||
val allBmlContacts = withContext(Dispatchers.IO) {
|
val allBmlContacts = withContext(Dispatchers.IO) {
|
||||||
store.getBmlLoginIds().flatMap { loginId ->
|
store.getBmlLoginIds().flatMap { loginId ->
|
||||||
val session = app.anyBmlSessionFor(loginId) ?: return@flatMap emptyList()
|
val session = app.anyBmlSessionFor(loginId) ?: return@flatMap emptyList()
|
||||||
val contacts = BmlLoginFlow().fetchContacts(session, loginId)
|
val contacts = BmlContactsClient().fetchContacts(session, loginId)
|
||||||
if (contacts.isNotEmpty()) ContactsCache.saveBml(this@HomeActivity, loginId, contacts)
|
if (contacts.isNotEmpty()) ContactsCache.saveBml(this@HomeActivity, loginId, contacts)
|
||||||
contacts
|
contacts
|
||||||
}
|
}
|
||||||
@@ -736,13 +738,11 @@ fun applyNavLabelVisibility() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val groups = withContext(Dispatchers.IO) {
|
val groups = withContext(Dispatchers.IO) {
|
||||||
val flow = FahipayLoginFlow()
|
FahipayContactsClient().fetchContacts(session)
|
||||||
flow.setSessionCookie(session.sessionCookie)
|
|
||||||
flow.fetchContacts(session)
|
|
||||||
}
|
}
|
||||||
if (groups.isEmpty()) return@launch
|
if (groups.isEmpty()) return@launch
|
||||||
val contacts = groups.flatMap { it.contacts }
|
val contacts = groups.flatMap { it.contacts }
|
||||||
val categories = groups.map { MibBeneficiaryCategory(it.categoryId, it.label, it.contacts.size) }
|
val categories = groups.map { BankContactCategory(it.categoryId, it.label, it.contacts.size) }
|
||||||
ContactsCache.saveFahipay(this@HomeActivity, contacts, categories)
|
ContactsCache.saveFahipay(this@HomeActivity, contacts, categories)
|
||||||
val mibContacts = ContactsCache.loadContacts(this@HomeActivity)
|
val mibContacts = ContactsCache.loadContacts(this@HomeActivity)
|
||||||
val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds()
|
val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds()
|
||||||
@@ -755,11 +755,11 @@ fun applyNavLabelVisibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun mergeContacts(
|
private fun mergeContacts(
|
||||||
mib: List<MibBeneficiary>,
|
mib: List<BankContact>,
|
||||||
bml: List<MibBeneficiary>
|
bml: List<BankContact>
|
||||||
): List<MibBeneficiary> {
|
): List<BankContact> {
|
||||||
val seen = mutableSetOf<String>()
|
val seen = mutableSetOf<String>()
|
||||||
val result = mutableListOf<MibBeneficiary>()
|
val result = mutableListOf<BankContact>()
|
||||||
for (c in mib) if (seen.add(c.benefNo)) result.add(c)
|
for (c in mib) if (seen.add(c.benefNo)) result.add(c)
|
||||||
for (c in bml) if (seen.add(c.benefNo)) result.add(c)
|
for (c in bml) if (seen.add(c.benefNo)) result.add(c)
|
||||||
return result
|
return result
|
||||||
@@ -774,8 +774,8 @@ fun applyNavLabelVisibility() {
|
|||||||
val (allContacts, allCategories) = withContext(Dispatchers.IO) {
|
val (allContacts, allCategories) = withContext(Dispatchers.IO) {
|
||||||
val seenContacts = mutableSetOf<String>()
|
val seenContacts = mutableSetOf<String>()
|
||||||
val seenCategories = mutableSetOf<String>()
|
val seenCategories = mutableSetOf<String>()
|
||||||
val contacts = mutableListOf<MibBeneficiary>()
|
val contacts = mutableListOf<BankContact>()
|
||||||
val categories = mutableListOf<MibBeneficiaryCategory>()
|
val categories = mutableListOf<BankContactCategory>()
|
||||||
for (profile in profiles) {
|
for (profile in profiles) {
|
||||||
try {
|
try {
|
||||||
flow.switchProfile(session, profile)
|
flow.switchProfile(session, profile)
|
||||||
@@ -803,7 +803,7 @@ fun applyNavLabelVisibility() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshBalances(src: MibAccount) {
|
fun refreshBalances(src: BankAccount) {
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val current = viewModel.accounts.value ?: emptyList()
|
val current = viewModel.accounts.value ?: emptyList()
|
||||||
@@ -811,12 +811,10 @@ fun applyNavLabelVisibility() {
|
|||||||
val fresh = withContext(Dispatchers.IO) {
|
val fresh = withContext(Dispatchers.IO) {
|
||||||
val sess = app.fahipaySessionFor(src) ?: return@withContext null
|
val sess = app.fahipaySessionFor(src) ?: return@withContext null
|
||||||
try {
|
try {
|
||||||
val flow = FahipayLoginFlow()
|
val balance = FahipayAccountClient().fetchBalance(sess)
|
||||||
flow.setSessionCookie(sess.sessionCookie)
|
val profile = FahipayAccountClient().fetchProfile(sess)
|
||||||
val balance = flow.fetchBalance(sess)
|
|
||||||
val profile = flow.fetchProfile(sess)
|
|
||||||
val loginTag = "fahipay_${profile.profileId}"
|
val loginTag = "fahipay_${profile.profileId}"
|
||||||
val accounts = listOf(flow.buildAccount(profile, balance, loginTag))
|
val accounts = listOf(FahipayAccountClient().buildAccount(profile, balance, loginTag))
|
||||||
val loginId = src.loginTag.removePrefix("fahipay_")
|
val loginId = src.loginTag.removePrefix("fahipay_")
|
||||||
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != src.loginTag } + accounts
|
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != src.loginTag } + accounts
|
||||||
@@ -830,7 +828,7 @@ fun applyNavLabelVisibility() {
|
|||||||
val fresh = withContext(Dispatchers.IO) {
|
val fresh = withContext(Dispatchers.IO) {
|
||||||
val sess = app.bmlSessionFor(src) ?: return@withContext null
|
val sess = app.bmlSessionFor(src) ?: return@withContext null
|
||||||
try {
|
try {
|
||||||
val accounts = BmlLoginFlow().fetchAccounts(sess, src.loginTag)
|
val accounts = BmlAccountClient().fetchAccounts(sess, src.loginTag)
|
||||||
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
|
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
|
||||||
val otherBml = app.bmlAccounts.filter { it.loginTag != src.loginTag }
|
val otherBml = app.bmlAccounts.filter { it.loginTag != src.loginTag }
|
||||||
app.bmlAccounts = otherBml + accounts
|
app.bmlAccounts = otherBml + accounts
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ package sh.sar.basedbank.ui.home
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
import sh.sar.basedbank.api.models.BankContactCategory
|
||||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
val accounts = MutableLiveData<List<MibAccount>>(emptyList())
|
val accounts = MutableLiveData<List<BankAccount>>(emptyList())
|
||||||
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
||||||
val contacts = MutableLiveData<List<MibBeneficiary>>(emptyList())
|
val contacts = MutableLiveData<List<BankContact>>(emptyList())
|
||||||
val contactCategories = MutableLiveData<List<MibBeneficiaryCategory>>(emptyList())
|
val contactCategories = MutableLiveData<List<BankContactCategory>>(emptyList())
|
||||||
|
|
||||||
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
|
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
|
||||||
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
|
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlAccountClient
|
||||||
|
import sh.sar.basedbank.api.mib.MibProfileClient
|
||||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||||
import sh.sar.basedbank.databinding.FragmentOtpBinding
|
import sh.sar.basedbank.databinding.FragmentOtpBinding
|
||||||
import sh.sar.basedbank.databinding.ItemOtpCardBinding
|
import sh.sar.basedbank.databinding.ItemOtpCardBinding
|
||||||
@@ -94,7 +95,7 @@ class OtpFragment : Fragment() {
|
|||||||
val session = app.mibSessions[loginId] ?: continue
|
val session = app.mibSessions[loginId] ?: continue
|
||||||
val flow = app.mibFlowFor(loginId)
|
val flow = app.mibFlowFor(loginId)
|
||||||
val profile = withContext(Dispatchers.IO) {
|
val profile = withContext(Dispatchers.IO) {
|
||||||
try { flow.fetchPersonalProfile(session) } catch (_: Exception) { null }
|
try { MibProfileClient().fetchPersonalProfile(session) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile(
|
store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile(
|
||||||
@@ -114,7 +115,7 @@ class OtpFragment : Fragment() {
|
|||||||
if (store.loadBmlUserProfile(loginId)?.fullName.isNullOrBlank()) {
|
if (store.loadBmlUserProfile(loginId)?.fullName.isNullOrBlank()) {
|
||||||
val session = app.bmlSessions[loginId] ?: continue
|
val session = app.bmlSessions[loginId] ?: continue
|
||||||
val info = withContext(Dispatchers.IO) {
|
val info = withContext(Dispatchers.IO) {
|
||||||
try { BmlLoginFlow().fetchUserInfo(session) } catch (_: Exception) { null }
|
try { BmlAccountClient().fetchUserInfo(session) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
store.saveBmlUserProfile(loginId, CredentialStore.BmlUserProfile(
|
store.saveBmlUserProfile(loginId, CredentialStore.BmlUserProfile(
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.databinding.FragmentPayMvQrBinding
|
import sh.sar.basedbank.databinding.FragmentPayMvQrBinding
|
||||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||||
import sh.sar.basedbank.util.PaymvQrParser
|
import sh.sar.basedbank.util.PaymvQrParser
|
||||||
@@ -46,7 +46,7 @@ class PayMvQrFragment : Fragment() {
|
|||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private val viewModel: HomeViewModel by activityViewModels()
|
private val viewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
private var selectedAccount: MibAccount? = null
|
private var selectedAccount: BankAccount? = null
|
||||||
private var generatedBitmap: Bitmap? = null
|
private var generatedBitmap: Bitmap? = null
|
||||||
private var generateJob: Job? = null
|
private var generateJob: Job? = null
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ class PayMvQrFragment : Fragment() {
|
|||||||
|
|
||||||
private fun renderQrCard(
|
private fun renderQrCard(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
account: MibAccount,
|
account: BankAccount,
|
||||||
qrPayload: String,
|
qrPayload: String,
|
||||||
amountStr: String?
|
amountStr: String?
|
||||||
): Bitmap {
|
): Bitmap {
|
||||||
@@ -378,10 +378,10 @@ class PayMvQrFragment : Fragment() {
|
|||||||
|
|
||||||
private inner class QrAccountAdapter(
|
private inner class QrAccountAdapter(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val accounts: List<MibAccount>
|
private val accounts: List<BankAccount>
|
||||||
) : BaseAdapter(), Filterable {
|
) : BaseAdapter(), Filterable {
|
||||||
|
|
||||||
fun getAccount(position: Int): MibAccount? = accounts.getOrNull(position)
|
fun getAccount(position: Int): BankAccount? = accounts.getOrNull(position)
|
||||||
|
|
||||||
override fun getCount() = accounts.size
|
override fun getCount() = accounts.size
|
||||||
override fun getItem(position: Int) = accounts.getOrNull(position)
|
override fun getItem(position: Int) = accounts.getOrNull(position)
|
||||||
@@ -411,7 +411,7 @@ class PayMvQrFragment : Fragment() {
|
|||||||
FilterResults().apply { values = accounts; count = accounts.size }
|
FilterResults().apply { values = accounts; count = accounts.size }
|
||||||
override fun publishResults(c: CharSequence?, r: FilterResults?) = notifyDataSetChanged()
|
override fun publishResults(c: CharSequence?, r: FilterResults?) = notifyDataSetChanged()
|
||||||
override fun convertResultToString(r: Any?) =
|
override fun convertResultToString(r: Any?) =
|
||||||
(r as? MibAccount)?.let {
|
(r as? BankAccount)?.let {
|
||||||
val prefix = if (it.bank == "BML" && it.profileName.isNotBlank()) "${it.profileName} · " else ""
|
val prefix = if (it.bank == "BML" && it.profileName.isNotBlank()) "${it.profileName} · " else ""
|
||||||
"$prefix${it.accountBriefName}"
|
"$prefix${it.accountBriefName}"
|
||||||
} ?: ""
|
} ?: ""
|
||||||
|
|||||||
@@ -9,17 +9,17 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import sh.sar.basedbank.api.mib.Transaction
|
import sh.sar.basedbank.api.models.BankTransaction
|
||||||
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||||
import sh.sar.basedbank.databinding.ItemLoadingFooterBinding
|
import sh.sar.basedbank.databinding.ItemLoadingFooterBinding
|
||||||
import sh.sar.basedbank.databinding.ItemTransactionBinding
|
import sh.sar.basedbank.databinding.ItemTransactionBinding
|
||||||
|
|
||||||
/** Adapter for Transaction History — date-grouped, shows account name in secondary line. */
|
/** Adapter for BankTransaction History — date-grouped, shows account name in secondary line. */
|
||||||
class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private sealed class Item {
|
private sealed class Item {
|
||||||
data class DateHeader(val label: String) : Item()
|
data class DateHeader(val label: String) : Item()
|
||||||
data class Trx(val transaction: Transaction) : Item()
|
data class Trx(val transaction: BankTransaction) : Item()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val displayItems = mutableListOf<Item>()
|
private val displayItems = mutableListOf<Item>()
|
||||||
@@ -67,7 +67,7 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Replace the full sorted transaction list and rebuild date groups. */
|
/** Replace the full sorted transaction list and rebuild date groups. */
|
||||||
fun setTransactions(transactions: List<Transaction>) {
|
fun setTransactions(transactions: List<BankTransaction>) {
|
||||||
_showLoadingFooter = false
|
_showLoadingFooter = false
|
||||||
displayItems.clear()
|
displayItems.clear()
|
||||||
var lastDateKey = ""
|
var lastDateKey = ""
|
||||||
@@ -114,7 +114,7 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
|
|
||||||
inner class TransactionVH(private val b: ItemTransactionBinding) :
|
inner class TransactionVH(private val b: ItemTransactionBinding) :
|
||||||
RecyclerView.ViewHolder(b.root) {
|
RecyclerView.ViewHolder(b.root) {
|
||||||
fun bind(trx: Transaction) {
|
fun bind(trx: BankTransaction) {
|
||||||
val isCredit = trx.amount >= 0
|
val isCredit = trx.amount >= 0
|
||||||
val color = AccountHistoryAdapter.sourceColor(trx.source)
|
val color = AccountHistoryAdapter.sourceColor(trx.source)
|
||||||
val name = trx.counterpartyName ?: trx.description
|
val name = trx.counterpartyName ?: trx.description
|
||||||
@@ -141,7 +141,7 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
}
|
}
|
||||||
b.tvDescription.text = trx.description
|
b.tvDescription.text = trx.description
|
||||||
|
|
||||||
// Show account name in secondary line for Transaction History
|
// Show account name in secondary line for BankTransaction History
|
||||||
b.tvCounterparty.text = trx.accountDisplayName
|
b.tvCounterparty.text = trx.accountDisplayName
|
||||||
b.tvCounterparty.visibility = View.VISIBLE
|
b.tvCounterparty.visibility = View.VISIBLE
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
b.root.setOnClickListener { showDetail(trx) }
|
b.root.setOnClickListener { showDetail(trx) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showDetail(trx: Transaction) {
|
private fun showDetail(trx: BankTransaction) {
|
||||||
val ctx = b.root.context
|
val ctx = b.root.context
|
||||||
val title = trx.counterpartyName?.takeIf { it.isNotBlank() } ?: trx.description
|
val title = trx.counterpartyName?.takeIf { it.isNotBlank() } ?: trx.description
|
||||||
val details = buildString {
|
val details = buildString {
|
||||||
|
|||||||
@@ -36,12 +36,13 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlTransferClient
|
||||||
|
import sh.sar.basedbank.api.bml.BmlTransferResult
|
||||||
|
import sh.sar.basedbank.api.bml.BmlValidateClient
|
||||||
import sh.sar.basedbank.api.dhiraagu.DhiraaguClient
|
import sh.sar.basedbank.api.dhiraagu.DhiraaguClient
|
||||||
import sh.sar.basedbank.api.fahipay.OoredooClient
|
import sh.sar.basedbank.api.fahipay.OoredooClient
|
||||||
import sh.sar.basedbank.api.bml.BmlTransferResult
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibIpsAccountInfo
|
import sh.sar.basedbank.api.mib.MibIpsAccountInfo
|
||||||
import sh.sar.basedbank.api.mib.MibLookupException
|
import sh.sar.basedbank.api.mib.MibLookupException
|
||||||
@@ -65,11 +66,11 @@ class TransferFragment : Fragment() {
|
|||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private val viewModel: HomeViewModel by activityViewModels()
|
private val viewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
private var selectedAccount: MibAccount? = null
|
private var selectedAccount: BankAccount? = null
|
||||||
private val session get() = selectedAccount
|
private val session get() = selectedAccount
|
||||||
?.let { (requireActivity().application as BasedBankApp).mibSessionFor(it) }
|
?.let { (requireActivity().application as BasedBankApp).mibSessionFor(it) }
|
||||||
?: (requireActivity().application as BasedBankApp).anyMibSession()
|
?: (requireActivity().application as BasedBankApp).anyMibSession()
|
||||||
private fun bmlSessionFor(account: MibAccount?) =
|
private fun bmlSessionFor(account: BankAccount?) =
|
||||||
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
|
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
|
||||||
?: (requireActivity().application as BasedBankApp).anyBmlSession()
|
?: (requireActivity().application as BasedBankApp).anyBmlSession()
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ class TransferFragment : Fragment() {
|
|||||||
private const val ARG_AMOUNT_PREFILL = "amount_prefill"
|
private const val ARG_AMOUNT_PREFILL = "amount_prefill"
|
||||||
private const val ARG_REMARKS_PREFILL = "remarks_prefill"
|
private const val ARG_REMARKS_PREFILL = "remarks_prefill"
|
||||||
|
|
||||||
fun newInstanceFrom(account: MibAccount) = TransferFragment().apply {
|
fun newInstanceFrom(account: BankAccount) = TransferFragment().apply {
|
||||||
arguments = Bundle().apply { putString(ARG_FROM_ACCOUNT, account.accountNumber) }
|
arguments = Bundle().apply { putString(ARG_FROM_ACCOUNT, account.accountNumber) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +240,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFromCard(account: MibAccount) {
|
private fun showFromCard(account: BankAccount) {
|
||||||
val colorHex = when (account.bank) {
|
val colorHex = when (account.bank) {
|
||||||
"BML" -> "#0066A1"
|
"BML" -> "#0066A1"
|
||||||
"FAHIPAY" -> "#15BEA7"
|
"FAHIPAY" -> "#15BEA7"
|
||||||
@@ -284,7 +285,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateAmountPrefix(account: MibAccount) {
|
private fun updateAmountPrefix(account: BankAccount) {
|
||||||
binding.tilAmount.prefixText = if (account.currencyName == "USD") "USD " else "MVR "
|
binding.tilAmount.prefixText = if (account.currencyName == "USD") "USD " else "MVR "
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,10 +370,9 @@ class TransferFragment : Fragment() {
|
|||||||
val info = withContext(Dispatchers.IO) {
|
val info = withContext(Dispatchers.IO) {
|
||||||
if (isBmlSource && bmlSess != null) {
|
if (isBmlSource && bmlSess != null) {
|
||||||
val inputType = AccountInputParser.detect(accountNumber)
|
val inputType = AccountInputParser.detect(accountNumber)
|
||||||
val bmlFlow = BmlLoginFlow()
|
|
||||||
val bmlResult = try {
|
val bmlResult = try {
|
||||||
if (inputType == AccountInputParser.InputType.MIB_ACCOUNT) bmlFlow.verifyMibAccount(bmlSess, accountNumber)
|
if (inputType == AccountInputParser.InputType.MIB_ACCOUNT) BmlValidateClient().verifyMibAccount(bmlSess, accountNumber)
|
||||||
else bmlFlow.validateAccount(bmlSess, accountNumber)
|
else BmlValidateClient().validateAccount(bmlSess, accountNumber)
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
if (bmlResult != null) {
|
if (bmlResult != null) {
|
||||||
val bankId = when (bmlResult.trnType) {
|
val bankId = when (bmlResult.trnType) {
|
||||||
@@ -393,7 +393,7 @@ class TransferFragment : Fragment() {
|
|||||||
catch (e: MibLookupException) { errorMsg = e.message; null }
|
catch (e: MibLookupException) { errorMsg = e.message; null }
|
||||||
catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null }
|
catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null }
|
||||||
} else {
|
} else {
|
||||||
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null }
|
val bmlResult = try { BmlValidateClient().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null }
|
||||||
if (bmlResult != null) {
|
if (bmlResult != null) {
|
||||||
val bankId = when (bmlResult.trnType) {
|
val bankId = when (bmlResult.trnType) {
|
||||||
"IAT" -> "MALBMVMV"
|
"IAT" -> "MALBMVMV"
|
||||||
@@ -731,7 +731,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun doMibTransfer(
|
private fun doMibTransfer(
|
||||||
src: MibAccount,
|
src: BankAccount,
|
||||||
destAccount: String,
|
destAccount: String,
|
||||||
destName: String,
|
destName: String,
|
||||||
destDisplay: String,
|
destDisplay: String,
|
||||||
@@ -790,7 +790,7 @@ class TransferFragment : Fragment() {
|
|||||||
mibReferenceNo = result.trxId,
|
mibReferenceNo = result.trxId,
|
||||||
mibTransactionDate = result.date
|
mibTransactionDate = result.date
|
||||||
)
|
)
|
||||||
Triple(true, "Transaction ID: ${result.trxId}\n${result.date}", receipt)
|
Triple(true, "BankTransaction ID: ${result.trxId}\n${result.date}", receipt)
|
||||||
} else {
|
} else {
|
||||||
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null)
|
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null)
|
||||||
}
|
}
|
||||||
@@ -800,7 +800,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun doBmlTransfer(
|
private fun doBmlTransfer(
|
||||||
src: MibAccount,
|
src: BankAccount,
|
||||||
destAccount: String,
|
destAccount: String,
|
||||||
destDisplay: String,
|
destDisplay: String,
|
||||||
amount: Double,
|
amount: Double,
|
||||||
@@ -809,8 +809,8 @@ class TransferFragment : Fragment() {
|
|||||||
isSrcCard: Boolean,
|
isSrcCard: Boolean,
|
||||||
isDestMib: Boolean,
|
isDestMib: Boolean,
|
||||||
currency: String,
|
currency: String,
|
||||||
allAccounts: List<MibAccount>,
|
allAccounts: List<BankAccount>,
|
||||||
allContacts: List<MibBeneficiary>
|
allContacts: List<BankContact>
|
||||||
): Triple<Boolean, String, TransferReceiptData?> {
|
): Triple<Boolean, String, TransferReceiptData?> {
|
||||||
val loginId = src.loginTag.removePrefix("bml_")
|
val loginId = src.loginTag.removePrefix("bml_")
|
||||||
val sess = bmlSessionFor(src) ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
val sess = bmlSessionFor(src) ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||||
@@ -845,10 +845,9 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
|
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
|
||||||
|
|
||||||
val bmlFlow = BmlLoginFlow()
|
|
||||||
// Step 1: initiate
|
// Step 1: initiate
|
||||||
val initiated = try {
|
val initiated = try {
|
||||||
bmlFlow.initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank)
|
BmlTransferClient().initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank)
|
||||||
} catch (e: Exception) { return Triple(false, e.message ?: "Initiation failed", null) }
|
} catch (e: Exception) { return Triple(false, e.message ?: "Initiation failed", null) }
|
||||||
|
|
||||||
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
|
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
|
||||||
@@ -858,7 +857,7 @@ class TransferFragment : Fragment() {
|
|||||||
?.let { Totp.generate(it) } ?: otp
|
?.let { Totp.generate(it) } ?: otp
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val result = bmlFlow.confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
|
val result = BmlTransferClient().confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
val receipt = TransferReceiptData(
|
val receipt = TransferReceiptData(
|
||||||
bank = "BML",
|
bank = "BML",
|
||||||
@@ -1000,12 +999,12 @@ class TransferFragment : Fragment() {
|
|||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MibAccount.toDisplayString() = "$accountBriefName · $accountNumber"
|
private fun BankAccount.toDisplayString() = "$accountBriefName · $accountNumber"
|
||||||
|
|
||||||
// items: String = section header, MibAccount = selectable row
|
// items: String = section header, BankAccount = selectable row
|
||||||
private inner class AccountDropdownAdapter(
|
private inner class AccountDropdownAdapter(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
accounts: List<MibAccount>
|
accounts: List<BankAccount>
|
||||||
) : BaseAdapter(), Filterable {
|
) : BaseAdapter(), Filterable {
|
||||||
|
|
||||||
private val items: List<Any> = buildList {
|
private val items: List<Any> = buildList {
|
||||||
@@ -1018,7 +1017,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAccount(position: Int): MibAccount? = (items.getOrNull(position) as? MibAccount)
|
fun getAccount(position: Int): BankAccount? = (items.getOrNull(position) as? BankAccount)
|
||||||
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
|
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
|
||||||
|
|
||||||
override fun getCount() = items.size
|
override fun getCount() = items.size
|
||||||
@@ -1043,7 +1042,7 @@ class TransferFragment : Fragment() {
|
|||||||
b.tvHeader.text = item
|
b.tvHeader.text = item
|
||||||
b.root
|
b.root
|
||||||
} else {
|
} else {
|
||||||
val acc = item as MibAccount
|
val acc = item as BankAccount
|
||||||
val b = if (convertView?.tag is ItemAccountDropdownBinding) {
|
val b = if (convertView?.tag is ItemAccountDropdownBinding) {
|
||||||
convertView.tag as ItemAccountDropdownBinding
|
convertView.tag as ItemAccountDropdownBinding
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlHistoryClient
|
||||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
import sh.sar.basedbank.api.fahipay.FahipayHistoryClient
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibHistoryClient
|
import sh.sar.basedbank.api.mib.MibHistoryClient
|
||||||
import sh.sar.basedbank.api.mib.Transaction
|
import sh.sar.basedbank.api.models.BankTransaction
|
||||||
import sh.sar.basedbank.api.mib.TransactionCache
|
import sh.sar.basedbank.api.mib.TransactionCache
|
||||||
import sh.sar.basedbank.databinding.FragmentTransferHistoryBinding
|
import sh.sar.basedbank.databinding.FragmentTransferHistoryBinding
|
||||||
import sh.sar.basedbank.util.ContactImageCache
|
import sh.sar.basedbank.util.ContactImageCache
|
||||||
@@ -48,12 +48,12 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
private lateinit var adapter: TransactionAdapter
|
private lateinit var adapter: TransactionAdapter
|
||||||
|
|
||||||
// All merged transactions sorted by date desc
|
// All merged transactions sorted by date desc
|
||||||
private val allTransactions = mutableListOf<Transaction>()
|
private val allTransactions = mutableListOf<BankTransaction>()
|
||||||
private var searchQuery = ""
|
private var searchQuery = ""
|
||||||
|
|
||||||
// Per-account pagination state
|
// Per-account pagination state
|
||||||
private data class AccountState(
|
private data class AccountState(
|
||||||
val account: MibAccount,
|
val account: BankAccount,
|
||||||
var mibNextStart: Int = 1,
|
var mibNextStart: Int = 1,
|
||||||
var mibTotalCount: Int = -1,
|
var mibTotalCount: Int = -1,
|
||||||
var bmlNextPage: Int = 1,
|
var bmlNextPage: Int = 1,
|
||||||
@@ -158,7 +158,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val newTransactions = withContext(Dispatchers.IO) {
|
val newTransactions = withContext(Dispatchers.IO) {
|
||||||
val results = mutableListOf<Transaction>()
|
val results = mutableListOf<BankTransaction>()
|
||||||
|
|
||||||
// BML accounts: fetch in parallel
|
// BML accounts: fetch in parallel
|
||||||
val bmlStates = activeStates.filter { it.account.bank == "BML" }
|
val bmlStates = activeStates.filter { it.account.bank == "BML" }
|
||||||
@@ -172,7 +172,7 @@ 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++
|
||||||
BmlLoginFlow().fetchCardHistory(
|
BmlHistoryClient().fetchCardHistory(
|
||||||
session = session,
|
session = session,
|
||||||
cardId = state.account.internalId,
|
cardId = state.account.internalId,
|
||||||
accountDisplayName = state.account.accountBriefName,
|
accountDisplayName = state.account.accountBriefName,
|
||||||
@@ -182,7 +182,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
|
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
|
||||||
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
|
val (list, totalPages) = BmlHistoryClient().fetchAccountHistory(
|
||||||
session = session,
|
session = session,
|
||||||
accountId = state.account.internalId,
|
accountId = state.account.internalId,
|
||||||
accountDisplayName = state.account.accountBriefName,
|
accountDisplayName = state.account.accountBriefName,
|
||||||
@@ -194,7 +194,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
list
|
list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { emptyList<Transaction>() }
|
} catch (_: Exception) { emptyList<BankTransaction>() }
|
||||||
}
|
}
|
||||||
}.awaitAll().flatten())
|
}.awaitAll().flatten())
|
||||||
|
|
||||||
@@ -203,9 +203,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
for (state in fahipayStates) {
|
for (state in fahipayStates) {
|
||||||
val session = app.fahipaySessionFor(state.account) ?: continue
|
val session = app.fahipaySessionFor(state.account) ?: continue
|
||||||
try {
|
try {
|
||||||
val flow = FahipayLoginFlow()
|
val (list, total) = FahipayHistoryClient().fetchHistory(
|
||||||
flow.setSessionCookie(session.sessionCookie)
|
|
||||||
val (list, total) = flow.fetchHistory(
|
|
||||||
session = session,
|
session = session,
|
||||||
accountDisplayName = state.account.accountBriefName,
|
accountDisplayName = state.account.accountBriefName,
|
||||||
accountNumber = state.account.accountNumber,
|
accountNumber = state.account.accountNumber,
|
||||||
|
|||||||
@@ -18,14 +18,17 @@ import kotlinx.coroutines.withContext
|
|||||||
import sh.sar.basedbank.util.Totp
|
import sh.sar.basedbank.util.Totp
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.bml.BmlAccountClient
|
||||||
import sh.sar.basedbank.api.bml.BmlActivationResult
|
import sh.sar.basedbank.api.bml.BmlActivationResult
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||||
import sh.sar.basedbank.api.bml.BmlOtpChannel
|
import sh.sar.basedbank.api.bml.BmlOtpChannel
|
||||||
import sh.sar.basedbank.api.bml.BmlProfile
|
import sh.sar.basedbank.api.bml.BmlProfile
|
||||||
|
import sh.sar.basedbank.api.fahipay.FahipayAccountClient
|
||||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||||
import sh.sar.basedbank.api.fahipay.FahipaySession
|
import sh.sar.basedbank.api.fahipay.FahipaySession
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||||
|
import sh.sar.basedbank.api.mib.MibProfileClient
|
||||||
import sh.sar.basedbank.util.AccountCache
|
import sh.sar.basedbank.util.AccountCache
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.databinding.FragmentCredentialsBinding
|
import sh.sar.basedbank.databinding.FragmentCredentialsBinding
|
||||||
@@ -56,7 +59,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
// BML multi-profile state
|
// BML multi-profile state
|
||||||
private var bmlFlow: BmlLoginFlow? = null
|
private var bmlFlow: BmlLoginFlow? = null
|
||||||
private var bmlLoginId: String = ""
|
private var bmlLoginId: String = ""
|
||||||
private var bmlAccumulatedAccounts = mutableListOf<MibAccount>()
|
private var bmlAccumulatedAccounts = mutableListOf<BankAccount>()
|
||||||
private var bmlPendingBusinessProfiles = ArrayDeque<Pair<BmlProfile, List<BmlOtpChannel>>>()
|
private var bmlPendingBusinessProfiles = ArrayDeque<Pair<BmlProfile, List<BmlOtpChannel>>>()
|
||||||
private var bmlCurrentBusinessProfile: BmlProfile? = null
|
private var bmlCurrentBusinessProfile: BmlProfile? = null
|
||||||
private var bmlSelectedChannel: String? = null
|
private var bmlSelectedChannel: String? = null
|
||||||
@@ -202,7 +205,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
store.saveMibCredentials(loginId, username, passwordHash, otpSeed)
|
store.saveMibCredentials(loginId, username, passwordHash, otpSeed)
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
flow.lastSession?.let { s ->
|
flow.lastSession?.let { s ->
|
||||||
val profile = flow.fetchPersonalProfile(s)
|
val profile = MibProfileClient().fetchPersonalProfile(s)
|
||||||
if (profile != null) store.saveMibUserProfile(
|
if (profile != null) store.saveMibUserProfile(
|
||||||
loginId,
|
loginId,
|
||||||
CredentialStore.MibUserProfile(
|
CredentialStore.MibUserProfile(
|
||||||
@@ -428,7 +431,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
val anySession = app.anyBmlSessionFor(bmlLoginId)
|
val anySession = app.anyBmlSessionFor(bmlLoginId)
|
||||||
if (anySession != null) {
|
if (anySession != null) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val info = BmlLoginFlow().fetchUserInfo(anySession)
|
val info = BmlAccountClient().fetchUserInfo(anySession)
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
store.saveBmlUserProfile(
|
store.saveBmlUserProfile(
|
||||||
bmlLoginId,
|
bmlLoginId,
|
||||||
@@ -582,13 +585,13 @@ class CredentialsFragment : Fragment() {
|
|||||||
store: CredentialStore
|
store: CredentialStore
|
||||||
) {
|
) {
|
||||||
val (profile, balance) = withContext(Dispatchers.IO) {
|
val (profile, balance) = withContext(Dispatchers.IO) {
|
||||||
val p = flow.fetchProfile(session)
|
val p = FahipayAccountClient().fetchProfile(session)
|
||||||
val b = flow.fetchBalance(session)
|
val b = FahipayAccountClient().fetchBalance(session)
|
||||||
Pair(p, b)
|
Pair(p, b)
|
||||||
}
|
}
|
||||||
val loginId = profile.profileId
|
val loginId = profile.profileId
|
||||||
val loginTag = "fahipay_$loginId"
|
val loginTag = "fahipay_$loginId"
|
||||||
val account = flow.buildAccount(profile, balance, loginTag)
|
val account = FahipayAccountClient().buildAccount(profile, balance, loginTag)
|
||||||
store.saveFahipayCredentials(loginId, idCard, password)
|
store.saveFahipayCredentials(loginId, idCard, password)
|
||||||
store.saveFahipaySession(loginId, session.authId, session.sessionCookie)
|
store.saveFahipaySession(loginId, session.authId, session.sessionCookie)
|
||||||
store.saveFahipayUserProfile(
|
store.saveFahipayUserProfile(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package sh.sar.basedbank.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
|
||||||
object AccountCache {
|
object AccountCache {
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ object AccountCache {
|
|||||||
private fun bmlKey(loginId: String) = "bml_accounts_$loginId"
|
private fun bmlKey(loginId: String) = "bml_accounts_$loginId"
|
||||||
private fun fahipayKey(loginId: String) = "fahipay_accounts_$loginId"
|
private fun fahipayKey(loginId: String) = "fahipay_accounts_$loginId"
|
||||||
|
|
||||||
fun save(context: Context, accounts: List<MibAccount>) {
|
fun save(context: Context, accounts: List<BankAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (acc in accounts) {
|
for (acc in accounts) {
|
||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
@@ -38,7 +38,7 @@ object AccountCache {
|
|||||||
.edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveBml(context: Context, loginId: String, accounts: List<MibAccount>) {
|
fun saveBml(context: Context, loginId: String, accounts: List<BankAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (acc in accounts) {
|
for (acc in accounts) {
|
||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
@@ -61,14 +61,14 @@ object AccountCache {
|
|||||||
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBml(context: Context, loginId: String): List<MibAccount> {
|
fun loadBml(context: Context, loginId: String): List<BankAccount> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(bmlKey(loginId), null) ?: return emptyList()
|
.getString(bmlKey(loginId), null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibAccount(
|
BankAccount(
|
||||||
bank = "BML",
|
bank = "BML",
|
||||||
profileName = o.optString("profileName"),
|
profileName = o.optString("profileName"),
|
||||||
profileType = o.optString("profileType"),
|
profileType = o.optString("profileType"),
|
||||||
@@ -89,10 +89,10 @@ object AccountCache {
|
|||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBml(context: Context, loginIds: List<String>): List<MibAccount> =
|
fun loadBml(context: Context, loginIds: List<String>): List<BankAccount> =
|
||||||
loginIds.flatMap { loadBml(context, it) }
|
loginIds.flatMap { loadBml(context, it) }
|
||||||
|
|
||||||
fun saveFahipay(context: Context, loginId: String, accounts: List<MibAccount>) {
|
fun saveFahipay(context: Context, loginId: String, accounts: List<BankAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (acc in accounts) {
|
for (acc in accounts) {
|
||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
@@ -115,14 +115,14 @@ object AccountCache {
|
|||||||
.edit().putString(fahipayKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(fahipayKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipay(context: Context, loginId: String): List<MibAccount> {
|
fun loadFahipay(context: Context, loginId: String): List<BankAccount> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(fahipayKey(loginId), null) ?: return emptyList()
|
.getString(fahipayKey(loginId), null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibAccount(
|
BankAccount(
|
||||||
bank = "FAHIPAY",
|
bank = "FAHIPAY",
|
||||||
profileName = o.optString("profileName"),
|
profileName = o.optString("profileName"),
|
||||||
profileType = o.optString("profileType"),
|
profileType = o.optString("profileType"),
|
||||||
@@ -143,14 +143,14 @@ object AccountCache {
|
|||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipay(context: Context, loginIds: List<String>): List<MibAccount> =
|
fun loadFahipay(context: Context, loginIds: List<String>): List<BankAccount> =
|
||||||
loginIds.flatMap { loadFahipay(context, it) }
|
loginIds.flatMap { loadFahipay(context, it) }
|
||||||
|
|
||||||
fun clear(context: Context) {
|
fun clear(context: Context) {
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load(context: Context): List<MibAccount> {
|
fun load(context: Context): List<BankAccount> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_MIB, null) ?: return emptyList()
|
.getString(KEY_MIB, null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
@@ -158,7 +158,7 @@ object AccountCache {
|
|||||||
val arr = JSONArray(json)
|
val arr = JSONArray(json)
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibAccount(
|
BankAccount(
|
||||||
bank = o.optString("bank", "MIB"),
|
bank = o.optString("bank", "MIB"),
|
||||||
profileName = o.optString("profileName"),
|
profileName = o.optString("profileName"),
|
||||||
profileType = o.optString("profileType"),
|
profileType = o.optString("profileType"),
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package sh.sar.basedbank.util
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.util.bmlapi.BmlHistoryParser
|
import sh.sar.basedbank.util.bmlapi.BmlHistoryParser
|
||||||
import sh.sar.basedbank.util.fahipayapi.FahipayHistoryParser
|
import sh.sar.basedbank.util.fahipayapi.FahipayHistoryParser
|
||||||
import sh.sar.basedbank.util.mibapi.MibHistoryParser
|
import sh.sar.basedbank.util.mibapi.MibHistoryParser
|
||||||
|
|
||||||
object AccountHistoryParser {
|
object AccountHistoryParser {
|
||||||
|
|
||||||
fun from(account: MibAccount): AccountHistoryDisplay? = when (account.bank) {
|
fun from(account: BankAccount): AccountHistoryDisplay? = when (account.bank) {
|
||||||
"BML" -> BmlHistoryParser.displayData(account)
|
"BML" -> BmlHistoryParser.displayData(account)
|
||||||
"FAHIPAY" -> FahipayHistoryParser.displayData(account)
|
"FAHIPAY" -> FahipayHistoryParser.displayData(account)
|
||||||
"MIB" -> MibHistoryParser.displayData(account)
|
"MIB" -> MibHistoryParser.displayData(account)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package sh.sar.basedbank.util
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.util.bmlapi.BmlDashboardParser
|
import sh.sar.basedbank.util.bmlapi.BmlDashboardParser
|
||||||
import sh.sar.basedbank.util.fahipayapi.FahipayAccountParser
|
import sh.sar.basedbank.util.fahipayapi.FahipayAccountParser
|
||||||
import sh.sar.basedbank.util.mibapi.MibAccountParser
|
import sh.sar.basedbank.util.mibapi.MibAccountParser
|
||||||
|
|
||||||
object AccountListParser {
|
object AccountListParser {
|
||||||
|
|
||||||
fun from(account: MibAccount): AccountListDisplay? = when (account.bank) {
|
fun from(account: BankAccount): AccountListDisplay? = when (account.bank) {
|
||||||
"BML" -> BmlDashboardParser.displayData(account)
|
"BML" -> BmlDashboardParser.displayData(account)
|
||||||
"FAHIPAY" -> FahipayAccountParser.displayData(account)
|
"FAHIPAY" -> FahipayAccountParser.displayData(account)
|
||||||
"MIB" -> MibAccountParser.displayData(account)
|
"MIB" -> MibAccountParser.displayData(account)
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package sh.sar.basedbank.util
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.util.bmlapi.BmlContactParser
|
import sh.sar.basedbank.util.bmlapi.BmlContactParser
|
||||||
import sh.sar.basedbank.util.fahipayapi.FahipayContactParser
|
import sh.sar.basedbank.util.fahipayapi.FahipayContactParser
|
||||||
import sh.sar.basedbank.util.mibapi.MibContactParser
|
import sh.sar.basedbank.util.mibapi.MibContactParser
|
||||||
|
|
||||||
object ContactListParser {
|
object ContactListParser {
|
||||||
|
|
||||||
fun from(contact: MibBeneficiary): ContactDisplay? = when {
|
fun from(contact: BankContact): ContactDisplay? = when {
|
||||||
contact.benefCategoryId == "BML" -> BmlContactParser.displayData(contact)
|
contact.benefCategoryId == "BML" -> BmlContactParser.displayData(contact)
|
||||||
contact.benefType == "FAHIPAY" -> FahipayContactParser.displayData(contact)
|
contact.benefType == "FAHIPAY" -> FahipayContactParser.displayData(contact)
|
||||||
contact.benefType in setOf("I", "L", "S") -> MibContactParser.displayData(contact)
|
contact.benefType in setOf("I", "L", "S") -> MibContactParser.displayData(contact)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromList(contacts: List<MibBeneficiary>): List<ContactDisplay> = contacts.mapNotNull { from(it) }
|
fun fromList(contacts: List<BankContact>): List<ContactDisplay> = contacts.mapNotNull { from(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package sh.sar.basedbank.util
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,7 +21,7 @@ object ContactManager {
|
|||||||
private fun deleteBml(contact: ContactDisplay, app: BasedBankApp): Boolean {
|
private fun deleteBml(contact: ContactDisplay, app: BasedBankApp): Boolean {
|
||||||
val sess = app.bmlSessions[contact.profileId] ?: app.anyBmlSession() ?: return false
|
val sess = app.bmlSessions[contact.profileId] ?: app.anyBmlSession() ?: return false
|
||||||
val contactId = contact.id.removePrefix("bml_")
|
val contactId = contact.id.removePrefix("bml_")
|
||||||
return try { BmlLoginFlow().deleteContact(sess, contactId) } catch (_: Exception) { false }
|
return try { BmlContactsClient().deleteContact(sess, contactId) } catch (_: Exception) { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteMib(contact: ContactDisplay, app: BasedBankApp): Boolean {
|
private fun deleteMib(contact: ContactDisplay, app: BasedBankApp): Boolean {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package sh.sar.basedbank.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
import sh.sar.basedbank.api.models.BankContactCategory
|
||||||
|
|
||||||
object ContactsCache {
|
object ContactsCache {
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ object ContactsCache {
|
|||||||
|
|
||||||
fun save(
|
fun save(
|
||||||
context: Context,
|
context: Context,
|
||||||
contacts: List<MibBeneficiary>,
|
contacts: List<BankContact>,
|
||||||
categories: List<MibBeneficiaryCategory>
|
categories: List<BankContactCategory>
|
||||||
) {
|
) {
|
||||||
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ object ContactsCache {
|
|||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadContacts(context: Context): List<MibBeneficiary> {
|
fun loadContacts(context: Context): List<BankContact> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_CONTACTS, null) ?: return emptyList()
|
.getString(KEY_CONTACTS, null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
@@ -63,7 +63,7 @@ object ContactsCache {
|
|||||||
val arr = JSONArray(json)
|
val arr = JSONArray(json)
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibBeneficiary(
|
BankContact(
|
||||||
benefNo = o.optString("benefNo"),
|
benefNo = o.optString("benefNo"),
|
||||||
benefName = o.optString("benefName"),
|
benefName = o.optString("benefName"),
|
||||||
benefNickName = o.optString("benefNickName"),
|
benefNickName = o.optString("benefNickName"),
|
||||||
@@ -86,7 +86,7 @@ object ContactsCache {
|
|||||||
|
|
||||||
private fun bmlKey(loginId: String) = "bml_contacts_$loginId"
|
private fun bmlKey(loginId: String) = "bml_contacts_$loginId"
|
||||||
|
|
||||||
fun saveBml(context: Context, loginId: String, contacts: List<MibBeneficiary>) {
|
fun saveBml(context: Context, loginId: String, contacts: List<BankContact>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (c in contacts) {
|
for (c in contacts) {
|
||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
@@ -108,14 +108,14 @@ object ContactsCache {
|
|||||||
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBml(context: Context, loginId: String): List<MibBeneficiary> {
|
fun loadBml(context: Context, loginId: String): List<BankContact> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(bmlKey(loginId), null) ?: return emptyList()
|
.getString(bmlKey(loginId), null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibBeneficiary(
|
BankContact(
|
||||||
benefNo = o.optString("benefNo"),
|
benefNo = o.optString("benefNo"),
|
||||||
benefName = o.optString("benefName"),
|
benefName = o.optString("benefName"),
|
||||||
benefNickName = o.optString("benefNickName"),
|
benefNickName = o.optString("benefNickName"),
|
||||||
@@ -134,10 +134,10 @@ object ContactsCache {
|
|||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBml(context: Context, loginIds: List<String>): List<MibBeneficiary> =
|
fun loadBml(context: Context, loginIds: List<String>): List<BankContact> =
|
||||||
loginIds.flatMap { loadBml(context, it) }
|
loginIds.flatMap { loadBml(context, it) }
|
||||||
|
|
||||||
fun saveFahipay(context: Context, contacts: List<MibBeneficiary>, categories: List<MibBeneficiaryCategory>) {
|
fun saveFahipay(context: Context, contacts: List<BankContact>, categories: List<BankContactCategory>) {
|
||||||
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (c in contacts) arr.put(JSONObject().apply {
|
for (c in contacts) arr.put(JSONObject().apply {
|
||||||
@@ -159,14 +159,14 @@ object ContactsCache {
|
|||||||
prefs.apply()
|
prefs.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipay(context: Context): List<MibBeneficiary> {
|
fun loadFahipay(context: Context): List<BankContact> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString("fahipay_contacts", null) ?: return emptyList()
|
.getString("fahipay_contacts", null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibBeneficiary(
|
BankContact(
|
||||||
benefNo = o.optString("benefNo"),
|
benefNo = o.optString("benefNo"),
|
||||||
benefName = "",
|
benefName = "",
|
||||||
benefNickName = o.optString("benefNickName"),
|
benefNickName = o.optString("benefNickName"),
|
||||||
@@ -185,19 +185,19 @@ object ContactsCache {
|
|||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipayCategories(context: Context): List<MibBeneficiaryCategory> {
|
fun loadFahipayCategories(context: Context): List<BankContactCategory> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString("fahipay_categories", null) ?: return emptyList()
|
.getString("fahipay_categories", null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibBeneficiaryCategory(o.optString("id"), o.optString("categoryName"), o.optInt("numBenef"))
|
BankContactCategory(o.optString("id"), o.optString("categoryName"), o.optInt("numBenef"))
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadCategories(context: Context): List<MibBeneficiaryCategory> {
|
fun loadCategories(context: Context): List<BankContactCategory> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_CATEGORIES, null) ?: return emptyList()
|
.getString(KEY_CATEGORIES, null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
@@ -205,7 +205,7 @@ object ContactsCache {
|
|||||||
val arr = JSONArray(json)
|
val arr = JSONArray(json)
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibBeneficiaryCategory(
|
BankContactCategory(
|
||||||
id = o.optString("id"),
|
id = o.optString("id"),
|
||||||
categoryName = o.optString("categoryName"),
|
categoryName = o.optString("categoryName"),
|
||||||
numBenef = o.optInt("numBenef", 0)
|
numBenef = o.optInt("numBenef", 0)
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlHistoryClient
|
||||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
import sh.sar.basedbank.api.fahipay.FahipayHistoryClient
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibHistoryClient
|
import sh.sar.basedbank.api.mib.MibHistoryClient
|
||||||
import sh.sar.basedbank.api.mib.Transaction
|
import sh.sar.basedbank.api.models.BankTransaction
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -18,7 +18,7 @@ import java.util.Locale
|
|||||||
* The fragment holds one instance per account and calls [hasMore] / [fetchNextPage]
|
* The fragment holds one instance per account and calls [hasMore] / [fetchNextPage]
|
||||||
* without knowing which bank it is talking to.
|
* without knowing which bank it is talking to.
|
||||||
*/
|
*/
|
||||||
class HistoryFetcher(private val account: MibAccount) {
|
class HistoryFetcher(private val account: BankAccount) {
|
||||||
|
|
||||||
private val isMib get() = account.bank == "MIB"
|
private val isMib get() = account.bank == "MIB"
|
||||||
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||||
@@ -46,18 +46,16 @@ class HistoryFetcher(private val account: MibAccount) {
|
|||||||
else -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
else -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchNextPage(app: BasedBankApp, pageSize: Int = 10): List<Transaction> = when {
|
suspend fun fetchNextPage(app: BasedBankApp, pageSize: Int = 10): List<BankTransaction> = when {
|
||||||
isFahipay -> withContext(Dispatchers.IO) { fetchFahipay(app) }
|
isFahipay -> withContext(Dispatchers.IO) { fetchFahipay(app) }
|
||||||
isMib -> app.mibMutex.withLock { withContext(Dispatchers.IO) { fetchMib(app, pageSize) } }
|
isMib -> app.mibMutex.withLock { withContext(Dispatchers.IO) { fetchMib(app, pageSize) } }
|
||||||
isBmlCard -> withContext(Dispatchers.IO) { fetchBmlCard(app) }
|
isBmlCard -> withContext(Dispatchers.IO) { fetchBmlCard(app) }
|
||||||
else -> withContext(Dispatchers.IO) { fetchBmlCasa(app) }
|
else -> withContext(Dispatchers.IO) { fetchBmlCasa(app) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchFahipay(app: BasedBankApp): List<Transaction> {
|
private fun fetchFahipay(app: BasedBankApp): List<BankTransaction> {
|
||||||
val session = app.fahipaySessionFor(account) ?: return emptyList()
|
val session = app.fahipaySessionFor(account) ?: return emptyList()
|
||||||
val flow = FahipayLoginFlow()
|
val (list, total) = FahipayHistoryClient().fetchHistory(
|
||||||
flow.setSessionCookie(session.sessionCookie)
|
|
||||||
val (list, total) = flow.fetchHistory(
|
|
||||||
session = session,
|
session = session,
|
||||||
accountDisplayName = account.accountBriefName,
|
accountDisplayName = account.accountBriefName,
|
||||||
accountNumber = account.accountNumber,
|
accountNumber = account.accountNumber,
|
||||||
@@ -68,7 +66,7 @@ class HistoryFetcher(private val account: MibAccount) {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchMib(app: BasedBankApp, pageSize: Int): List<Transaction> {
|
private fun fetchMib(app: BasedBankApp, pageSize: Int): List<BankTransaction> {
|
||||||
val loginId = account.loginTag.removePrefix("mib_")
|
val loginId = account.loginTag.removePrefix("mib_")
|
||||||
val session = app.mibSessions[loginId] ?: return emptyList()
|
val session = app.mibSessions[loginId] ?: return emptyList()
|
||||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
@@ -86,13 +84,13 @@ class HistoryFetcher(private val account: MibAccount) {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchBmlCard(app: BasedBankApp): List<Transaction> {
|
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()
|
||||||
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 BmlLoginFlow().fetchCardHistory(
|
return BmlHistoryClient().fetchCardHistory(
|
||||||
session = session,
|
session = session,
|
||||||
cardId = account.internalId,
|
cardId = account.internalId,
|
||||||
accountDisplayName = account.accountBriefName,
|
accountDisplayName = account.accountBriefName,
|
||||||
@@ -101,9 +99,9 @@ class HistoryFetcher(private val account: MibAccount) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchBmlCasa(app: BasedBankApp): List<Transaction> {
|
private fun fetchBmlCasa(app: BasedBankApp): List<BankTransaction> {
|
||||||
val session = app.bmlSessionFor(account) ?: return emptyList()
|
val session = app.bmlSessionFor(account) ?: return emptyList()
|
||||||
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
|
val (list, totalPages) = BmlHistoryClient().fetchAccountHistory(
|
||||||
session = session,
|
session = session,
|
||||||
accountId = account.internalId,
|
accountId = account.internalId,
|
||||||
accountDisplayName = account.accountBriefName,
|
accountDisplayName = account.accountBriefName,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package sh.sar.basedbank.util.bmlapi
|
package sh.sar.basedbank.util.bmlapi
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.util.ContactDisplay
|
import sh.sar.basedbank.util.ContactDisplay
|
||||||
import sh.sar.basedbank.util.TransferNetwork
|
import sh.sar.basedbank.util.TransferNetwork
|
||||||
|
|
||||||
object BmlContactParser {
|
object BmlContactParser {
|
||||||
|
|
||||||
fun displayData(contact: MibBeneficiary) = ContactDisplay(
|
fun displayData(contact: BankContact) = ContactDisplay(
|
||||||
id = contact.benefNo,
|
id = contact.benefNo,
|
||||||
name = contact.benefNickName,
|
name = contact.benefNickName,
|
||||||
realName = contact.benefName,
|
realName = contact.benefName,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package sh.sar.basedbank.util.bmlapi
|
package sh.sar.basedbank.util.bmlapi
|
||||||
|
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.util.AccountListDisplay
|
import sh.sar.basedbank.util.AccountListDisplay
|
||||||
|
|
||||||
object BmlDashboardParser {
|
object BmlDashboardParser {
|
||||||
@@ -10,7 +10,7 @@ object BmlDashboardParser {
|
|||||||
* Returns all display fields for an account/card row in the accounts list.
|
* Returns all display fields for an account/card row in the accounts list.
|
||||||
* Handles both BML CASA accounts and BML prepaid/credit cards.
|
* Handles both BML CASA accounts and BML prepaid/credit cards.
|
||||||
*/
|
*/
|
||||||
fun displayData(account: MibAccount): AccountListDisplay {
|
fun displayData(account: BankAccount): AccountListDisplay {
|
||||||
val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||||
return if (isCard) {
|
return if (isCard) {
|
||||||
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
||||||
@@ -51,7 +51,7 @@ object BmlDashboardParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Balance shown in the accounts list — ledger (working) balance for BML CASA. */
|
/** Balance shown in the accounts list — ledger (working) balance for BML CASA. */
|
||||||
fun listBalance(account: MibAccount): String =
|
fun listBalance(account: BankAccount): String =
|
||||||
"${account.currencyName} ${account.currentBalance}"
|
"${account.currencyName} ${account.currentBalance}"
|
||||||
|
|
||||||
fun cardBrandIcon(productName: String): Int = when {
|
fun cardBrandIcon(productName: String): Int = when {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package sh.sar.basedbank.util.bmlapi
|
package sh.sar.basedbank.util.bmlapi
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.util.AccountHistoryDisplay
|
import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||||
|
|
||||||
object BmlHistoryParser {
|
object BmlHistoryParser {
|
||||||
|
|
||||||
fun displayData(account: MibAccount): AccountHistoryDisplay {
|
fun displayData(account: BankAccount): AccountHistoryDisplay {
|
||||||
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
|
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
|
||||||
return AccountHistoryDisplay(
|
return AccountHistoryDisplay(
|
||||||
name = account.accountBriefName,
|
name = account.accountBriefName,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package sh.sar.basedbank.util.fahipayapi
|
package sh.sar.basedbank.util.fahipayapi
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.util.AccountListDisplay
|
import sh.sar.basedbank.util.AccountListDisplay
|
||||||
|
|
||||||
object FahipayAccountParser {
|
object FahipayAccountParser {
|
||||||
|
|
||||||
fun displayData(account: MibAccount) = AccountListDisplay(
|
fun displayData(account: BankAccount) = AccountListDisplay(
|
||||||
name = account.accountBriefName,
|
name = account.accountBriefName,
|
||||||
number = account.accountNumber,
|
number = account.accountNumber,
|
||||||
typeLabel = account.accountTypeName,
|
typeLabel = account.accountTypeName,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package sh.sar.basedbank.util.fahipayapi
|
package sh.sar.basedbank.util.fahipayapi
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.util.ContactDisplay
|
import sh.sar.basedbank.util.ContactDisplay
|
||||||
import sh.sar.basedbank.util.TransferNetwork
|
import sh.sar.basedbank.util.TransferNetwork
|
||||||
|
|
||||||
object FahipayContactParser {
|
object FahipayContactParser {
|
||||||
|
|
||||||
fun displayData(contact: MibBeneficiary) = ContactDisplay(
|
fun displayData(contact: BankContact) = ContactDisplay(
|
||||||
id = contact.benefNo,
|
id = contact.benefNo,
|
||||||
name = contact.benefNickName,
|
name = contact.benefNickName,
|
||||||
realName = contact.benefName,
|
realName = contact.benefName,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package sh.sar.basedbank.util.fahipayapi
|
package sh.sar.basedbank.util.fahipayapi
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.util.AccountHistoryDisplay
|
import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||||
|
|
||||||
object FahipayHistoryParser {
|
object FahipayHistoryParser {
|
||||||
|
|
||||||
fun displayData(account: MibAccount) = AccountHistoryDisplay(
|
fun displayData(account: BankAccount) = AccountHistoryDisplay(
|
||||||
name = account.accountBriefName,
|
name = account.accountBriefName,
|
||||||
number = account.accountNumber,
|
number = account.accountNumber,
|
||||||
bankPill = "FP",
|
bankPill = "FP",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package sh.sar.basedbank.util.mibapi
|
package sh.sar.basedbank.util.mibapi
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.util.AccountListDisplay
|
import sh.sar.basedbank.util.AccountListDisplay
|
||||||
|
|
||||||
object MibAccountParser {
|
object MibAccountParser {
|
||||||
|
|
||||||
fun displayData(account: MibAccount) = AccountListDisplay(
|
fun displayData(account: BankAccount) = AccountListDisplay(
|
||||||
name = account.accountBriefName,
|
name = account.accountBriefName,
|
||||||
number = account.accountNumber,
|
number = account.accountNumber,
|
||||||
typeLabel = productLabel(account.accountTypeName),
|
typeLabel = productLabel(account.accountTypeName),
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package sh.sar.basedbank.util.mibapi
|
package sh.sar.basedbank.util.mibapi
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.util.ContactDisplay
|
import sh.sar.basedbank.util.ContactDisplay
|
||||||
import sh.sar.basedbank.util.TransferNetwork
|
import sh.sar.basedbank.util.TransferNetwork
|
||||||
|
|
||||||
object MibContactParser {
|
object MibContactParser {
|
||||||
|
|
||||||
fun displayData(contact: MibBeneficiary): ContactDisplay {
|
fun displayData(contact: BankContact): ContactDisplay {
|
||||||
val network = when (contact.benefType) {
|
val network = when (contact.benefType) {
|
||||||
"I" -> TransferNetwork.MIB
|
"I" -> TransferNetwork.MIB
|
||||||
"S" -> TransferNetwork.SWIFT
|
"S" -> TransferNetwork.SWIFT
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package sh.sar.basedbank.util.mibapi
|
package sh.sar.basedbank.util.mibapi
|
||||||
|
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.util.AccountHistoryDisplay
|
import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||||
|
|
||||||
object MibHistoryParser {
|
object MibHistoryParser {
|
||||||
|
|
||||||
fun displayData(account: MibAccount) = AccountHistoryDisplay(
|
fun displayData(account: BankAccount) = AccountHistoryDisplay(
|
||||||
name = account.accountBriefName,
|
name = account.accountBriefName,
|
||||||
number = account.accountNumber,
|
number = account.accountNumber,
|
||||||
bankPill = null, // MIB has no bank pill
|
bankPill = null, // MIB has no bank pill
|
||||||
|
|||||||
Reference in New Issue
Block a user