Compare commits
No commits in common. "04e78b042ec301113b0fb831094acfedcf654a39" and "98f15d3a8d41462557951e900decd4e33ed65871" have entirely different histories.
04e78b042e
...
98f15d3a8d
|
@ -37,13 +37,6 @@ 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)
|
||||||
|
@ -258,7 +251,7 @@ open class SqliteBankingRepository : BankingRepository {
|
||||||
bankId,
|
bankId,
|
||||||
|
|
||||||
displayName,
|
displayName,
|
||||||
mapToEnum(type, TanMethodType.entries, TanMethodTypesToMigrate),
|
mapToEnum(type, TanMethodType.entries),
|
||||||
identifier,
|
identifier,
|
||||||
mapToInt(maxTanInputLength),
|
mapToInt(maxTanInputLength),
|
||||||
mapToEnum(allowedTanFormat, AllowedTanFormat.entries),
|
mapToEnum(allowedTanFormat, AllowedTanFormat.entries),
|
||||||
|
@ -604,16 +597,7 @@ 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 =
|
||||||
try {
|
values.first { it.name == enumName }
|
||||||
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,7 +78,6 @@ 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.dankito.banking.bankfinder.BankInfo
|
import net.codinux.banking.ui.model.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,6 +12,7 @@ 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.*
|
||||||
|
@ -22,7 +23,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.dankito.banking.bankfinder.BankInfo
|
import net.codinux.banking.ui.model.BankInfo
|
||||||
import net.codinux.log.Log
|
import net.codinux.log.Log
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,11 +143,9 @@ fun AddAccountDialog(
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth().padding(top = 6.dp)) {
|
Row(Modifier.fillMaxWidth().padding(top = 6.dp)) {
|
||||||
Text(bank.bankCode, color = textColor)
|
Text(bank.domesticBankCode, color = textColor)
|
||||||
|
|
||||||
Text((bank.bic ?: "").padEnd(11, ' '), color = textColor, modifier = Modifier.padding(horizontal = 8.dp))
|
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f).padding(start = 8.dp), color = if (supportsFinTs) Color.Gray else textColor)
|
||||||
|
|
||||||
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f), color = if (supportsFinTs) Color.Gray else textColor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
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,7 +1,5 @@
|
||||||
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,16 +1,119 @@
|
||||||
package net.codinux.banking.ui.service
|
package net.codinux.banking.ui.service
|
||||||
|
|
||||||
import net.dankito.banking.bankfinder.BankInfo
|
import bankmeister.composeapp.generated.resources.Res
|
||||||
import net.dankito.banking.bankfinder.InMemoryBankFinder
|
import kotlinx.serialization.json.Json
|
||||||
|
import net.codinux.banking.ui.model.BankInfo
|
||||||
|
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||||
|
|
||||||
class BankFinder {
|
class BankFinder {
|
||||||
|
|
||||||
private val bankFinder by lazy { InMemoryBankFinder() }
|
private lateinit var bankList: List<BankInfo>
|
||||||
|
|
||||||
fun findBankByNameBicBankCodeOrCity(query: String?, maxItems: Int? = null): List<BankInfo> =
|
suspend fun getBankList(maxItems: Int? = null): List<BankInfo> {
|
||||||
bankFinder.findBankByNameBicBankCodeOrCity(query, maxItems)
|
if (this::bankList.isInitialized == false) {
|
||||||
|
bankList = loadBankList()
|
||||||
|
}
|
||||||
|
|
||||||
fun findBankByBicOrIban(bic: String?, iban: String): BankInfo? =
|
return bankList.take(maxItems ?: Int.MAX_VALUE)
|
||||||
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.dankito.banking.bankfinder.BankInfo
|
import net.codinux.banking.ui.model.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.bankCode, loginName, password, options, mapBankInfo(bank)))
|
val response = client.getAccountDataAsync(GetAccountDataRequest(bank.domesticBankCode, 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?.let { BankingGroup.valueOf(it.name) }
|
bank.name, bank.bic, bank.pinTanAddress, bank.bankingGroup
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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"
|
||||||
|
@ -36,7 +35,6 @@ 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