Compare commits

...

3 Commits

Author SHA1 Message Date
dankito 04e78b042e Displaying also BIC 2024-10-18 06:53:30 +02:00
dankito 0c87d99d77 Using now BankFinder library 2024-10-18 06:48:47 +02:00
dankito b5116604c1 Fixed loading the renamed TanMethodTypes 2024-10-18 05:34:02 +02:00
9 changed files with 40 additions and 148 deletions

View File

@ -37,6 +37,13 @@ expect fun createSqlDriverDriver(dbName: String, schema: SqlSchema<QueryResult.A
open class SqliteBankingRepository : BankingRepository { open class SqliteBankingRepository : BankingRepository {
companion object {
val TanMethodTypesToMigrate = mapOf(
"ChipTanManuell" to TanMethodType.ChipTanManual.name,
"ChipTanFlickercode" to TanMethodType.ChipTanFlickerCode.name
)
}
private val schema = BankmeisterDb.Schema private val schema = BankmeisterDb.Schema
private val sqlDriver = createSqlDriverDriver("Bankmeister.db", schema, 2L) private val sqlDriver = createSqlDriverDriver("Bankmeister.db", schema, 2L)
@ -251,7 +258,7 @@ open class SqliteBankingRepository : BankingRepository {
bankId, bankId,
displayName, displayName,
mapToEnum(type, TanMethodType.entries), mapToEnum(type, TanMethodType.entries, TanMethodTypesToMigrate),
identifier, identifier,
mapToInt(maxTanInputLength), mapToInt(maxTanInputLength),
mapToEnum(allowedTanFormat, AllowedTanFormat.entries), mapToEnum(allowedTanFormat, AllowedTanFormat.entries),
@ -597,7 +604,16 @@ open class SqliteBankingRepository : BankingRepository {
private fun <E : Enum<E>> mapEnum(enum: Enum<E>): String = enum.name private fun <E : Enum<E>> mapEnum(enum: Enum<E>): String = enum.name
private fun <E : Enum<E>> mapToEnum(enumName: String, values: EnumEntries<E>): E = private fun <E : Enum<E>> mapToEnum(enumName: String, values: EnumEntries<E>): E =
values.first { it.name == enumName } try {
values.first { it.name == enumName }
} catch (e: Throwable) {
log.error(e) { "Could not map enumName '$enumName' to ${values.first()::class}"}
throw e
}
private fun <E : Enum<E>> mapToEnum(enumName: String, values: EnumEntries<E>, enumNamesToMigrate: Map<String, String>): E =
mapToEnum(enumNamesToMigrate[enumName] ?: enumName, values)
private fun <E : Enum<E>> mapToEnumNullable(enumName: String, values: EnumEntries<E>): E? { private fun <E : Enum<E>> mapToEnumNullable(enumName: String, values: EnumEntries<E>): E? {
val mapped = values.firstOrNull { it.name == enumName } val mapped = values.firstOrNull { it.name == enumName }

View File

@ -78,6 +78,7 @@ kotlin {
implementation(libs.banking.client.model) implementation(libs.banking.client.model)
implementation(libs.fints4k.banking.client) implementation(libs.fints4k.banking.client)
implementation(libs.bank.finder)
implementation(libs.epcqrcode) implementation(libs.epcqrcode)
implementation(libs.kcsv) 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.BankAccess
import net.codinux.banking.client.model.BankViewInfo import net.codinux.banking.client.model.BankViewInfo
import net.codinux.banking.ui.config.DI 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 import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper
private val bankIconService = DI.bankIconService private val bankIconService = DI.bankIconService
@ -31,7 +31,7 @@ private val bankingGroupMapper = BankingGroupMapper()
@Composable @Composable
fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) { 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) 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.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.* 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.extensions.ImeNext
import net.codinux.banking.ui.forms.* import net.codinux.banking.ui.forms.*
import net.codinux.banking.ui.forms.OutlinedTextField 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 import net.codinux.log.Log
@ -143,9 +142,11 @@ fun AddAccountDialog(
} }
Row(Modifier.fillMaxWidth().padding(top = 6.dp)) { 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) Text((bank.bic ?: "").padEnd(11, ' '), color = textColor, modifier = Modifier.padding(horizontal = 8.dp))
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f), 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 package net.codinux.banking.ui.model
import net.dankito.banking.bankfinder.BankInfo
data class RecipientSuggestion( data class RecipientSuggestion(
val name: String, val name: String,
val bankIdentifier: String?, val bankIdentifier: String?,

View File

@ -1,119 +1,16 @@
package net.codinux.banking.ui.service package net.codinux.banking.ui.service
import bankmeister.composeapp.generated.resources.Res import net.dankito.banking.bankfinder.BankInfo
import kotlinx.serialization.json.Json import net.dankito.banking.bankfinder.InMemoryBankFinder
import net.codinux.banking.ui.model.BankInfo
import org.jetbrains.compose.resources.ExperimentalResourceApi
class BankFinder { class BankFinder {
private lateinit var bankList: List<BankInfo> private val bankFinder by lazy { InMemoryBankFinder() }
suspend fun getBankList(maxItems: Int? = null): List<BankInfo> { fun findBankByNameBicBankCodeOrCity(query: String?, maxItems: Int? = null): List<BankInfo> =
if (this::bankList.isInitialized == false) { bankFinder.findBankByNameBicBankCodeOrCity(query, maxItems)
bankList = loadBankList()
}
return bankList.take(maxItems ?: Int.MAX_VALUE) fun findBankByBicOrIban(bic: String?, iban: String): BankInfo? =
} bankFinder.findBankByBicOrIban(bic, iban)
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)
}
} }

View File

@ -25,7 +25,7 @@ import net.codinux.banking.persistence.entities.HoldingEntity
import net.codinux.banking.persistence.entities.UiSettingsEntity import net.codinux.banking.persistence.entities.UiSettingsEntity
import net.codinux.banking.ui.IOorDefault import net.codinux.banking.ui.IOorDefault
import net.codinux.banking.ui.model.AccountTransactionViewModel 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.error.*
import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent
import net.codinux.banking.ui.model.events.TransferredMoneyEvent import net.codinux.banking.ui.model.events.TransferredMoneyEvent
@ -190,7 +190,7 @@ class BankingService(
val retrieveTransactions = if (retrieveAllTransactions) RetrieveTransactions.All else RetrieveTransactions.OfLast90Days val retrieveTransactions = if (retrieveAllTransactions) RetrieveTransactions.All else RetrieveTransactions.OfLast90Days
val options = GetAccountDataOptions(retrieveTransactions, preferredTanMethods = preferredTanMethods) 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) { if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulGetAccountDataResponse(response.data!!) handleSuccessfulGetAccountDataResponse(response.data!!)
@ -211,7 +211,7 @@ class BankingService(
private fun mapBankInfo(bank: BankInfo): net.codinux.banking.client.model.BankInfo? = private fun mapBankInfo(bank: BankInfo): net.codinux.banking.client.model.BankInfo? =
if (bank.pinTanAddress != null) { if (bank.pinTanAddress != null) {
net.codinux.banking.client.model.BankInfo( 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 { } else {
null null

View File

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