Compare commits
3 Commits
98f15d3a8d
...
04e78b042e
Author | SHA1 | Date |
---|---|---|
dankito | 04e78b042e | |
dankito | 0c87d99d77 | |
dankito | b5116604c1 |
|
@ -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 }
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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?,
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
Loading…
Reference in New Issue