Compare commits

..

16 Commits

Author SHA1 Message Date
dankito 3c3734d565 Released version 10 / 1.0.0-Alpha-12 of Android app 2024-09-17 15:24:55 +02:00
dankito af99608dd6 Set Min-SDK Version to 23 to include Android 6 (hope it works) 2024-09-17 15:24:26 +02:00
dankito a11c862ecc Set list item header color to Zinc500 2024-09-17 04:15:09 +02:00
dankito f6dfada5bf Using now the appId of the old Bankmeister app to not have to create a new PlayStore entry, and suffixed develop appId with '.develop' 2024-09-17 04:10:43 +02:00
dankito b4ea02bbd8 Added properties for saving window state (but not using them yet) 2024-09-17 03:57:40 +02:00
dankito 2d472d1683 Implemented saving UiSettings 2024-09-17 03:41:53 +02:00
dankito de36a403df Fixed that bankCode has been renamed to domesticBankCode 2024-09-17 02:37:12 +02:00
dankito debae8b7ca Implemented persisting AppSettings 2024-09-17 02:36:49 +02:00
dankito 09a1a41c20 Showing unsupported account types as disabled 2024-09-17 02:33:58 +02:00
dankito 75ccc648c5 Adjusted to new data model that AccountTransaction now has userSetReference and userSetOtherPartyName, and that userSetDisplayName got added to TanMedium, TanMethod and Holding 2024-09-17 00:59:08 +02:00
dankito 34f2fca126 Added indices on bankId and accountId (not senseful in all cases (currently) ) 2024-09-17 00:20:22 +02:00
dankito e883713eba Updated database model to updated data model 2024-09-17 00:08:32 +02:00
dankito 4cd727b5c0 Adjusted to updated model that lists are now val MutableLists 2024-09-16 23:33:56 +02:00
dankito 96c8cf59cd Adjusted to updated model that bankCode has been renamed to domesticBankCode and countryCode got added 2024-09-16 23:21:20 +02:00
dankito d7a9acbe56 Appended ' Dev' to debug Android app name 2024-09-16 23:16:58 +02:00
dankito 9330b72726 Updated to model changes: User has been renamed to BankAccess and bic is now nullable 2024-09-16 23:14:02 +02:00
51 changed files with 698 additions and 399 deletions

View File

@ -137,10 +137,10 @@ android {
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
defaultConfig {
applicationId = "net.codinux.banking.ui"
applicationId = "net.codinux.banking.android" // the appId of the old Bankmeister app to be able to use the old PlayStore entry
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionCode = 10
versionName = "1.0.0-Alpha-12"
}
packaging {
@ -148,18 +148,26 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
named("debug") {
applicationIdSuffix = ".develop"
}
named("release") {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
dependencies {
debugImplementation(compose.uiTooling)
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Bankmeister Dev</string>
</resources>

View File

@ -13,8 +13,8 @@ import net.codinux.banking.ui.model.TanChallengeReceived
@Composable
fun EnterTanDialogPreview_EnterTan() {
val tanMethods = listOf(TanMethod("Zeig mich an", TanMethodType.AppTan, "902"))
val user = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
val tanChallenge = TanChallenge(TanChallengeType.EnterTan, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, user = user)
val bank = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
val tanChallenge = TanChallenge(TanChallengeType.EnterTan, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}
@ -27,9 +27,9 @@ fun EnterTanDialogPreview_TanImage() {
val tanMethod = TanMethod("photoTAN-Verfahren", TanMethodType.photoTan, "902", 6, AllowedTanFormat.Numeric)
val tanImage = TanImage("image/png", tanImageBytes)
val user = BankViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank", BankingGroup.Comdirect)
val bank = BankViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank", BankingGroup.Comdirect)
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethod.identifier, listOf(tanMethod), null, emptyList(), tanImage, null, user)
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethod.identifier, listOf(tanMethod), null, emptyList(), tanImage, null, bank)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}
@ -50,10 +50,10 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
TanMedium(TanMediumType.TanGenerator, "SparkassenCard (Debitkarte)", TanMediumStatus.Used, TanGeneratorTanMedium("5432109876"))
)
val user = BankViewInfo("10010010", "Ihr krasser Login Name", "Eine ganz gewöhnliche Sparkasse", BankingGroup.Sparkasse)
val bank = BankViewInfo("10010010", "Ihr krasser Login Name", "Eine ganz gewöhnliche Sparkasse", BankingGroup.Sparkasse)
val account = BankAccountViewInfo("12345678", null, BankAccountType.CheckingAccount, null, "Giro Konto")
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetTransactions, "Sie möchten eine \"Umsatzabfrage\" freigeben: Bitte bestätigen Sie den \"Startcode 80061030\" mit der Taste \"OK\".", "913", tanMethods, "SparkassenCard (Debitkarte)", tanMedia, tanImage, null, user, account)
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetTransactions, "Sie möchten eine \"Umsatzabfrage\" freigeben: Bitte bestätigen Sie den \"Startcode 80061030\" mit der Taste \"OK\".", "913", tanMethods, "SparkassenCard (Debitkarte)", tanMedia, tanImage, null, bank, account)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}
@ -62,8 +62,8 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
@Composable
fun EnterTanDialogPreview_Flickercode() {
val tanMethods = listOf(TanMethod("chipTAN Flickercode", TanMethodType.ChipTanFlickercode, "902"))
val user = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
val tanChallenge = TanChallenge(TanChallengeType.Flickercode, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, user = user, flickerCode = FlickerCode("", ""))
val bank = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
val tanChallenge = TanChallenge(TanChallengeType.Flickercode, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank, flickerCode = FlickerCode("", ""))
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,30 @@
package net.codinux.banking.dataaccess
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.HoldingEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.settings.UiSettings
interface BankingRepository {
fun getAllUsers(): List<UserEntity>
fun getAppSettings(): AppSettings?
suspend fun persistUser(user: User): UserEntity
suspend fun saveAppSettings(settings: AppSettings)
fun getUiSettings(settings: UiSettings)
suspend fun saveUiSettings(settings: UiSettings)
fun getAllBanks(): List<BankAccessEntity>
suspend fun persistBank(bank: BankAccess): BankAccessEntity
suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity>
@ -29,7 +40,7 @@ interface BankingRepository {
fun getAllAccountTransactions(): List<AccountTransactionEntity>
fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity>
fun getAllTransactionsForBank(bank: BankAccessEntity): List<AccountTransactionEntity>
fun getTransactionById(transactionId: Long): AccountTransactionEntity?

View File

@ -1,31 +1,51 @@
package net.codinux.banking.dataaccess
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.HoldingEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.settings.UiSettings
class InMemoryBankingRepository(
users: Collection<User> = emptyList(),
transactions: Collection<AccountTransaction> = emptyList()
banks: Collection<BankAccess> = emptyList(),
transactions: Collection<AccountTransaction> = emptyList(),
private var appSettings: AppSettings = AppSettings()
) : BankingRepository {
private var nextId = 0L // TODO: make thread-safe
private val users = users.map { map(it) }.toMutableList()
private val banks = banks.map { map(it) }.toMutableList()
private val transactions = transactions.map { map(it) }.toMutableList()
private lateinit var uiSettings: UiSettings
override fun getAllUsers(): List<UserEntity> = users.toList()
override suspend fun persistUser(user: User): UserEntity {
val entity = map(user) // TODO: may fix someday and add also BankAccounts and their id
this.users.add(entity)
override fun getAppSettings(): AppSettings? = appSettings
override suspend fun saveAppSettings(settings: AppSettings) {
this.appSettings = settings
}
override fun getUiSettings(settings: UiSettings) {
this.uiSettings = settings
}
override suspend fun saveUiSettings(settings: UiSettings) {
this.uiSettings = settings
}
override fun getAllBanks(): List<BankAccessEntity> = banks.toList()
override suspend fun persistBank(bank: BankAccess): BankAccessEntity {
val entity = map(bank) // TODO: may fix someday and add also BankAccounts and their id
this.banks.add(entity)
return entity
}
@ -50,27 +70,27 @@ class InMemoryBankingRepository(
override fun getAllAccountTransactions(): List<AccountTransactionEntity> = transactions.toList()
override fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity> =
getAllAccountTransactions().filter { it.userId == user.id }
override fun getAllTransactionsForBank(bank: BankAccessEntity): List<AccountTransactionEntity> =
getAllAccountTransactions().filter { it.bankId == bank.id }
override fun getTransactionById(transactionId: Long): AccountTransactionEntity? =
getAllAccountTransactions().firstOrNull { it.id == transactionId }
private fun map(account: User) = UserEntity(
private fun map(bank: BankAccess) = BankAccessEntity(
nextId++,
account.bankCode, account.loginName, account.password, account.bankName, account.bic, account.customerName, account.userId,
bank.domesticBankCode, bank.loginName, bank.password, bank.bankName, bank.bic, bank.customerName, bank.userId,
// TODO: may fix someday and also add BankAccounts
emptyList(), account.selectedTanMethodIdentifier, emptyList(), account.selectedTanMediumIdentifier, emptyList(),
account.bankingGroup, account.serverAddress,
account.userSetDisplayName, account.displayIndex,
account.iconUrl, account.wrongCredentialsEntered,
emptyList(), bank.selectedTanMethodIdentifier, mutableListOf(), bank.selectedTanMediumIdentifier, mutableListOf(),
bank.bankingGroup, bank.serverAddress, bank.countryCode,
bank.userSetDisplayName, bank.displayIndex,
bank.iconUrl, bank.wrongCredentialsEntered,
)
// TODO: someday may fix and get userId and bankAccountId
private fun map(transaction: AccountTransaction, userId: Long = nextId++, bankAccountId: Long = nextId++) = AccountTransactionEntity(
// TODO: someday may fix and get bankId and accountId
private fun map(transaction: AccountTransaction, bankId: Long = nextId++, accountId: Long = nextId++) = AccountTransactionEntity(
nextId++,
userId, bankAccountId,
bankId, accountId,
transaction.amount, transaction.currency, transaction.reference,
transaction.bookingDate, transaction.valueDate,
transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId,

View File

@ -8,6 +8,10 @@ import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.client.model.tan.*
import net.codinux.banking.dataaccess.entities.*
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.TransactionsGrouping
import net.codinux.banking.ui.model.settings.AppAuthenticationMethod
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.settings.UiSettings
import net.codinux.log.logger
import kotlin.enums.EnumEntries
import kotlin.js.JsName
@ -19,53 +23,90 @@ open class SqliteBankingRepository(
private val database = BankmeisterDb(sqlDriver)
private val userQueries = database.userQueries
private val settingsQueries = database.settingsQueries
private val bankQueries = database.bankQueries
private val accountTransactionQueries = database.accountTransactionQueries
private val log by logger()
override fun getAllUsers(): List<UserEntity> {
val bankAccounts = getAllBankAccounts().groupBy { it.userId }
val tanMethods = getAllTanMethods().groupBy { it.userId }
val tanMedia = getAllTanMedia().groupBy { it.userId }
val holdings = getAllHoldings().groupBy { it.bankAccountId }
override fun getAppSettings(): AppSettings? =
settingsQueries.getAppSettings { _,
authenticationMethod, hashedPassword, updateAccountsOnAppStart, updateAccountsIfNoUpdatedForHours,
sideMenuWidth,
windowPositionX, windowPositionY, windowWidth, windowHeight,
windowState
->
AppSettings(mapToEnum(authenticationMethod, AppAuthenticationMethod.entries), hashedPassword, updateAccountsOnAppStart, mapToInt(updateAccountsIfNoUpdatedForHours))
}.executeAsOneOrNull()
return userQueries.selectAllUsers { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, userSetDisplayName, clientData, displayIndex, iconUrl, wrongCredentialsEntered ->
UserEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, getAccountsOfUser(id, bankAccounts, holdings), selectedTanMethodIdentifier, tanMethods[id] ?: emptyList(), selectedTanMediumIdentifier, tanMedia[id] ?: emptyList(),
bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered)
override suspend fun saveAppSettings(settings: AppSettings) {
settingsQueries.upsertAppSettings(
mapEnum(settings.authenticationMethod), settings.hashedPassword, settings.updateAccountsOnAppStart, mapInt(settings.updateAccountsIfNoUpdatedForHours),
0,
0, 0, 0, 0,
null
)
}
override fun getUiSettings(settings: UiSettings) {
settingsQueries.getUiSettings { _, transactionsGrouping, showBalance, showBankIcons, showColoredAmounts, showTransactionsInAlternatingColors ->
settings.transactionsGrouping.value = mapToEnum(transactionsGrouping, TransactionsGrouping.entries)
settings.showBalance.value = showBalance
settings.showBankIcons.value = showBankIcons
settings.showColoredAmounts.value = showColoredAmounts
settings.showTransactionsInAlternatingColors.value = showTransactionsInAlternatingColors
}.executeAsOneOrNull()
}
override suspend fun saveUiSettings(settings: UiSettings) {
settingsQueries.upsertUiSettings(mapEnum(settings.transactionsGrouping.value), settings.showBalance.value, settings.showBankIcons.value, settings.showColoredAmounts.value, settings.showTransactionsInAlternatingColors.value)
}
override fun getAllBanks(): List<BankAccessEntity> {
val bankAccounts = getAllBankAccounts().groupBy { it.bankId }
val tanMethods = getAllTanMethods().groupBy { it.bankId }.mapValues { it.value.toMutableList() }
val tanMedia = getAllTanMedia().groupBy { it.bankId }.mapValues { it.value.toMutableList() }
val holdings = getAllHoldings().groupBy { it.accountId }
return bankQueries.getAllBanks { id, domesticBankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, countryCode, userSetDisplayName, clientData, displayIndex, iconUrl, wrongCredentialsEntered ->
BankAccessEntity(id, domesticBankCode, loginName, password, bankName, bic, customerName, userId, getAccountsOfBank(id, bankAccounts, holdings), selectedTanMethodIdentifier, tanMethods[id] ?: mutableListOf(), selectedTanMediumIdentifier, tanMedia[id] ?: mutableListOf(),
bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, countryCode, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered)
}.executeAsList()
}
protected open fun getAccountsOfUser(userId: Long, bankAccounts: Map<Long, List<BankAccountEntity>>, holdings: Map<Long, List<HoldingEntity>>): List<BankAccountEntity> {
return bankAccounts[userId].orEmpty().onEach {
it.holdings = holdings[it.id].orEmpty()
protected open fun getAccountsOfBank(bankId: Long, bankAccounts: Map<Long, List<BankAccountEntity>>, holdings: Map<Long, List<HoldingEntity>>): List<BankAccountEntity> {
return bankAccounts[bankId].orEmpty().onEach {
it.addHoldings(holdings[it.id].orEmpty())
}
}
override suspend fun persistUser(user: User): UserEntity {
return userQueries.transactionWithResult {
userQueries.insertUser(user.bankCode, user.loginName, user.password, user.bankName, user.bic,
user.customerName, user.userId, user.selectedTanMethodIdentifier, user.selectedTanMediumIdentifier,
user.bankingGroup?.name, user.serverAddress, null, user.userSetDisplayName, user.displayIndex.toLong(), user.iconUrl, user.wrongCredentialsEntered
override suspend fun persistBank(bank: BankAccess): BankAccessEntity {
return bankQueries.transactionWithResult {
bankQueries.insertBank(bank.domesticBankCode, bank.loginName, bank.password, bank.bankName, bank.bic,
bank.customerName, bank.userId, bank.selectedTanMethodIdentifier, bank.selectedTanMediumIdentifier,
bank.bankingGroup?.name, bank.serverAddress, bank.countryCode, null, bank.userSetDisplayName, bank.displayIndex.toLong(), bank.iconUrl, bank.wrongCredentialsEntered
)
val userId = getLastInsertedId() // getLastInsertedId() / last_insert_rowid() has to be called in a transaction with the insert operation, otherwise it will not work
val bankId = getLastInsertedId() // getLastInsertedId() / last_insert_rowid() has to be called in a transaction with the insert operation, otherwise it will not work
val bankAccounts = persistBankAccounts(userId, user.accounts)
val bankAccounts = persistBankAccounts(bankId, bank.accounts)
val tanMethods = persistTanMethods(userId, user.tanMethods)
val tanMedia = persistTanMedia(userId, user.tanMedia)
val tanMethods = persistTanMethods(bankId, bank.tanMethods)
val tanMedia = persistTanMedia(bankId, bank.tanMedia)
UserEntity(userId, user, bankAccounts, tanMethods, tanMedia)
BankAccessEntity(bankId, bank, bankAccounts, tanMethods, tanMedia)
}
}
fun getAllBankAccounts(): List<BankAccountEntity> = userQueries.selectAllBankAccounts { id, userId, identifier, subAccountNumber, iban, productName, accountHolderName, type, currency, accountLimit, isAccountTypeSupportedByApplication, features, balance, serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ->
fun getAllBankAccounts(): List<BankAccountEntity> = bankQueries.getAllBankAccounts { id, bankId, identifier, subAccountNumber, iban, productName, accountHolderName, type, currency, accountLimit, isAccountTypeSupportedByApplication, features, balance, serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ->
BankAccountEntity(
id, userId,
id, bankId,
identifier, subAccountNumber, iban, productName,
@ -79,22 +120,22 @@ open class SqliteBankingRepository(
mapToInt(serverTransactionsRetentionDays),
mapToInstant(lastAccountUpdateTime), mapToDate(retrievedTransactionsFrom),
mutableListOf(), mutableListOf(), emptyList(),
mutableListOf(), mutableListOf(), mutableListOf(),
userSetDisplayName, mapToInt(displayIndex),
hideAccount, includeInAutomaticAccountsUpdate
)
}.executeAsList()
private suspend fun persistBankAccounts(userId: Long, bankAccounts: Collection<BankAccount>): List<BankAccountEntity> =
bankAccounts.map { persistBankAccount(userId, it) }
private suspend fun persistBankAccounts(bankId: Long, bankAccounts: Collection<BankAccount>): List<BankAccountEntity> =
bankAccounts.map { persistBankAccount(bankId, it) }
/**
* Has to be executed in a transaction in order that getting persisted BankAccount's id works~
*/
private suspend fun persistBankAccount(userId: Long, account: BankAccount): BankAccountEntity {
userQueries.insertBankAccount(
userId,
private suspend fun persistBankAccount(bankId: Long, account: BankAccount): BankAccountEntity {
bankQueries.insertBankAccount(
bankId,
account.identifier, account.accountHolderName, mapEnum(account.type),
account.iban, account.subAccountNumber, account.productName, account.currency, account.accountLimit,
@ -111,49 +152,53 @@ open class SqliteBankingRepository(
val accountId = getLastInsertedId()
val accountTransactionEntities = account.bookedTransactions.map { transaction ->
persistTransaction(userId, accountId, transaction)
persistTransaction(bankId, accountId, transaction)
}
val holdings = account.holdings.map { holding -> persistHolding(userId, accountId, holding) }
val holdings = account.holdings.map { holding -> persistHolding(bankId, accountId, holding) }
return BankAccountEntity(accountId, userId, account, accountTransactionEntities, holdings)
return BankAccountEntity(accountId, bankId, account, accountTransactionEntities, holdings)
}
private fun getAllTanMethods(): List<TanMethodEntity> = userQueries.selectAllTanMethods { id, userId, displayName, type, identifier, maxTanInputLength, allowedTanFormat ->
private fun getAllTanMethods(): List<TanMethodEntity> = bankQueries.getAllTanMethods { id, bankId, displayName, type, identifier, maxTanInputLength, allowedTanFormat, userSetDisplayName ->
TanMethodEntity(
id,
userId,
bankId,
displayName,
mapToEnum(type, TanMethodType.entries),
identifier,
mapToInt(maxTanInputLength),
mapToEnum(allowedTanFormat, AllowedTanFormat.entries)
mapToEnum(allowedTanFormat, AllowedTanFormat.entries),
userSetDisplayName
)
}.executeAsList()
private suspend fun persistTanMethods(userId: Long, tanMethods: List<TanMethod>): List<TanMethodEntity> =
tanMethods.map { persistTanMethod(userId, it) }
private suspend fun persistTanMethods(bankId: Long, tanMethods: List<TanMethod>): List<TanMethodEntity> =
tanMethods.map { persistTanMethod(bankId, it) }
private suspend fun persistTanMethod(userId: Long, tanMethod: TanMethod): TanMethodEntity {
userQueries.insertTanMethod(
userId,
private suspend fun persistTanMethod(bankId: Long, tanMethod: TanMethod): TanMethodEntity {
bankQueries.insertTanMethod(
bankId,
tanMethod.displayName,
mapEnum(tanMethod.type),
tanMethod.identifier,
mapInt(tanMethod.maxTanInputLength),
mapEnum(tanMethod.allowedTanFormat)
mapEnum(tanMethod.allowedTanFormat),
tanMethod.userSetDisplayName
)
val tanMethodId = getLastInsertedId()
return TanMethodEntity(tanMethodId, userId, tanMethod)
return TanMethodEntity(tanMethodId, bankId, tanMethod)
}
private fun getAllTanMedia(): List<TanMediumEntity> = userQueries.selectAllTanMedia { id, userId, type, mediumName, status, phoneNumber, concealedPhoneNumber, cardNumber, cardSequenceNumber, cardType, validFrom, validTo ->
private fun getAllTanMedia(): List<TanMediumEntity> = bankQueries.getAllTanMedia { id, bankId, type, mediumName, status, phoneNumber, concealedPhoneNumber, cardNumber, cardSequenceNumber, cardType, validFrom, validTo, userSetDisplayName ->
val mobilePhone = if (phoneNumber != null || concealedPhoneNumber != null) {
MobilePhoneTanMedium(phoneNumber, concealedPhoneNumber)
} else {
@ -168,23 +213,25 @@ open class SqliteBankingRepository(
TanMediumEntity(
id,
userId,
bankId,
mapToEnum(type, TanMediumType.entries),
mediumName,
mapToEnum(status, TanMediumStatus.entries),
tanGenerator,
mobilePhone
mobilePhone,
userSetDisplayName
)
}.executeAsList()
private suspend fun persistTanMedia(userId: Long, tanMedia: List<TanMedium>): List<TanMediumEntity> =
tanMedia.map { persistTanMedium(userId, it) }
private suspend fun persistTanMedia(bankId: Long, tanMedia: List<TanMedium>): List<TanMediumEntity> =
tanMedia.map { persistTanMedium(bankId, it) }
private suspend fun persistTanMedium(userId: Long, medium: TanMedium): TanMediumEntity {
userQueries.insertTanMedium(
userId,
private suspend fun persistTanMedium(bankId: Long, medium: TanMedium): TanMediumEntity {
bankQueries.insertTanMedium(
bankId,
mapEnum(medium.type),
medium.mediumName,
@ -197,31 +244,33 @@ open class SqliteBankingRepository(
medium.tanGenerator?.cardSequenceNumber,
mapInt(medium.tanGenerator?.cardType),
mapDate(medium.tanGenerator?.validFrom),
mapDate(medium.tanGenerator?.validTo)
mapDate(medium.tanGenerator?.validTo),
medium.userSetDisplayName
)
val tanMediumId = getLastInsertedId()
return TanMediumEntity(tanMediumId, userId, medium)
return TanMediumEntity(tanMediumId, bankId, medium)
}
protected open fun getAllHoldings(): List<HoldingEntity> =
accountTransactionQueries.selectAllHoldings { id, userId, bankAccountId, name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate ->
HoldingEntity(id, userId, bankAccountId, name, isin, wkn, mapToInt(quantity), currency, mapToAmount(totalBalance), mapToAmount(marketValue), performancePercentage?.toFloat(), mapToAmount(totalCostPrice), mapToAmount(averageCostPrice), mapToInstant(pricingTime), mapToDate(buyingDate))
accountTransactionQueries.selectAllHoldings { id, bankId, accountId, name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate ->
HoldingEntity(id, bankId, accountId, name, isin, wkn, mapToInt(quantity), currency, mapToAmount(totalBalance), mapToAmount(marketValue), performancePercentage?.toFloat(), mapToAmount(totalCostPrice), mapToAmount(averageCostPrice), mapToInstant(pricingTime), mapToDate(buyingDate))
}.executeAsList()
override suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity> =
accountTransactionQueries.transactionWithResult {
holdings.map { persistHolding(bankAccount.userId, bankAccount.id, it) }
holdings.map { persistHolding(bankAccount.bankId, bankAccount.id, it) }
}
/**
* Has to be executed in a transaction in order that getting persisted Holding's id works~
*/
protected open suspend fun persistHolding(userId: Long, bankAccountId: Long, holding: Holding): HoldingEntity {
protected open suspend fun persistHolding(bankId: Long, accountId: Long, holding: Holding): HoldingEntity {
accountTransactionQueries.insertHolding(
userId, bankAccountId,
bankId, accountId,
holding.name, holding.isin, holding.wkn,
@ -234,7 +283,7 @@ open class SqliteBankingRepository(
mapInstant(holding.pricingTime), mapDate(holding.buyingDate)
)
return HoldingEntity(getLastInsertedId(), userId, bankAccountId, holding)
return HoldingEntity(getLastInsertedId(), bankId, accountId, holding)
}
override suspend fun updateHoldings(holdings: List<HoldingEntity>) {
@ -266,16 +315,16 @@ open class SqliteBankingRepository(
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
accountTransactionQueries.selectAllTransactionsAsViewModel { id, userId, bankAccountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, category ->
AccountTransactionViewModel(id, userId, bankAccountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, category)
accountTransactionQueries.getAllTransactionsAsViewModel { id, bankId, accountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, userSetOtherPartyName ->
AccountTransactionViewModel(id, bankId, accountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, userSetOtherPartyName)
}.executeAsList()
override fun getAllAccountTransactions(): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactions(::mapTransaction).executeAsList()
return accountTransactionQueries.getAllTransactions(::mapTransaction).executeAsList()
}
override fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactionsOfUser(user.id, ::mapTransaction).executeAsList()
override fun getAllTransactionsForBank(bank: BankAccessEntity): List<AccountTransactionEntity> {
return accountTransactionQueries.getAllTransactionsForBank(bank.id, ::mapTransaction).executeAsList()
}
override fun getTransactionById(transactionId: Long): AccountTransactionEntity? =
@ -285,7 +334,7 @@ open class SqliteBankingRepository(
override suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity> {
return accountTransactionQueries.transactionWithResult {
transactions.map { transaction ->
persistTransaction(bankAccount.userId, bankAccount.id, transaction)
persistTransaction(bankAccount.bankId, bankAccount.id, transaction)
}
}
}
@ -293,9 +342,9 @@ open class SqliteBankingRepository(
/**
* Has to be executed in a transaction in order that getting persisted AccountTransaction's id works~
*/
protected open suspend fun persistTransaction(userId: Long, bankAccountId: Long, transaction: AccountTransaction): AccountTransactionEntity {
protected open suspend fun persistTransaction(bankId: Long, accountId: Long, transaction: AccountTransaction): AccountTransactionEntity {
accountTransactionQueries.insertTransaction(
userId, bankAccountId,
bankId, accountId,
mapAmount(transaction.amount), transaction.currency, transaction.reference,
mapDate(transaction.bookingDate), mapDate(transaction.valueDate),
@ -305,7 +354,8 @@ open class SqliteBankingRepository(
mapAmount(transaction.openingBalance), mapAmount(transaction.closingBalance),
transaction.userSetDisplayName, transaction.category, transaction.notes,
transaction.userSetReference, transaction.userSetReference,
transaction.category, transaction.notes,
transaction.statementNumber?.toLong(), transaction.sheetNumber?.toLong(),
@ -326,16 +376,16 @@ open class SqliteBankingRepository(
transaction.isReversal
)
return AccountTransactionEntity(getLastInsertedId(), userId, bankAccountId, transaction)
return AccountTransactionEntity(getLastInsertedId(), bankId, accountId, transaction)
}
private fun getLastInsertedId(): Long =
userQueries.getLastInsertedId().executeAsOne()
bankQueries.getLastInsertedId().executeAsOne()
private fun mapTransaction(
id: Long, userId: Long, bankAccountId: Long,
id: Long, bankId: Long, accountId: Long,
amount: String, currency: String, reference: String?,
bookingDate: String, valueDate: String,
@ -345,7 +395,8 @@ open class SqliteBankingRepository(
openingBalance: String?, closingBalance: String?,
userSetDisplayName: String?, category: String?, notes: String?,
userSetDisplayName: String?, userSetReference: String?,
category: String?, notes: String?,
statementNumber: Long?, sheetNumber: Long?,
@ -366,7 +417,7 @@ open class SqliteBankingRepository(
isReversal: Boolean
): AccountTransactionEntity = AccountTransactionEntity(
id,
userId, bankAccountId,
bankId, accountId,
Amount(amount), currency, reference,
mapToDate(bookingDate), mapToDate(valueDate),
@ -375,7 +426,8 @@ open class SqliteBankingRepository(
mapToAmount(openingBalance), mapToAmount(closingBalance),
userSetDisplayName, category, notes,
userSetDisplayName, userSetReference,
category, notes,
statementNumber?.toInt(), sheetNumber?.toInt(),

View File

@ -6,8 +6,8 @@ import net.codinux.banking.client.model.Amount
class AccountTransactionEntity(
val id: Long,
val userId: Long,
val bankAccountId: Long,
val bankId: Long,
val accountId: Long,
amount: Amount,
currency: String,
@ -25,7 +25,8 @@ class AccountTransactionEntity(
openingBalance: Amount? = null,
closingBalance: Amount? = null,
userSetDisplayName: String? = null,
userSetReference: String? = null,
userSetOtherPartyName: String? = null,
category: String? = null,
notes: String? = null,
@ -80,10 +81,10 @@ class AccountTransactionEntity(
isReversal,
userSetDisplayName, category, notes
userSetReference, userSetOtherPartyName, category, notes
) {
constructor(id: Long, userId: Long, bankAccountId: Long, transaction: AccountTransaction) : this(
id, userId, bankAccountId,
constructor(id: Long, bankId: Long, accountId: Long, transaction: AccountTransaction) : this(
id, bankId, accountId,
transaction.amount, transaction.currency, transaction.reference,
transaction.bookingDate, transaction.valueDate,
@ -93,7 +94,8 @@ class AccountTransactionEntity(
transaction.openingBalance, transaction.closingBalance,
transaction.userSetDisplayName, transaction.category, transaction.notes,
transaction.userSetReference, transaction.userSetOtherPartyName,
transaction.category, transaction.notes,
transaction.statementNumber, transaction.sheetNumber,
@ -116,7 +118,7 @@ class AccountTransactionEntity(
override val identifier: String by lazy {
"$userId ${super.identifier}"
"$bankId ${super.identifier}"
}
}

View File

@ -0,0 +1,58 @@
package net.codinux.banking.dataaccess.entities
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.BankingGroup
import net.codinux.banking.client.model.tan.TanMedium
class BankAccessEntity(
val id: Long,
domesticBankCode: String,
loginName: String,
password: String?,
bankName: String,
bic: String?,
customerName: String,
userId: String? = null,
override val accounts: List<BankAccountEntity> = emptyList(),
selectedTanMethodIdentifier: String? = null,
override val tanMethods: MutableList<TanMethodEntity> = mutableListOf(),
selectedTanMediumIdentifier: String? = null,
override val tanMedia: MutableList<TanMediumEntity> = mutableListOf(),
bankingGroup: BankingGroup? = null,
serverAddress: String? = null,
countryCode: String = "de",
userSetDisplayName: String? = null,
displayIndex: Int = 0,
iconUrl: String? = null,
wrongCredentialsEntered: Boolean = false
) : BankAccess(domesticBankCode, loginName, password, bankName, bic, customerName, userId, accounts, selectedTanMethodIdentifier, tanMethods, selectedTanMediumIdentifier, tanMedia, bankingGroup, serverAddress, countryCode) {
init {
this.userSetDisplayName = userSetDisplayName
this.displayIndex = displayIndex
this.iconUrl = iconUrl
this.wrongCredentialsEntered = wrongCredentialsEntered
}
constructor(id: Long, bank: BankAccess, bankAccounts: List<BankAccountEntity>, tanMethods: List<TanMethodEntity>, tanMedia: List<TanMediumEntity>) : this(
id,
bank.domesticBankCode, bank.loginName, bank.password, bank.bankName, bank.bic, bank.customerName, bank.userId,
bankAccounts,
bank.selectedTanMethodIdentifier, tanMethods.toMutableList(), bank.selectedTanMediumIdentifier, tanMedia.toMutableList(),
bank.bankingGroup, bank.serverAddress, bank.countryCode,
bank.userSetDisplayName, bank.displayIndex,
bank.iconUrl, bank.wrongCredentialsEntered,
)
}

View File

@ -3,11 +3,11 @@ package net.codinux.banking.dataaccess.entities
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.*
import net.codinux.banking.client.model.securitiesaccount.Holding
import kotlin.jvm.JvmName
class BankAccountEntity(
val id: Long,
val userId: Long,
val bankId: Long,
identifier: String,
subAccountNumber: String? = null,
@ -28,9 +28,9 @@ class BankAccountEntity(
lastAccountUpdateTime: Instant? = null,
retrievedTransactionsFrom: LocalDate? = null,
bookedTransactions: MutableList<AccountTransactionEntity> = mutableListOf(),
override val bookedTransactions: MutableList<AccountTransactionEntity> = mutableListOf(),
prebookedTransactions: MutableList<PrebookedAccountTransaction> = mutableListOf(),
override var holdings: List<HoldingEntity> = emptyList(),
override val holdings: MutableList<HoldingEntity> = mutableListOf(),
userSetDisplayName: String? = null,
displayIndex: Int = 0,
@ -48,14 +48,14 @@ class BankAccountEntity(
serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom,
bookedTransactions as MutableList<AccountTransaction>, prebookedTransactions,
bookedTransactions, prebookedTransactions,
holdings,
userSetDisplayName, displayIndex,
hideAccount, includeInAutomaticAccountsUpdate
) {
constructor(id: Long, userId: Long, account: BankAccount, transactions: List<AccountTransactionEntity> = emptyList(), holdings: List<HoldingEntity> = emptyList()) : this(
id, userId,
constructor(id: Long, bankId: Long, account: BankAccount, transactions: List<AccountTransactionEntity> = emptyList(), holdings: List<HoldingEntity> = emptyList()) : this(
id, bankId,
account.identifier, account.subAccountNumber, account.iban, account.productName,
account.accountHolderName, account.type,
@ -68,12 +68,16 @@ class BankAccountEntity(
account.serverTransactionsRetentionDays,
account.lastAccountUpdateTime, account.retrievedTransactionsFrom,
transactions.toMutableList(), mutableListOf(), holdings,
transactions.toMutableList(), mutableListOf(), holdings.toMutableList(),
account.userSetDisplayName, account.displayIndex,
account.hideAccount, account.includeInAutomaticAccountsUpdate
)
val bookedTransactionsEntities: MutableList<AccountTransactionEntity> = bookedTransactions
@JvmName("addHoldingsEntities")
fun addHoldings(holdings: List<HoldingEntity>) {
this.holdings.addAll(holdings)
}
}

View File

@ -7,8 +7,8 @@ import net.codinux.banking.client.model.securitiesaccount.Holding
class HoldingEntity(
val id: Long,
val userId: Long,
val bankAccountId: Long,
val bankId: Long,
val accountId: Long,
name: String,
@ -30,8 +30,8 @@ class HoldingEntity(
buyingDate: LocalDate? = null
) : Holding(name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate) {
constructor(id: Long, userId: Long, bankAccountId: Long, holding: Holding) : this(
id, userId, bankAccountId,
constructor(id: Long, bankId: Long, accountId: Long, holding: Holding) : this(
id, bankId, accountId,
holding.name, holding.isin, holding.wkn,

View File

@ -4,17 +4,19 @@ import net.codinux.banking.client.model.tan.*
class TanMediumEntity(
val id: Long,
val userId: Long,
val bankId: Long,
type: TanMediumType,
mediumName: String?,
status: TanMediumStatus,
tanGenerator: TanGeneratorTanMedium? = null,
mobilePhone: MobilePhoneTanMedium? = null
) : TanMedium(type, mediumName, status, tanGenerator, mobilePhone) {
mobilePhone: MobilePhoneTanMedium? = null,
constructor(id: Long, userId: Long, medium: TanMedium)
: this(id, userId, medium.type, medium.mediumName, medium.status, medium.tanGenerator, medium.mobilePhone)
userSetDisplayName: String? = null
) : TanMedium(type, mediumName, status, tanGenerator, mobilePhone, userSetDisplayName) {
constructor(id: Long, bankId: Long, medium: TanMedium)
: this(id, bankId, medium.type, medium.mediumName, medium.status, medium.tanGenerator, medium.mobilePhone, medium.userSetDisplayName)
}

View File

@ -6,16 +6,18 @@ import net.codinux.banking.client.model.tan.TanMethodType
class TanMethodEntity(
val id: Long,
val userId: Long,
val bankId: Long,
displayName: String,
type: TanMethodType,
identifier: String,
maxTanInputLength: Int? = null,
allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
) : TanMethod(displayName, type, identifier, maxTanInputLength, allowedTanFormat) {
allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric,
constructor(id: Long, userId: Long, tanMethod: TanMethod)
: this(id, userId, tanMethod.displayName, tanMethod.type, tanMethod.identifier, tanMethod.maxTanInputLength, tanMethod.allowedTanFormat)
userSetDisplayName: String? = null,
) : TanMethod(displayName, type, identifier, maxTanInputLength, allowedTanFormat, userSetDisplayName) {
constructor(id: Long, bankId: Long, tanMethod: TanMethod)
: this(id, bankId, tanMethod.displayName, tanMethod.type, tanMethod.identifier, tanMethod.maxTanInputLength, tanMethod.allowedTanFormat, tanMethod.userSetDisplayName)
}

View File

@ -1,58 +0,0 @@
package net.codinux.banking.dataaccess.entities
import net.codinux.banking.client.model.BankingGroup
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.tan.TanMedium
import net.codinux.banking.client.model.tan.TanMethod
class UserEntity(
val id: Long,
bankCode: String,
loginName: String,
password: String?,
bankName: String,
bic: String,
customerName: String,
userId: String? = null,
override val accounts: List<BankAccountEntity> = emptyList(),
selectedTanMethodIdentifier: String? = null,
override val tanMethods: List<TanMethodEntity> = listOf(),
selectedTanMediumIdentifier: String? = null,
tanMedia: List<TanMedium> = listOf(),
bankingGroup: BankingGroup? = null,
serverAddress: String? = null,
userSetDisplayName: String? = null,
displayIndex: Int = 0,
iconUrl: String? = null,
wrongCredentialsEntered: Boolean = false
) : User(bankCode, loginName, password, bankName, bic, customerName, userId, accounts, selectedTanMethodIdentifier, tanMethods, selectedTanMediumIdentifier, tanMedia, bankingGroup, serverAddress) {
init {
this.userSetDisplayName = userSetDisplayName
this.displayIndex = displayIndex
this.iconUrl = iconUrl
this.wrongCredentialsEntered = wrongCredentialsEntered
}
constructor(id: Long, user: User, bankAccounts: List<BankAccountEntity>, tanMethods: List<TanMethodEntity>, tanMedia: List<TanMediumEntity>) : this(
id,
user.bankCode, user.loginName, user.password, user.bankName, user.bic, user.customerName, user.userId,
bankAccounts,
user.selectedTanMethodIdentifier, tanMethods, user.selectedTanMediumIdentifier, tanMedia,
user.bankingGroup, user.serverAddress,
user.userSetDisplayName, user.displayIndex,
user.iconUrl, user.wrongCredentialsEntered,
)
}

View File

@ -29,7 +29,7 @@ private val IconWidth = 48.dp
@Composable
fun BottomBar(showMenuDrawer: Boolean = true) {
val users by uiState.users.collectAsState()
val banks by uiState.banks.collectAsState()
val transactionsFilter by uiState.transactionsFilter.collectAsState()
@ -65,7 +65,7 @@ fun BottomBar(showMenuDrawer: Boolean = true) {
} else if (selectedAccount.bankAccount != null) {
selectedAccount.bankAccount.displayName
} else {
selectedAccount.user.displayName
selectedAccount.bank.displayName
}
Text(title, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis)
@ -122,7 +122,7 @@ fun BottomBar(showMenuDrawer: Boolean = true) {
}
if (users.isNotEmpty()) {
if (banks.isNotEmpty()) {
if (showSearchbar == false) {
Row(Modifier.fillMaxHeight().widthIn(IconWidth, IconWidth), verticalAlignment = Alignment.CenterVertically) {
IconButton({ showSearchbar = true }, Modifier.width(IconWidth)) {

View File

@ -2,7 +2,6 @@ package net.codinux.banking.ui.appskeleton
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
@ -55,7 +54,7 @@ private val VerticalSpacing = 8.dp
fun SideMenuContent() {
val drawerState = uiState.drawerState.collectAsState().value
val accounts = uiState.users.collectAsState().value
val accounts = uiState.banks.collectAsState().value
val coroutineScope = rememberCoroutineScope()

View File

@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.User
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
@ -21,8 +21,8 @@ private val bankIconService = DI.bankIconService
private val DefaultIconModifier: Modifier = Modifier.size(16.dp)
@Composable
fun BankIcon(user: User?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null, fallbackIconTintColor: Color? = null) {
val iconUrl by remember(user?.bic) { mutableStateOf(user?.let { bankIconService.findIconForBank(it) }) }
fun BankIcon(bank: BankAccess?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null, fallbackIconTintColor: Color? = null) {
val iconUrl by remember(bank?.bic) { mutableStateOf(bank?.let { bankIconService.findIconForBank(it) }) }
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon, fallbackIconTintColor = fallbackIconTintColor)
}
@ -37,8 +37,8 @@ fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifi
}
@Composable
fun BankIcon(user: BankViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) {
val iconUrl = user?.let { bankIconService.findIconForBank(it.bankName, null, it.bankingGroup) }
fun BankIcon(bank: BankViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) {
val iconUrl = bank?.let { bankIconService.findIconForBank(it.bankName, null, it.bankingGroup) }
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
}

View File

@ -11,7 +11,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.config.DI
private val uiState = DI.uiState
@ -27,9 +27,9 @@ fun BanksList(
textColor: Color = Color.White,
itemModifier: Modifier = Modifier.height(48.dp).widthIn(min = 300.dp),
itemHorizontalPadding: Dp = 8.dp,
accountSelected: ((UserEntity?, BankAccountEntity?) -> Unit)? = null
accountSelected: ((BankAccessEntity?, BankAccountEntity?) -> Unit)? = null
) {
val users = uiState.users.collectAsState()
val banks = uiState.banks.collectAsState()
Column(modifier) {
@ -37,16 +37,16 @@ fun BanksList(
accountSelected?.invoke(null, null)
}
users.value.sortedBy { it.displayIndex }.forEach { user ->
banks.value.sortedBy { it.displayIndex }.forEach { bank ->
Spacer(Modifier.fillMaxWidth().height(12.dp))
NavigationMenuItem(itemModifier, user.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, user, fallbackIcon = defaultBankIcon) {
accountSelected?.invoke(user, null)
NavigationMenuItem(itemModifier, bank.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, bank, fallbackIcon = defaultBankIcon) {
accountSelected?.invoke(bank, null)
}
user.accounts.sortedBy { it.displayIndex }.forEach { account ->
bank.accounts.sortedBy { it.displayIndex }.forEach { account ->
NavigationMenuItem(itemModifier, account.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, bankAccount = account) {
accountSelected?.invoke(user, account)
accountSelected?.invoke(bank, account)
}
}
}

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -19,7 +20,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
@ -37,7 +38,7 @@ fun NavigationMenuItem(
iconSize: Dp = 24.dp,
iconTextSpacing: Dp = 24.dp,
horizontalPadding: Dp = 8.dp,
user: UserEntity? = null,
bank: BankAccessEntity? = null,
bankAccount: BankAccountEntity? = null,
fallbackIcon: ImageVector? = null,
icon: (@Composable () -> Unit)? = null,
@ -49,12 +50,26 @@ fun NavigationMenuItem(
val showColoredAmounts by DI.uiSettings.showColoredAmounts.collectAsState()
val isUnsupportedAccountType = bankAccount?.isAccountTypeSupportedByApplication == false
val effectiveText = if (isUnsupportedAccountType) "$text (nicht unterstützt)" else text
val effectiveTextColor = if (isUnsupportedAccountType) textColor.copy(ContentAlpha.disabled) else textColor
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.clickable { onClick?.invoke() }
modifier = modifier.let {
if (isUnsupportedAccountType == false) {
it.clickable {
onClick?.invoke()
}
} else {
it
}
}
.let {
if (user != null && filterService.isSelected(user, transactionsFilter)
if (bank != null && filterService.isSelected(bank, transactionsFilter)
|| bankAccount != null && filterService.isSelected(bankAccount, transactionsFilter)) {
it.background(Colors.AccentAsSelectionBackground, shape = RoundedCornerShape(4.dp))
} else {
@ -68,17 +83,17 @@ fun NavigationMenuItem(
icon()
}
} else {
BankIcon(user, Modifier.padding(end = iconTextSpacing), Modifier.size(iconSize), fallbackIcon = fallbackIcon, fallbackIconTintColor = textColor)
BankIcon(bank, Modifier.padding(end = iconTextSpacing), Modifier.size(iconSize), fallbackIcon = fallbackIcon, fallbackIconTintColor = effectiveTextColor)
}
Text(text, color = textColor, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(effectiveText, color = effectiveTextColor, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
val balance = if (showBalance == false) {
val balance = if (showBalance == false || isUnsupportedAccountType) {
null
} else if (bankAccount != null) {
bankAccount.balance
} else if (user != null) {
calculator.calculateBalanceOfUser(user)
} else if (bank != null) {
calculator.calculateBalanceOfBankAccess(bank)
} else {
null
}

View File

@ -75,7 +75,7 @@ fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
}
snackbarHostState.showSnackbar(
message = "$messagePrefix für ${event.user.displayName} ${event.account.displayName}",
message = "$messagePrefix für ${event.bank.displayName} ${event.account.displayName}",
actionLabel = actionLabel,
duration = SnackbarDuration.Long
)

View File

@ -23,7 +23,7 @@ fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) {
val transactionsGrouping by uiSettings.transactionsGrouping.collectAsState()
val zebraStripes by uiSettings.zebraStripes.collectAsState()
val showTransactionsInAlternatingColors by uiSettings.showTransactionsInAlternatingColors.collectAsState()
val showBankIcons by uiSettings.showBankIcons.collectAsState()
@ -33,7 +33,7 @@ fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) {
Column(modifier) {
BooleanOption("Kontostand anzeigen", showBalance, textColor = textColor) { uiSettings.showBalance.value = it }
BooleanOption("Zebra Stripes", zebraStripes, textColor = textColor) { uiSettings.zebraStripes.value = it }
BooleanOption("Umsätze in alternierenden Farben anzeigen", showTransactionsInAlternatingColors, textColor = textColor) { uiSettings.showTransactionsInAlternatingColors.value = it }
BooleanOption("Bank Icons anzeigen", showBankIcons, textColor = textColor) { uiSettings.showBankIcons.value = it }

View File

@ -14,9 +14,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Style
import net.codinux.banking.ui.forms.RoundedCornersCard
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.TransactionsGrouping
@ -31,7 +32,7 @@ fun GroupedTransactionsListItems(
modifier: Modifier,
transactionsToDisplay: List<AccountTransactionViewModel>,
holdingsToDisplay: List<Holding>,
usersById: Map<Long, UserEntity>,
banksById: Map<Long, BankAccessEntity>,
transactionsGrouping: TransactionsGrouping
) {
val groupingService = remember { TransactionsGroupingService() }
@ -49,6 +50,7 @@ fun GroupedTransactionsListItems(
Column(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 16.dp)) {
Text(
text = "Depotwerte",
color = Style.ListItemHeaderTextColor,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(bottom = 2.dp),
@ -77,6 +79,7 @@ fun GroupedTransactionsListItems(
Column(Modifier.fillMaxWidth()) {
Text(
text = DI.formatUtil.formatGroupingDate(groupingDate, transactionsGrouping),
color = Style.ListItemHeaderTextColor,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 8.dp, bottom = 2.dp),
@ -90,7 +93,7 @@ fun GroupedTransactionsListItems(
Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed
monthTransactions.forEachIndexed { index, transaction ->
key(transaction.id) {
TransactionListItem(usersById[transaction.userId], transaction, index, monthTransactions.size)
TransactionListItem(banksById[transaction.bankId], transaction, index, monthTransactions.size)
}
}
}

View File

@ -9,7 +9,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.Amount
@ -32,11 +31,11 @@ fun HoldingListItem(holding: Holding, isOddItem: Boolean = false, isNotLastItem:
// TODO: also regard showBalance?
val showColoredAmounts by uiSettings.showColoredAmounts.collectAsState()
val zebraStripes by uiSettings.zebraStripes.collectAsState()
val showTransactionsInAlternatingColors by uiSettings.showTransactionsInAlternatingColors.collectAsState()
val showBankIcons by uiSettings.showBankIcons.collectAsState()
val backgroundColor = if (zebraStripes && isOddItem) Colors.ZebraStripesColor else Color.White
val backgroundColor = if (showTransactionsInAlternatingColors && isOddItem) Colors.ZebraStripesColor else Color.White
val currency = holding.currency ?: fallbackCurrency
@ -49,6 +48,7 @@ fun HoldingListItem(holding: Holding, isOddItem: Boolean = false, isNotLastItem:
Text(
holding.name,
color = Style.ListItemHeaderTextColor,
fontWeight = Style.ListItemHeaderWeight,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View File

@ -9,12 +9,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.ui.composables.BankIcon
import net.codinux.banking.ui.composables.text.ItemDivider
import net.codinux.banking.ui.config.Colors
@ -28,14 +27,14 @@ private val uiSettings = DI.uiSettings
private val formatUtil = DI.formatUtil
@Composable
fun TransactionListItem(user: User?, transaction: AccountTransactionViewModel, itemIndex: Int, countItems: Int) {
val zebraStripes by uiSettings.zebraStripes.collectAsState()
fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewModel, itemIndex: Int, countItems: Int) {
val showTransactionsInAlternatingColors by uiSettings.showTransactionsInAlternatingColors.collectAsState()
val showBankIcons by uiSettings.showBankIcons.collectAsState()
val showColoredAmounts by uiSettings.showColoredAmounts.collectAsState()
val backgroundColor = if (zebraStripes && itemIndex % 2 == 1) Colors.ZebraStripesColor else Color.White
val backgroundColor = if (showTransactionsInAlternatingColors && itemIndex % 2 == 1) Colors.ZebraStripesColor else Color.White
val bottomPadding = 56.dp
@ -51,7 +50,7 @@ fun TransactionListItem(user: User?, transaction: AccountTransactionViewModel, i
val transactionEntity = DI.bankingService.getTransaction(transaction.id)
DI.uiState.showTransferMoneyDialogData.value = ShowTransferMoneyDialogData(
DI.uiState.users.value.firstNotNullOf { it.accounts.firstOrNull { it.id == transaction.bankAccountId } },
DI.uiState.banks.value.firstNotNullOf { it.accounts.firstOrNull { it.id == transaction.accountId } },
transaction.otherPartyName,
transactionEntity?.otherPartyBankId,
transactionEntity?.otherPartyAccountId,
@ -79,12 +78,13 @@ fun TransactionListItem(user: User?, transaction: AccountTransactionViewModel, i
Column(Modifier.weight(1f)) {
Row {
if (showBankIcons) {
BankIcon(user, Modifier.padding(end = 6.dp))
BankIcon(bank, Modifier.padding(end = 6.dp))
}
Text(
text = transaction.otherPartyName ?: transaction.postingText ?: "",
Modifier.fillMaxWidth(),
color = Style.ListItemHeaderTextColor,
fontWeight = Style.ListItemHeaderWeight,
maxLines = 1,
overflow = TextOverflow.Ellipsis

View File

@ -25,9 +25,9 @@ private val formatUtil = DI.formatUtil
@Composable
fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean = true) {
val users by uiState.users.collectAsState()
val usersById by remember(users) {
derivedStateOf { users.associateBy { it.id } }
val banks by uiState.banks.collectAsState()
val banksById by remember(banks) {
derivedStateOf { banks.associateBy { it.id } }
}
val transactionsFilter by uiState.transactionsFilter.collectAsState()
@ -59,13 +59,13 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
Spacer(Modifier.weight(1f))
if (showBalance) {
val balance = calculator.calculateBalanceOfDisplayedTransactions(transactionsToDisplay, users, transactionsFilter)
val balance = calculator.calculateBalanceOfDisplayedTransactions(transactionsToDisplay, banks, transactionsFilter)
Text(formatUtil.formatAmount(balance, "EUR"), color = formatUtil.getColorForAmount(balance, showColoredAmounts))
}
}
if (transactionsGrouping != TransactionsGrouping.None) {
GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, holdingsToDisplay, usersById, transactionsGrouping)
GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, holdingsToDisplay, banksById, transactionsGrouping)
} else {
LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) {
itemsIndexed(holdingsToDisplay) { index, holding ->
@ -76,7 +76,7 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
itemsIndexed(transactionsToDisplay) { index, transaction ->
key(transaction.id) {
TransactionListItem(usersById[transaction.userId], transaction, index, transactionsToDisplay.size)
TransactionListItem(banksById[transaction.bankId], transaction, index, transactionsToDisplay.size)
}
}
}

View File

@ -39,6 +39,8 @@ object Colors {
val Zinc200 = Color(228, 228, 231)
val Zinc500 = Color(0xFF71717a)
val Zinc700 = Color(63, 63, 70)

View File

@ -36,7 +36,7 @@ object DI {
var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList())
val bankingService by lazy { BankingService(uiState, bankingRepository, bankFinder) }
val bankingService by lazy { BankingService(uiState, uiSettings, bankingRepository, bankFinder) }
fun setRepository(sqlDriver: SqlDriver) = setRepository(SqliteBankingRepository(sqlDriver))

View File

@ -13,6 +13,9 @@ object Style {
val HeaderFontWeight: FontWeight = FontWeight.Bold
val ListItemHeaderTextColor: Color = Colors.Zinc500
val ListItemHeaderWeight = FontWeight.Medium // couldn't believe it, the FontWeights look different on Desktop and Android

View File

@ -136,7 +136,7 @@ fun AddAccountDialog(
}
Row(Modifier.fillMaxWidth().padding(top = 6.dp)) {
Text(bank.bankCode, color = textColor)
Text(bank.domesticBankCode, color = textColor)
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f).padding(start = 8.dp), color = if (supportsFinTs) Color.Gray else textColor)
}

View File

@ -94,9 +94,9 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
Column(Modifier.fillMaxWidth()) {
Column(Modifier.fillMaxWidth()) {
Row {
BankIcon(challenge.user, Modifier.padding(end = 6.dp))
BankIcon(challenge.bank, Modifier.padding(end = 6.dp))
Text("${challenge.user.bankName}, Nutzer ${challenge.user.loginName}${challenge.account?.let { ", Konto ${it.productName ?: it.identifier}" } ?: ""}")
Text("${challenge.bank.bankName}, Nutzer ${challenge.bank.loginName}${challenge.account?.let { ", Konto ${it.productName ?: it.identifier}" } ?: ""}")
}
Text(
"TAN benötigt ${Internationalization.getTextForActionRequiringTan(challenge.forAction)}",

View File

@ -38,11 +38,11 @@ fun TransferMoneyDialog(
data: ShowTransferMoneyDialogData,
onDismiss: () -> Unit,
) {
val users = uiState.users.value
val accountsToUser = users.sortedBy { it.displayIndex }
.flatMap { user -> user.accounts.sortedBy { it.displayIndex }.map { it to user } }.toMap()
val banks = uiState.banks.value
val accountsToBank = banks.sortedBy { it.displayIndex }
.flatMap { bank -> bank.accounts.sortedBy { it.displayIndex }.map { it to bank } }.toMap()
val accountsSupportingTransferringMoney = users.flatMap { it.accounts }
val accountsSupportingTransferringMoney = banks.flatMap { it.accounts }
.filter { it.supportsMoneyTransfer }
if (accountsSupportingTransferringMoney.isEmpty()) {
@ -98,7 +98,7 @@ fun TransferMoneyDialog(
transferMoneyJob = coroutineScope.launch(Dispatchers.IOorDefault) {
val successful = bankingService.transferMoney(
accountsToUser[senderAccount]!!, senderAccount,
accountsToBank[senderAccount]!!, senderAccount,
recipientName, recipientAccountIdentifier,
Amount(amount), // TODO: verify entered amount is valid
"EUR", // TODO: add input field for currency
@ -132,13 +132,13 @@ fun TransferMoneyDialog(
Select(
"Konto",
accountsSupportingTransferringMoney, senderAccount, { senderAccount = it },
{ account -> "${accountsToUser[account]?.displayName} ${account.displayName}" },
leadingIcon = { BankIcon(accountsToUser[senderAccount]) }
{ account -> "${accountsToBank[account]?.displayName} ${account.displayName}" },
leadingIcon = { BankIcon(accountsToBank[senderAccount]) }
) { account ->
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
BankIcon(accountsToUser[account], Modifier.padding(end = 6.dp))
BankIcon(accountsToBank[account], Modifier.padding(end = 6.dp))
Text("${accountsToUser[account]?.displayName} ${account.displayName}")
Text("${accountsToBank[account]?.displayName} ${account.displayName}")
}
}
}

View File

@ -7,8 +7,8 @@ import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
data class AccountTransactionViewModel(
val id: Long,
val userId: Long,
val bankAccountId: Long,
val bankId: Long,
val accountId: Long,
val amount: Amount,
val currency: String,
@ -17,11 +17,11 @@ data class AccountTransactionViewModel(
val otherPartyName: String? = null,
val postingText: String? = null,
val userSetDisplayName: String? = null,
val category: String? = null
val userSetReference: String? = null,
val userSetOtherPartyName: String? = null
) {
constructor(entity: AccountTransactionEntity) : this(entity.id, entity.userId, entity.bankAccountId, entity)
constructor(entity: AccountTransactionEntity) : this(entity.id, entity.bankId, entity.accountId, entity)
constructor(id: Long, userId: Long, bankAccountId: Long, transaction: AccountTransaction)
: this(id, userId, bankAccountId, transaction.amount, transaction.currency, transaction.reference, transaction.valueDate, transaction.otherPartyName, transaction.postingText)
constructor(id: Long, bankId: Long, accountId: Long, transaction: AccountTransaction)
: this(id, bankId, accountId, transaction.amount, transaction.currency, transaction.reference, transaction.valueDate, transaction.otherPartyName, transaction.postingText, transaction.userSetReference, transaction.userSetOtherPartyName)
}

View File

@ -2,7 +2,7 @@ package net.codinux.banking.ui.model
import androidx.compose.runtime.*
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
class AccountTransactionsFilter {
@ -19,7 +19,7 @@ class AccountTransactionsFilter {
val selectedAccount: BankAccountFilter?
get() = selectedAccounts.value.firstOrNull()
fun selectedAccountChanged(user: UserEntity?, bankAccount: BankAccountEntity?) {
fun selectedAccountChanged(user: BankAccessEntity?, bankAccount: BankAccountEntity?) {
selectedAccounts.value = if (user == null) {
emptyList()
} else {

View File

@ -1,9 +1,9 @@
package net.codinux.banking.ui.model
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
data class BankAccountFilter(
val user: UserEntity,
val bank: BankAccessEntity,
val bankAccount: BankAccountEntity? = null
)

View File

@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
@Serializable
class BankInfo(
val name: String,
val bankCode: String,
val domesticBankCode: String,
val bic: String = "",
val postalCode: String,
val city: String,
@ -23,5 +23,5 @@ class BankInfo(
get() = pinTanVersion == "FinTS V3.0"
override fun toString() = "$bankCode $name $city"
override fun toString() = "$domesticBankCode $name $city"
}

View File

@ -1,12 +1,12 @@
package net.codinux.banking.ui.model.events
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.ui.model.AccountTransactionViewModel
data class AccountTransactionsRetrievedEvent(
val user: User,
val bank: BankAccess,
val account: BankAccount,
val newTransactions: List<AccountTransactionViewModel>,
val updatedHoldings: List<Holding> = emptyList()

View File

@ -0,0 +1,7 @@
package net.codinux.banking.ui.model.settings
enum class AppAuthenticationMethod {
None,
Password,
Biometric
}

View File

@ -0,0 +1,11 @@
package net.codinux.banking.ui.model.settings
class AppSettings(
var authenticationMethod: AppAuthenticationMethod = AppAuthenticationMethod.None,
var hashedPassword: String? = null,
var updateAccountsOnAppStart: Boolean = false,
var updateAccountsIfNoUpdatedForHours: Int = 6
) {
override fun toString() = "$authenticationMethod"
}

View File

@ -2,7 +2,7 @@ package net.codinux.banking.ui.service
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.HoldingEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.AccountTransactionsFilter
import net.codinux.banking.ui.model.BankAccountFilter
@ -38,9 +38,9 @@ class AccountTransactionsFilterService {
private fun matchesFilter(transaction: AccountTransactionViewModel, accountsFilter: List<BankAccountFilter>): Boolean =
accountsFilter.any { (user, bankAccount) ->
if (bankAccount != null) {
transaction.bankAccountId == bankAccount.id
transaction.accountId == bankAccount.id
} else {
transaction.userId == user.id
transaction.bankId == user.id
}
}
@ -71,9 +71,9 @@ class AccountTransactionsFilterService {
private fun matchesFilter(holding: HoldingEntity, filter: List<BankAccountFilter>): Boolean =
filter.any { (user, bankAccount) ->
if (bankAccount != null) {
holding.bankAccountId == bankAccount.id
holding.accountId == bankAccount.id
} else {
holding.userId == user.id
holding.bankId == user.id
}
}
@ -83,14 +83,14 @@ class AccountTransactionsFilterService {
|| holding.wkn?.contains(searchTerm, true) == true
fun isSelected(user: UserEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
fun isSelected(user: BankAccessEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
if (transactionsFilter.showAllAccounts) {
return false
}
val filter = transactionsFilter.selectedAccount
return filter?.user == user && filter.bankAccount == null
return filter?.bank == user && filter.bankAccount == null
}
fun isSelected(bankAccount: BankAccountEntity, transactionsFilter: AccountTransactionsFilter): Boolean {

View File

@ -36,7 +36,7 @@ class BankFinder {
return getBankList(maxItems)
}
return getBankList().asSequence().filter { it.bankCode.startsWith(query) }
return getBankList().asSequence().filter { it.domesticBankCode.startsWith(query) }
.max(maxItems)
}
@ -69,7 +69,7 @@ class BankFinder {
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.bankCode.startsWith(bankCode) }.max(2)
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

View File

@ -1,11 +1,11 @@
package net.codinux.banking.ui.service
import net.codinux.banking.client.model.BankingGroup
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.BankAccess
class BankIconService { // TODO: extract to a common library
fun findIconForBank(user: User) = findIconForBank(user.bankName, user.bic, user.bankingGroup)
fun findIconForBank(bank: BankAccess) = findIconForBank(bank.bankName, bank.bic, bank.bankingGroup)
fun findIconForBank(bankName: String, bic: String? = null, bankingGroup: BankingGroup? = null): String? = when (bankingGroup) {
BankingGroup.Sparkasse -> "https://sparkasse.de/favicon-32x32.png"

View File

@ -1,7 +1,9 @@
package net.codinux.banking.ui.service
import androidx.lifecycle.viewModelScope
import bankmeister.composeapp.generated.resources.Res
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.SimpleBankingClientCallback
import net.codinux.banking.client.fints4k.FinTs4kBankingClient
@ -22,6 +24,8 @@ import net.codinux.banking.ui.model.BankInfo
import net.codinux.banking.ui.model.error.*
import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent
import net.codinux.banking.ui.model.events.TransferredMoneyEvent
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.settings.UiSettings
import net.codinux.banking.ui.state.UiState
import net.codinux.csv.reader.CsvReader
import net.codinux.log.logger
@ -30,6 +34,7 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi
@OptIn(ExperimentalResourceApi::class)
class BankingService(
private val uiState: UiState,
private val uiSettings: UiSettings,
private val bankingRepository: BankingRepository,
private val bankFinder: BankFinder
) {
@ -45,21 +50,40 @@ class BankingService(
suspend fun init() {
try {
uiState.users.value = getAllUsers()
var appSettings = getAppSettings()
if (appSettings == null) {
appSettings = AppSettings()
saveAppSettings(appSettings)
}
uiState.appSettings.value = appSettings
bankingRepository.getUiSettings(uiSettings)
updateOnChanges(uiSettings)
uiState.banks.value = getAllBanks()
uiState.transactions.value = getAllAccountTransactionsAsViewModel()
uiState.holdings.value = uiState.users.value.flatMap { it.accounts }.flatMap { it.holdings }
uiState.holdings.value = uiState.banks.value.flatMap { it.accounts }.flatMap { it.holdings }
} catch (e: Throwable) {
log.error(e) { "Could not read all user accounts and account transactions from repository" }
log.error(e) { "Could not read all banks and account transactions from repository" }
}
}
fun getAllUsers() = bankingRepository.getAllUsers()
fun getAppSettings() = bankingRepository.getAppSettings()
suspend fun saveAppSettings(settings: AppSettings) = bankingRepository.saveAppSettings(settings)
suspend fun saveUiSettings(settings: UiSettings) = bankingRepository.saveUiSettings(settings)
fun getAllBanks() = bankingRepository.getAllBanks()
fun getAllAccountTransactions() = bankingRepository.getAllAccountTransactions()
fun getAllTransactionsOfUser(user: UserEntity) = bankingRepository.getAllTransactionsOfUser(user)
fun getAllTransactionsForBank(bank: BankAccessEntity) = bankingRepository.getAllTransactionsForBank(bank)
fun getAllAccountTransactionsAsViewModel() = bankingRepository.getAllAccountTransactionsAsViewModel()
@ -72,7 +96,7 @@ class BankingService(
suspend fun addAccount(bank: BankInfo, loginName: String, password: String, retrieveAllTransactions: Boolean = false): Boolean {
try {
val retrieveTransactions = if (retrieveAllTransactions) RetrieveTransactions.All else RetrieveTransactions.OfLast90Days
val response = client.getAccountDataAsync(GetAccountDataRequest(bank.bankCode, loginName, password, GetAccountDataOptions(retrieveTransactions), mapBankInfo(bank)))
val response = client.getAccountDataAsync(GetAccountDataRequest(bank.domesticBankCode, loginName, password, GetAccountDataOptions(retrieveTransactions), mapBankInfo(bank)))
if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulGetAccountDataResponse(response.data!!)
@ -101,21 +125,21 @@ class BankingService(
private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) {
try {
val newUser = response.user
newUser.displayIndex = uiState.users.value.size
val newBank = response.bank
newBank.displayIndex = uiState.banks.value.size
val newUserEntity = bankingRepository.persistUser(newUser)
val newBankEntity = bankingRepository.persistBank(newBank)
log.info { "Saved user account $newUserEntity with ${newUserEntity.accounts.flatMap { it.bookedTransactionsEntities }.size} transactions" }
log.info { "Saved bank $newBankEntity with ${newBankEntity.accounts.flatMap { it.bookedTransactions }.size} transactions" }
val users = uiState.users.value.toMutableList()
users.add(newUserEntity)
uiState.users.value = users
val banks = uiState.banks.value.toMutableList()
banks.add(newBankEntity)
uiState.banks.value = banks
updateTransactionsInUi(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities })
updateHoldingsInUi(newUserEntity.accounts.flatMap { it.holdings }, emptyList())
updateTransactionsInUi(newBankEntity.accounts.flatMap { it.bookedTransactions })
updateHoldingsInUi(newBankEntity.accounts.flatMap { it.holdings }, emptyList())
} catch (e: Throwable) {
log.error(e) { "Could not save user account ${response.user}" }
log.error(e) { "Could not save bank ${response.bank}" }
}
}
@ -123,45 +147,45 @@ class BankingService(
suspend fun updateAccountTransactions() {
val selectedAccount = uiState.transactionsFilter.value.selectedAccount
if (selectedAccount != null) {
updateAccountTransactions(selectedAccount.user, selectedAccount.bankAccount)
updateAccountTransactions(selectedAccount.bank, selectedAccount.bankAccount)
} else {
uiState.users.value.forEach { user ->
updateAccountTransactions(user)
uiState.banks.value.forEach { bank ->
updateAccountTransactions(bank)
}
}
}
private suspend fun updateAccountTransactions(user: UserEntity, bankAccount: BankAccountEntity? = null) {
private suspend fun updateAccountTransactions(bank: BankAccessEntity, bankAccount: BankAccountEntity? = null) {
withContext(Dispatchers.IOorDefault) {
try {
val response = client.updateAccountTransactionsAsync(user, bankAccount?.let { listOf(it) })
val response = client.updateAccountTransactionsAsync(bank, bankAccount?.let { listOf(it) })
if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulUpdateAccountTransactionsResponse(user, response.data!!)
handleSuccessfulUpdateAccountTransactionsResponse(bank, response.data!!)
} else {
handleUnsuccessfulBankingClientResponse(BankingClientAction.UpdateAccountTransactions, response)
}
} catch (e: Throwable) {
log.error(e) { "Could not update account transactions for $user" }
log.error(e) { "Could not update account transactions for $bank" }
}
}
}
private suspend fun handleSuccessfulUpdateAccountTransactionsResponse(user: UserEntity, responses: List<GetTransactionsResponse>) {
private suspend fun handleSuccessfulUpdateAccountTransactionsResponse(bank: BankAccessEntity, responses: List<GetTransactionsResponse>) {
try {
// TODO: when user gets updated by BankingClient, also update user in database
// val newUser = response.user
// TODO: when bank gets updated by BankingClient, also update bank in database
// val newUser = response.bank
// val newUserEntity = bankingRepository.persistUser(newUser)
//
// log.info { "Saved user account $newUserEntity with ${newUserEntity.accounts.flatMap { it.bookedTransactionsEntities }.size} transactions" }
val userTransactions = getAllTransactionsOfUser(user)
val transactionsForBank = getAllTransactionsForBank(bank)
responses.forEach { response ->
val account = (response.account as? BankAccountEntity) ?: user.accounts.first { it.identifier == response.account.identifier && it.subAccountNumber == response.account.subAccountNumber }
val account = (response.account as? BankAccountEntity) ?: bank.accounts.first { it.identifier == response.account.identifier && it.subAccountNumber == response.account.subAccountNumber }
// TODO: update BankAccount and may updated Transactions in database
val existingAccountTransactions = userTransactions.filter { it.bankAccountId == account.id }
val existingAccountTransactions = transactionsForBank.filter { it.accountId == account.id }
val newTransactions = modelService.findNewTransactions(response.bookedTransactions, existingAccountTransactions)
@ -181,18 +205,16 @@ class BankingService(
bankingRepository.updateHoldings(updateHoldings(updatedExistingHoldings, updatedRetrievedHoldings))
bankingRepository.deleteHoldings(deletedHoldings)
account.holdings = account.holdings.toMutableList().apply {
addAll(persistedNewHoldings)
removeAll(deletedHoldings)
}
account.holdings.removeAll(deletedHoldings)
account.addHoldings(persistedNewHoldings)
updateHoldingsInUi(persistedNewHoldings, deletedHoldings)
val transactionsViewModel = updateTransactionsInUi(newTransactionsEntities)
uiState.dispatchNewTransactionsRetrieved(AccountTransactionsRetrievedEvent(user, account, transactionsViewModel, response.holdings))
uiState.dispatchNewTransactionsRetrieved(AccountTransactionsRetrievedEvent(bank, account, transactionsViewModel, response.holdings))
}
} catch (e: Throwable) {
log.error(e) { "Could not save updated account transactions for user $user" }
log.error(e) { "Could not save updated account transactions for bank $bank" }
}
}
@ -235,11 +257,11 @@ class BankingService(
}
suspend fun transferMoney(user: UserEntity, account: BankAccountEntity,
suspend fun transferMoney(bank: BankAccessEntity, account: BankAccountEntity,
recipientName: String, recipientAccountIdentifier: String, amount: Amount, currency: String,
paymentReference: String? = null, instantTransfer: Boolean = false, recipientBankIdentifier: String? = null): Boolean {
val response = client.transferMoneyAsync(TransferMoneyRequestForUser(
user.bankCode, user.loginName, user.password!!,
bank.domesticBankCode, bank.loginName, bank.password!!,
BankAccountIdentifier(account.identifier, account.subAccountNumber, account.iban), // TODO: use BankingClient's one
recipientName, recipientAccountIdentifier, recipientBankIdentifier,
amount, "EUR",
@ -251,7 +273,7 @@ class BankingService(
} else if (response.type == ResponseType.Success) {
uiState.dispatchTransferredMoney(TransferredMoneyEvent(recipientName, amount, currency))
updateAccountTransactions(user, account)
updateAccountTransactions(bank, account)
}
return response.type == ResponseType.Success
@ -269,6 +291,24 @@ class BankingService(
}
private suspend fun updateOnChanges(uiSettings: UiSettings) {
updateOnChanges(uiSettings, uiSettings.transactionsGrouping)
updateOnChanges(uiSettings, uiSettings.showBalance)
updateOnChanges(uiSettings, uiSettings.showBankIcons)
updateOnChanges(uiSettings, uiSettings.showColoredAmounts)
updateOnChanges(uiSettings, uiSettings.showTransactionsInAlternatingColors)
}
private suspend fun updateOnChanges(uiSettings: UiSettings, state: MutableStateFlow<*>) {
uiSettings.viewModelScope.launch(Dispatchers.Unconfined) {
state.collect {
saveUiSettings(uiSettings)
}
}
}
private suspend fun readTransactionsFromCsv(): List<AccountTransaction> {
val csv = Res.readBytes("files/transactions.csv").decodeToString()
val csvReader = CsvReader(hasHeaderRow = true, reuseRowInstance = true, skipEmptyRows = true).read(csv)

View File

@ -1,7 +1,7 @@
package net.codinux.banking.ui.service
import net.codinux.banking.client.model.*
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.AccountTransactionsFilter
@ -10,8 +10,8 @@ class CalculatorService {
fun sumTransactions(transactions: Collection<AccountTransactionViewModel>): Amount =
transactions.map { it.amount }.sum()
fun calculateBalanceOfUser(user: User): Amount =
sumAmounts(user.accounts.map { it.balance })
fun calculateBalanceOfBankAccess(bank: BankAccess): Amount =
sumAmounts(bank.accounts.map { it.balance })
fun sumAmounts(amounts: Collection<Amount>): Amount =
amounts.sum()
@ -22,9 +22,9 @@ class CalculatorService {
fun sumExpenses(transactions: Collection<AccountTransactionViewModel>): Amount =
sumAmounts(transactions.map { it.amount }.filter { it.isNegative })
fun calculateBalanceOfDisplayedTransactions(transactions: Collection<AccountTransactionViewModel>, users: Collection<UserEntity>, filter: AccountTransactionsFilter): Amount {
fun calculateBalanceOfDisplayedTransactions(transactions: Collection<AccountTransactionViewModel>, banks: Collection<BankAccessEntity>, filter: AccountTransactionsFilter): Amount {
if (filter.noFiltersApplied) {
return sumAmounts(users.flatMap { it.accounts.map { it.balance } })
return sumAmounts(banks.flatMap { it.accounts.map { it.balance } })
}
val selectedAccount = filter.selectedAccount
@ -33,7 +33,7 @@ class CalculatorService {
if (selectedAccount.bankAccount != null) {
selectedAccount.bankAccount.balance
} else {
calculateBalanceOfUser(selectedAccount.user)
calculateBalanceOfBankAccess(selectedAccount.bank)
}
} else {
sumTransactions(transactions)

View File

@ -10,7 +10,7 @@ class UiSettings : ViewModel() {
val transactionsGrouping = MutableStateFlow(TransactionsGrouping.Month)
val zebraStripes = MutableStateFlow(true)
val showTransactionsInAlternatingColors = MutableStateFlow(true)
val showBankIcons = MutableStateFlow(true)

View File

@ -8,17 +8,21 @@ import kotlinx.coroutines.flow.MutableStateFlow
import net.codinux.banking.client.model.tan.EnterTanResult
import net.codinux.banking.client.model.tan.TanChallenge
import net.codinux.banking.dataaccess.entities.HoldingEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.model.*
import net.codinux.banking.ui.model.error.ApplicationError
import net.codinux.banking.ui.model.error.BankingClientError
import net.codinux.banking.ui.model.error.ErroneousAction
import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent
import net.codinux.banking.ui.model.events.TransferredMoneyEvent
import net.codinux.banking.ui.model.settings.AppSettings
class UiState : ViewModel() {
val users = MutableStateFlow<List<UserEntity>>(emptyList())
val appSettings = MutableStateFlow(AppSettings())
val banks = MutableStateFlow<List<BankAccessEntity>>(emptyList())
val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())

View File

@ -3,8 +3,8 @@ import kotlin.Boolean;
CREATE TABLE IF NOT EXISTS AccountTransaction (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
bankAccountId INTEGER NOT NULL,
bankId INTEGER NOT NULL,
accountId INTEGER NOT NULL,
amount TEXT NOT NULL,
currency TEXT NOT NULL,
@ -22,7 +22,8 @@ CREATE TABLE IF NOT EXISTS AccountTransaction (
openingBalance TEXT,
closingBalance TEXT,
userSetDisplayName TEXT,
userSetReference TEXT,
userSetOtherPartyName TEXT,
category TEXT,
notes TEXT,
@ -53,10 +54,16 @@ CREATE TABLE IF NOT EXISTS AccountTransaction (
isReversal INTEGER AS Boolean NOT NULL
);
CREATE INDEX idx_AccountTransaction_bankId
ON AccountTransaction (bankId);
CREATE INDEX idx_AccountTransaction_accountId
ON AccountTransaction (accountId);
insertTransaction:
INSERT INTO AccountTransaction(
userId, bankAccountId,
bankId, accountId,
amount, currency, reference,
bookingDate, valueDate,
@ -66,7 +73,8 @@ INSERT INTO AccountTransaction(
openingBalance, closingBalance,
userSetDisplayName, category, notes,
userSetReference, userSetOtherPartyName,
category, notes,
statementNumber, sheetNumber,
@ -96,7 +104,8 @@ VALUES(
?, ?,
?, ?, ?,
?, ?,
?, ?,
?, ?,
@ -118,18 +127,18 @@ VALUES(
);
selectAllTransactions:
getAllTransactions:
SELECT AccountTransaction.*
FROM AccountTransaction;
selectAllTransactionsAsViewModel:
SELECT id, userId, bankAccountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, category
getAllTransactionsAsViewModel:
SELECT id, bankId, accountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetReference, userSetOtherPartyName
FROM AccountTransaction;
selectAllTransactionsOfUser:
getAllTransactionsForBank:
SELECT AccountTransaction.*
FROM AccountTransaction WHERE userId = ?;
FROM AccountTransaction WHERE bankId = ?;
getTransactionWithId:
SELECT AccountTransaction.*
@ -141,8 +150,8 @@ FROM AccountTransaction WHERE id = ?;
CREATE TABLE IF NOT EXISTS Holding (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
bankAccountId INTEGER NOT NULL,
bankId INTEGER NOT NULL,
accountId INTEGER NOT NULL,
name TEXT NOT NULL,
isin TEXT,
@ -163,10 +172,16 @@ CREATE TABLE IF NOT EXISTS Holding (
buyingDate TEXT
);
CREATE INDEX idx_Holding_bankId
ON Holding (bankId);
CREATE INDEX idx_Holding_accountId
ON Holding (accountId);
insertHolding:
INSERT INTO Holding(
userId, bankAccountId,
bankId, accountId,
name, isin, wkn,

View File

@ -1,24 +1,25 @@
import kotlin.Boolean;
CREATE TABLE IF NOT EXISTS User (
CREATE TABLE IF NOT EXISTS BankAccess (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bankCode TEXT NOT NULL,
domesticBankCode TEXT NOT NULL,
loginName TEXT NOT NULL,
password TEXT,
bankName TEXT NOT NULL,
bic TEXT NOT NULL,
bic TEXT,
customerName TEXT NOT NULL,
userId TEXT,
selectedTanMethodId TEXT,
selectedTanMethodIdentifier TEXT,
selectedTanMediumName TEXT,
selectedTanMediumIdentifier TEXT,
bankingGroup TEXT,
serverAddress TEXT,
countryCode TEXT NOT NULL,
clientData TEXT,
@ -31,20 +32,21 @@ CREATE TABLE IF NOT EXISTS User (
);
insertUser:
INSERT INTO User(
bankCode, loginName, password,
insertBank:
INSERT INTO BankAccess(
domesticBankCode, loginName, password,
bankName, bic,
customerName, userId,
selectedTanMethodId,
selectedTanMethodIdentifier,
selectedTanMediumName,
selectedTanMediumIdentifier,
bankingGroup,
serverAddress,
countryCode,
clientData,
@ -61,7 +63,7 @@ VALUES(
?,
?, ?,
?, ?, ?,
?,
@ -72,16 +74,16 @@ VALUES(
);
selectAllUsers:
SELECT User.*
FROM User;
getAllBanks:
SELECT BankAccess.*
FROM BankAccess;
CREATE TABLE IF NOT EXISTS BankAccount (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
bankId INTEGER NOT NULL,
identifier TEXT NOT NULL,
subAccountNumber TEXT,
@ -109,10 +111,13 @@ CREATE TABLE IF NOT EXISTS BankAccount (
includeInAutomaticAccountsUpdate INTEGER AS Boolean NOT NULL
);
CREATE INDEX idx_BankAccount_bankId
ON BankAccount (bankId);
insertBankAccount:
INSERT INTO BankAccount(
userId,
bankId,
identifier, accountHolderName, type,
iban, subAccountNumber, productName, currency, accountLimit,
@ -145,7 +150,7 @@ VALUES(
);
selectAllBankAccounts:
getAllBankAccounts:
SELECT BankAccount.*
FROM BankAccount;
@ -154,25 +159,32 @@ FROM BankAccount;
CREATE TABLE IF NOT EXISTS TanMethod (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
bankId INTEGER NOT NULL,
displayName TEXT NOT NULL,
type TEXT NOT NULL,
identifier TEXT NOT NULL,
maxTanInputLength INTEGER ,
allowedTanFormat TEXT NOT NULL
allowedTanFormat TEXT NOT NULL,
userSetDisplayName TEXT
);
CREATE INDEX idx_TanMethod_bankId
ON TanMethod (bankId);
insertTanMethod:
INSERT INTO TanMethod(
userId,
bankId,
displayName,
type,
identifier,
maxTanInputLength,
allowedTanFormat
allowedTanFormat,
userSetDisplayName
)
VALUES (
?,
@ -181,11 +193,13 @@ VALUES (
?,
?,
?,
?,
?
);
selectAllTanMethods:
getAllTanMethods:
SELECT TanMethod.*
FROM TanMethod;
@ -194,7 +208,7 @@ FROM TanMethod;
CREATE TABLE IF NOT EXISTS TanMedium (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
bankId INTEGER NOT NULL,
type TEXT NOT NULL,
mediumName TEXT,
@ -209,13 +223,18 @@ CREATE TABLE IF NOT EXISTS TanMedium (
cardSequenceNumber TEXT,
cardType INTEGER,
validFrom TEXT,
validTo TEXT
validTo TEXT,
userSetDisplayName TEXT
);
CREATE INDEX idx_TanMedium_bankId
ON TanMedium (bankId);
insertTanMedium:
INSERT INTO TanMedium(
userId,
bankId,
type,
mediumName,
@ -228,7 +247,9 @@ INSERT INTO TanMedium(
cardSequenceNumber,
cardType,
validFrom,
validTo
validTo,
userSetDisplayName
)
VALUES (
?,
@ -244,11 +265,13 @@ VALUES (
?,
?,
?,
?,
?
);
selectAllTanMedia:
getAllTanMedia:
SELECT TanMedium.*
FROM TanMedium;

View File

@ -0,0 +1,72 @@
import kotlin.Boolean;
CREATE TABLE IF NOT EXISTS AppSettings (
id INTEGER PRIMARY KEY,
authenticationMethod TEXT NOT NULL,
hashedPassword TEXT,
updateAccountsOnAppStart INTEGER AS Boolean NOT NULL,
updateAccountsIfNoUpdatedForHours INTEGER NOT NULL,
sideMenuWidth INTEGER NOT NULL,
windowPositionX INTEGER NOT NULL,
windowPositionY INTEGER NOT NULL,
windowWidth INTEGER NOT NULL,
windowHeight INTEGER NOT NULL,
windowState TEXT
);
getAppSettings:
SELECT * FROM AppSettings WHERE id = 1;
upsertAppSettings:
INSERT OR REPLACE INTO AppSettings(
id,
authenticationMethod, hashedPassword, updateAccountsOnAppStart, updateAccountsIfNoUpdatedForHours,
sideMenuWidth,
windowPositionX, windowPositionY, windowWidth, windowHeight,
windowState
)
VALUES (
1,
?, ?, ?, ?,
?,
?, ?,
?, ?,
?
);
CREATE TABLE IF NOT EXISTS UiSettings (
id INTEGER PRIMARY KEY,
transactionsGrouping TEXT NOT NULL,
showBalance INTEGER AS Boolean NOT NULL,
showBankIcons INTEGER AS Boolean NOT NULL,
showColoredAmounts INTEGER AS Boolean NOT NULL,
showTransactionsInAlternatingColors INTEGER AS Boolean NOT NULL
);
getUiSettings:
SELECT * FROM UiSettings WHERE id = 1;
upsertUiSettings:
INSERT OR REPLACE INTO UiSettings(id, transactionsGrouping, showBalance, showBankIcons, showColoredAmounts, showTransactionsInAlternatingColors)
VALUES (1, ?, ?, ?, ?, ?);

View File

@ -13,8 +13,8 @@ import net.codinux.banking.ui.model.TanChallengeReceived
@Composable
fun EnterTanDialogPreview_EnterTan() {
val tanMethods = listOf(TanMethod("Zeig mich an", TanMethodType.AppTan, "902"))
val user = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
val tanChallenge = TanChallenge(TanChallengeType.EnterTan, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, user = user)
val bank = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
val tanChallenge = TanChallenge(TanChallengeType.EnterTan, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}
@ -27,9 +27,9 @@ fun EnterTanDialogPreview_TanImage() {
val tanMethod = TanMethod("photoTAN-Verfahren", TanMethodType.photoTan, "902", 6, AllowedTanFormat.Numeric)
val tanImage = TanImage("image/png", tanImageBytes)
val user = BankViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank", BankingGroup.Comdirect)
val bank = BankViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank", BankingGroup.Comdirect)
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethod.identifier, listOf(tanMethod), null, emptyList(), tanImage, null, user)
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethod.identifier, listOf(tanMethod), null, emptyList(), tanImage, null, bank)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}
@ -50,10 +50,10 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
TanMedium(TanMediumType.TanGenerator, "SparkassenCard (Debitkarte)", TanMediumStatus.Used, TanGeneratorTanMedium("5432109876"))
)
val user = BankViewInfo("10010010", "Ihr krasser Login Name", "Eine ganz gewöhnliche Sparkasse", BankingGroup.Sparkasse)
val bank = BankViewInfo("10010010", "Ihr krasser Login Name", "Eine ganz gewöhnliche Sparkasse", BankingGroup.Sparkasse)
val account = BankAccountViewInfo("12345678", null, BankAccountType.CheckingAccount, null, "Giro Konto")
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetTransactions, "Sie möchten eine \"Umsatzabfrage\" freigeben: Bitte bestätigen Sie den \"Startcode 80061030\" mit der Taste \"OK\".", "913", tanMethods, "SparkassenCard (Debitkarte)", tanMedia, tanImage, null, user, account)
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetTransactions, "Sie möchten eine \"Umsatzabfrage\" freigeben: Bitte bestätigen Sie den \"Startcode 80061030\" mit der Taste \"OK\".", "913", tanMethods, "SparkassenCard (Debitkarte)", tanMedia, tanImage, null, bank, account)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}

View File

@ -17,8 +17,8 @@ class SqliteBankingRepositoryTest {
}
private val underTest = object : SqliteBankingRepository(sqlDriver) {
override public suspend fun persistTransaction(userId: Long, bankAccountId: Long, transaction: AccountTransaction): AccountTransactionEntity =
super.persistTransaction(userId, bankAccountId, transaction)
override public suspend fun persistTransaction(bankId: Long, accountId: Long, transaction: AccountTransaction): AccountTransactionEntity =
super.persistTransaction(bankId, accountId, transaction)
}
@ -27,35 +27,35 @@ class SqliteBankingRepositoryTest {
val bankAccounts = listOf(
BankAccount("12345", null, null, null, "Monika Tester", BankAccountType.CheckingAccount, balance = Amount("12.34"), retrievedTransactionsFrom = LocalDate(2024, 5, 7), features = setOf(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.InstantTransfer), serverTransactionsRetentionDays = 320)
)
val user = User("12345678", "SupiDupiUser", "geheim", "Abzock-Bank", "ABCDDEBBXXX", "Monika Tester", accounts = bankAccounts, bankingGroup = BankingGroup.DKB, serverAddress = "").apply {
val bank = BankAccess("12345678", "SupiDupiUser", "geheim", "Abzock-Bank", "ABCDDEBBXXX", "Monika Tester", accounts = bankAccounts, bankingGroup = BankingGroup.DKB, serverAddress = "").apply {
wrongCredentialsEntered = true
displayIndex = 99
}
val persisted = underTest.persistUser(user)
val persisted = underTest.persistBank(bank)
assertNotNull(persisted.id)
assertEquals(user.bankCode, persisted.bankCode)
assertEquals(user.loginName, persisted.loginName)
assertEquals(user.password, persisted.password)
assertEquals(bank.domesticBankCode, persisted.domesticBankCode)
assertEquals(bank.loginName, persisted.loginName)
assertEquals(bank.password, persisted.password)
assertEquals(user.bankName, persisted.bankName)
assertEquals(user.bic, persisted.bic)
assertEquals(bank.bankName, persisted.bankName)
assertEquals(bank.bic, persisted.bic)
assertEquals(user.customerName, persisted.customerName)
assertEquals(user.userId, persisted.userId)
assertEquals(bank.customerName, persisted.customerName)
assertEquals(bank.userId, persisted.userId)
assertEquals(user.bankingGroup, persisted.bankingGroup)
assertEquals(bank.bankingGroup, persisted.bankingGroup)
assertEquals(user.wrongCredentialsEntered, persisted.wrongCredentialsEntered)
assertEquals(user.displayIndex, persisted.displayIndex)
assertEquals(bank.wrongCredentialsEntered, persisted.wrongCredentialsEntered)
assertEquals(bank.displayIndex, persisted.displayIndex)
assertEquals(1, persisted.accounts.size)
val persistedBankAccount = persisted.accounts.first()
assertNotNull(persistedBankAccount.id)
assertEquals(persisted.id, persistedBankAccount.userId)
assertEquals(persisted.id, persistedBankAccount.bankId)
assertEquals(bankAccounts.first().identifier, persistedBankAccount.identifier)
assertEquals(bankAccounts.first().accountHolderName, persistedBankAccount.accountHolderName)

View File

@ -14,7 +14,7 @@ sqlDelight = "2.0.2"
agp = "8.5.2"
android-compileSdk = "34"
android-minSdk = "24"
android-minSdk = "23"
android-targetSdk = "34"
androidx-activityCompose = "1.9.1"
androidx-appcompat = "1.7.0"