Using now BankFinder library

This commit is contained in:
dankito 2024-10-18 06:48:47 +02:00
parent b5116604c1
commit 0c87d99d77
8 changed files with 19 additions and 145 deletions

View File

@ -78,6 +78,7 @@ kotlin {
implementation(libs.banking.client.model)
implementation(libs.fints4k.banking.client)
implementation(libs.bank.finder)
implementation(libs.epcqrcode)
implementation(libs.kcsv)

View File

@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.BankViewInfo
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.BankInfo
import net.dankito.banking.bankfinder.BankInfo
import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper
private val bankIconService = DI.bankIconService
@ -31,7 +31,7 @@ private val bankingGroupMapper = BankingGroupMapper()
@Composable
fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) {
val iconUrl by remember(bank.bic) { mutableStateOf(bankIconService.findIconForBank(bank.name, bank.bic, bankingGroupMapper.getBankingGroup(bank.name, bank.bic))) }
val iconUrl by remember(bank.bic) { mutableStateOf(bankIconService.findIconForBank(bank.name, bank.bic, bankingGroupMapper.getBankingGroup(bank.name, bank.bic ?: ""))) }
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
}

View File

@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.*
@ -23,7 +22,7 @@ import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.extensions.ImeNext
import net.codinux.banking.ui.forms.*
import net.codinux.banking.ui.forms.OutlinedTextField
import net.codinux.banking.ui.model.BankInfo
import net.dankito.banking.bankfinder.BankInfo
import net.codinux.log.Log
@ -143,7 +142,7 @@ fun AddAccountDialog(
}
Row(Modifier.fillMaxWidth().padding(top = 6.dp)) {
Text(bank.domesticBankCode, color = textColor)
Text(bank.bankCode, color = textColor)
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f).padding(start = 8.dp), color = if (supportsFinTs) Color.Gray else textColor)
}

View File

@ -1,27 +0,0 @@
package net.codinux.banking.ui.model
import net.codinux.banking.client.model.BankingGroup
import kotlinx.serialization.Serializable
@Serializable
class BankInfo(
val name: String,
val domesticBankCode: String,
val bic: String = "",
val postalCode: String,
val city: String,
val pinTanAddress: String? = null,
val pinTanVersion: String? = null,
val bankingGroup: BankingGroup? = null,
val branchesInOtherCities: List<String> = listOf() // to have only one entry per bank its branches' cities are now stored in branchesInOtherCities so that branches' cities are still searchable
) {
val supportsPinTan: Boolean
get() = pinTanAddress.isNullOrEmpty() == false
val supportsFinTs3_0: Boolean
get() = pinTanVersion == "FinTS V3.0"
override fun toString() = "$domesticBankCode $name $city"
}

View File

@ -1,5 +1,7 @@
package net.codinux.banking.ui.model
import net.dankito.banking.bankfinder.BankInfo
data class RecipientSuggestion(
val name: String,
val bankIdentifier: String?,

View File

@ -1,119 +1,16 @@
package net.codinux.banking.ui.service
import bankmeister.composeapp.generated.resources.Res
import kotlinx.serialization.json.Json
import net.codinux.banking.ui.model.BankInfo
import org.jetbrains.compose.resources.ExperimentalResourceApi
import net.dankito.banking.bankfinder.BankInfo
import net.dankito.banking.bankfinder.InMemoryBankFinder
class BankFinder {
private lateinit var bankList: List<BankInfo>
private val bankFinder by lazy { InMemoryBankFinder() }
suspend fun getBankList(maxItems: Int? = null): List<BankInfo> {
if (this::bankList.isInitialized == false) {
bankList = loadBankList()
}
fun findBankByNameBicBankCodeOrCity(query: String?, maxItems: Int? = null): List<BankInfo> =
bankFinder.findBankByNameBicBankCodeOrCity(query, maxItems)
return bankList.take(maxItems ?: Int.MAX_VALUE)
}
suspend fun findBankByNameBicBankCodeOrCity(query: String?, maxItems: Int? = null): List<BankInfo> {
if (query.isNullOrBlank()) {
return getBankList(maxItems)
}
query.toIntOrNull()?.let { // if query is an integer, then it can only be an bank code, but not a bank name or city
return findBankByBankCode(query, maxItems)
}
// we already checked for BankCode above, so there's no need to do this again in method below
return findBankByNameBicOrCityForNonEmptyQuery(query, maxItems)
}
suspend fun findBankByBankCode(query: String, maxItems: Int?): List<BankInfo> {
if (query.isEmpty()) {
return getBankList(maxItems)
}
return getBankList().asSequence().filter { it.domesticBankCode.startsWith(query) }
.max(maxItems)
}
suspend fun findBankByBicOrIban(bic: String?, iban: String): BankInfo? {
if (bic == null || iban.length < 9) {
return null
}
return findBankByBic(bic) ?: findBankByIban(iban)
}
suspend fun findBankByBic(bic: String): BankInfo? {
if (bic.length != 8 && bic.length != 11) {
return null
}
val result = getBankList().asSequence().filter { it.bic == bic || (bic.length == 8 && it.bic.startsWith(bic)) }.max(2)
return if (result.size > 1) { // non unique result, but should actually never happen for BICs
null
} else {
result.firstOrNull()
}
}
suspend fun findBankByIban(iban: String): BankInfo? {
if (iban.length < 9) {
return null
}
val bankCode = iban.substring(4) // first two letters are the country code, third and fourth char are the checksum, bank code starts at 5th char
val result = getBankList().asSequence().filter { it.domesticBankCode.startsWith(bankCode) }.max(2)
return if (result.size > 1) { // non unique result, but should actually never happen for BICs
null
} else {
result.firstOrNull()
}
}
private suspend fun findBankByNameBicOrCityForNonEmptyQuery(query: String, maxItems: Int?): List<BankInfo> {
val queryPartsLowerCase = query.lowercase().split(" ", "-")
return getBankList().asSequence().filter { bankInfo ->
checkIfAllQueryPartsMatchBankNameBicOrCity(queryPartsLowerCase, bankInfo)
}
.max(maxItems)
}
private fun checkIfAllQueryPartsMatchBankNameBicOrCity(queryPartsLowerCase: List<String>, bankInfo: BankInfo): Boolean {
for (queryPartLowerCase in queryPartsLowerCase) {
if (checkIfQueryMatchesBankNameBicOrCity(bankInfo, queryPartLowerCase) == false) {
return false
}
}
return true
}
private fun checkIfQueryMatchesBankNameBicOrCity(bankInfo: BankInfo, queryLowerCase: String): Boolean {
return bankInfo.name.contains(queryLowerCase, true)
|| bankInfo.bic.startsWith(queryLowerCase, true)
|| bankInfo.city.startsWith(queryLowerCase, true)
|| bankInfo.branchesInOtherCities.any { it.startsWith(queryLowerCase, true) }
}
fun Sequence<BankInfo>.max(maxItems: Int? = null): List<BankInfo> =
this.take(maxItems ?: Int.MAX_VALUE)
.toList()
@OptIn(ExperimentalResourceApi::class)
private suspend fun loadBankList(): List<BankInfo> {
val json = Res.readBytes("files/BankList.json").decodeToString()
return Json.decodeFromString(json)
}
fun findBankByBicOrIban(bic: String?, iban: String): BankInfo? =
bankFinder.findBankByBicOrIban(bic, iban)
}

View File

@ -25,7 +25,7 @@ import net.codinux.banking.persistence.entities.HoldingEntity
import net.codinux.banking.persistence.entities.UiSettingsEntity
import net.codinux.banking.ui.IOorDefault
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.BankInfo
import net.dankito.banking.bankfinder.BankInfo
import net.codinux.banking.ui.model.error.*
import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent
import net.codinux.banking.ui.model.events.TransferredMoneyEvent
@ -190,7 +190,7 @@ class BankingService(
val retrieveTransactions = if (retrieveAllTransactions) RetrieveTransactions.All else RetrieveTransactions.OfLast90Days
val options = GetAccountDataOptions(retrieveTransactions, preferredTanMethods = preferredTanMethods)
val response = client.getAccountDataAsync(GetAccountDataRequest(bank.domesticBankCode, loginName, password, options, mapBankInfo(bank)))
val response = client.getAccountDataAsync(GetAccountDataRequest(bank.bankCode, loginName, password, options, mapBankInfo(bank)))
if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulGetAccountDataResponse(response.data!!)
@ -211,7 +211,7 @@ class BankingService(
private fun mapBankInfo(bank: BankInfo): net.codinux.banking.client.model.BankInfo? =
if (bank.pinTanAddress != null) {
net.codinux.banking.client.model.BankInfo(
bank.name, bank.bic, bank.pinTanAddress, bank.bankingGroup
bank.name, bank.bic ?: "", bank.pinTanAddress!!, bank.bankingGroup?.let { BankingGroup.valueOf(it.name) }
)
} else {
null

View File

@ -3,6 +3,7 @@ kotlin = "2.0.10"
kotlinx-coroutines = "1.8.1"
banking-client = "0.7.1"
bank-finder = "0.5.0-SNAPSHOT"
epcqrcode = "0.5.0"
kcsv = "2.2.0"
@ -35,6 +36,7 @@ camerax = "1.3.4"
[libraries]
banking-client-model = { group = "net.codinux.banking.client", name = "banking-client-model", version.ref = "banking-client" }
fints4k-banking-client = { group = "net.codinux.banking.client", name = "fints4k-banking-client", version.ref = "banking-client" }
bank-finder = { group = "net.codinux.banking", name = "bank-finder", version.ref = "bank-finder" }
epcqrcode = { group = "net.codinux.banking.epcqrcode", name = "epc-qr-code", version.ref = "epcqrcode" }
kcsv = { group = "net.codinux.csv", name = "kcsv", version.ref = "kcsv" }