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") sourceSets["main"].resources.srcDirs("src/commonMain/resources")
defaultConfig { 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() minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1 versionCode = 10
versionName = "1.0.0-Alpha-12" versionName = "1.0.0-Alpha-12"
} }
packaging { packaging {
@ -148,18 +148,26 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
buildTypes { buildTypes {
getByName("release") { named("debug") {
applicationIdSuffix = ".develop"
}
named("release") {
isMinifyEnabled = false isMinifyEnabled = false
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
buildFeatures { buildFeatures {
compose = true compose = true
} }
dependencies { dependencies {
debugImplementation(compose.uiTooling) 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 @Composable
fun EnterTanDialogPreview_EnterTan() { fun EnterTanDialogPreview_EnterTan() {
val tanMethods = listOf(TanMethod("Zeig mich an", TanMethodType.AppTan, "902")) val tanMethods = listOf(TanMethod("Zeig mich an", TanMethodType.AppTan, "902"))
val user = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank) val bank = 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 tanChallenge = TanChallenge(TanChallengeType.EnterTan, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { } EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
} }
@ -27,9 +27,9 @@ fun EnterTanDialogPreview_TanImage() {
val tanMethod = TanMethod("photoTAN-Verfahren", TanMethodType.photoTan, "902", 6, AllowedTanFormat.Numeric) val tanMethod = TanMethod("photoTAN-Verfahren", TanMethodType.photoTan, "902", 6, AllowedTanFormat.Numeric)
val tanImage = TanImage("image/png", tanImageBytes) 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) { }) { } EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
} }
@ -50,10 +50,10 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
TanMedium(TanMediumType.TanGenerator, "SparkassenCard (Debitkarte)", TanMediumStatus.Used, TanGeneratorTanMedium("5432109876")) 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 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) { }) { } EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
} }
@ -62,8 +62,8 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
@Composable @Composable
fun EnterTanDialogPreview_Flickercode() { fun EnterTanDialogPreview_Flickercode() {
val tanMethods = listOf(TanMethod("chipTAN Flickercode", TanMethodType.ChipTanFlickercode, "902")) val tanMethods = listOf(TanMethod("chipTAN Flickercode", TanMethodType.ChipTanFlickercode, "902"))
val user = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank) val bank = 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 tanChallenge = TanChallenge(TanChallengeType.Flickercode, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank, flickerCode = FlickerCode("", ""))
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { } 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 package net.codinux.banking.dataaccess
import net.codinux.banking.client.model.AccountTransaction 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.client.model.securitiesaccount.Holding
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.HoldingEntity 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.AccountTransactionViewModel
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.settings.UiSettings
interface BankingRepository { 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> suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity>
@ -29,7 +40,7 @@ interface BankingRepository {
fun getAllAccountTransactions(): List<AccountTransactionEntity> fun getAllAccountTransactions(): List<AccountTransactionEntity>
fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity> fun getAllTransactionsForBank(bank: BankAccessEntity): List<AccountTransactionEntity>
fun getTransactionById(transactionId: Long): AccountTransactionEntity? fun getTransactionById(transactionId: Long): AccountTransactionEntity?

View File

@ -1,31 +1,51 @@
package net.codinux.banking.dataaccess package net.codinux.banking.dataaccess
import net.codinux.banking.client.model.AccountTransaction 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.client.model.securitiesaccount.Holding
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.HoldingEntity 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.AccountTransactionViewModel
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.settings.UiSettings
class InMemoryBankingRepository( class InMemoryBankingRepository(
users: Collection<User> = emptyList(), banks: Collection<BankAccess> = emptyList(),
transactions: Collection<AccountTransaction> = emptyList() transactions: Collection<AccountTransaction> = emptyList(),
private var appSettings: AppSettings = AppSettings()
) : BankingRepository { ) : BankingRepository {
private var nextId = 0L // TODO: make thread-safe 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 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 { override fun getAppSettings(): AppSettings? = appSettings
val entity = map(user) // TODO: may fix someday and add also BankAccounts and their id
this.users.add(entity) 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 return entity
} }
@ -50,27 +70,27 @@ class InMemoryBankingRepository(
override fun getAllAccountTransactions(): List<AccountTransactionEntity> = transactions.toList() override fun getAllAccountTransactions(): List<AccountTransactionEntity> = transactions.toList()
override fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity> = override fun getAllTransactionsForBank(bank: BankAccessEntity): List<AccountTransactionEntity> =
getAllAccountTransactions().filter { it.userId == user.id } getAllAccountTransactions().filter { it.bankId == bank.id }
override fun getTransactionById(transactionId: Long): AccountTransactionEntity? = override fun getTransactionById(transactionId: Long): AccountTransactionEntity? =
getAllAccountTransactions().firstOrNull { it.id == transactionId } getAllAccountTransactions().firstOrNull { it.id == transactionId }
private fun map(account: User) = UserEntity( private fun map(bank: BankAccess) = BankAccessEntity(
nextId++, 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 // TODO: may fix someday and also add BankAccounts
emptyList(), account.selectedTanMethodIdentifier, emptyList(), account.selectedTanMediumIdentifier, emptyList(), emptyList(), bank.selectedTanMethodIdentifier, mutableListOf(), bank.selectedTanMediumIdentifier, mutableListOf(),
account.bankingGroup, account.serverAddress, bank.bankingGroup, bank.serverAddress, bank.countryCode,
account.userSetDisplayName, account.displayIndex, bank.userSetDisplayName, bank.displayIndex,
account.iconUrl, account.wrongCredentialsEntered, bank.iconUrl, bank.wrongCredentialsEntered,
) )
// TODO: someday may fix and get userId and bankAccountId // TODO: someday may fix and get bankId and accountId
private fun map(transaction: AccountTransaction, userId: Long = nextId++, bankAccountId: Long = nextId++) = AccountTransactionEntity( private fun map(transaction: AccountTransaction, bankId: Long = nextId++, accountId: Long = nextId++) = AccountTransactionEntity(
nextId++, nextId++,
userId, bankAccountId, bankId, accountId,
transaction.amount, transaction.currency, transaction.reference, transaction.amount, transaction.currency, transaction.reference,
transaction.bookingDate, transaction.valueDate, transaction.bookingDate, transaction.valueDate,
transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId, 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.client.model.tan.*
import net.codinux.banking.dataaccess.entities.* import net.codinux.banking.dataaccess.entities.*
import net.codinux.banking.ui.model.AccountTransactionViewModel 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 net.codinux.log.logger
import kotlin.enums.EnumEntries import kotlin.enums.EnumEntries
import kotlin.js.JsName import kotlin.js.JsName
@ -19,53 +23,90 @@ open class SqliteBankingRepository(
private val database = BankmeisterDb(sqlDriver) 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 accountTransactionQueries = database.accountTransactionQueries
private val log by logger() private val log by logger()
override fun getAllUsers(): List<UserEntity> { override fun getAppSettings(): AppSettings? =
val bankAccounts = getAllBankAccounts().groupBy { it.userId } settingsQueries.getAppSettings { _,
val tanMethods = getAllTanMethods().groupBy { it.userId } authenticationMethod, hashedPassword, updateAccountsOnAppStart, updateAccountsIfNoUpdatedForHours,
val tanMedia = getAllTanMedia().groupBy { it.userId } sideMenuWidth,
val holdings = getAllHoldings().groupBy { it.bankAccountId } 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 -> override suspend fun saveAppSettings(settings: AppSettings) {
UserEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, getAccountsOfUser(id, bankAccounts, holdings), selectedTanMethodIdentifier, tanMethods[id] ?: emptyList(), selectedTanMediumIdentifier, tanMedia[id] ?: emptyList(), settingsQueries.upsertAppSettings(
bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered) 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() }.executeAsList()
} }
protected open fun getAccountsOfUser(userId: Long, bankAccounts: Map<Long, List<BankAccountEntity>>, holdings: Map<Long, List<HoldingEntity>>): List<BankAccountEntity> { protected open fun getAccountsOfBank(bankId: Long, bankAccounts: Map<Long, List<BankAccountEntity>>, holdings: Map<Long, List<HoldingEntity>>): List<BankAccountEntity> {
return bankAccounts[userId].orEmpty().onEach { return bankAccounts[bankId].orEmpty().onEach {
it.holdings = holdings[it.id].orEmpty() it.addHoldings(holdings[it.id].orEmpty())
} }
} }
override suspend fun persistUser(user: User): UserEntity { override suspend fun persistBank(bank: BankAccess): BankAccessEntity {
return userQueries.transactionWithResult { return bankQueries.transactionWithResult {
userQueries.insertUser(user.bankCode, user.loginName, user.password, user.bankName, user.bic, bankQueries.insertBank(bank.domesticBankCode, bank.loginName, bank.password, bank.bankName, bank.bic,
user.customerName, user.userId, user.selectedTanMethodIdentifier, user.selectedTanMediumIdentifier, bank.customerName, bank.userId, bank.selectedTanMethodIdentifier, bank.selectedTanMediumIdentifier,
user.bankingGroup?.name, user.serverAddress, null, user.userSetDisplayName, user.displayIndex.toLong(), user.iconUrl, user.wrongCredentialsEntered 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 tanMethods = persistTanMethods(bankId, bank.tanMethods)
val tanMedia = persistTanMedia(userId, user.tanMedia) 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( BankAccountEntity(
id, userId, id, bankId,
identifier, subAccountNumber, iban, productName, identifier, subAccountNumber, iban, productName,
@ -79,22 +120,22 @@ open class SqliteBankingRepository(
mapToInt(serverTransactionsRetentionDays), mapToInt(serverTransactionsRetentionDays),
mapToInstant(lastAccountUpdateTime), mapToDate(retrievedTransactionsFrom), mapToInstant(lastAccountUpdateTime), mapToDate(retrievedTransactionsFrom),
mutableListOf(), mutableListOf(), emptyList(), mutableListOf(), mutableListOf(), mutableListOf(),
userSetDisplayName, mapToInt(displayIndex), userSetDisplayName, mapToInt(displayIndex),
hideAccount, includeInAutomaticAccountsUpdate hideAccount, includeInAutomaticAccountsUpdate
) )
}.executeAsList() }.executeAsList()
private suspend fun persistBankAccounts(userId: Long, bankAccounts: Collection<BankAccount>): List<BankAccountEntity> = private suspend fun persistBankAccounts(bankId: Long, bankAccounts: Collection<BankAccount>): List<BankAccountEntity> =
bankAccounts.map { persistBankAccount(userId, it) } bankAccounts.map { persistBankAccount(bankId, it) }
/** /**
* Has to be executed in a transaction in order that getting persisted BankAccount's id works~ * 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 { private suspend fun persistBankAccount(bankId: Long, account: BankAccount): BankAccountEntity {
userQueries.insertBankAccount( bankQueries.insertBankAccount(
userId, bankId,
account.identifier, account.accountHolderName, mapEnum(account.type), account.identifier, account.accountHolderName, mapEnum(account.type),
account.iban, account.subAccountNumber, account.productName, account.currency, account.accountLimit, account.iban, account.subAccountNumber, account.productName, account.currency, account.accountLimit,
@ -111,49 +152,53 @@ open class SqliteBankingRepository(
val accountId = getLastInsertedId() val accountId = getLastInsertedId()
val accountTransactionEntities = account.bookedTransactions.map { transaction -> 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( TanMethodEntity(
id, id,
userId, bankId,
displayName, displayName,
mapToEnum(type, TanMethodType.entries), mapToEnum(type, TanMethodType.entries),
identifier, identifier,
mapToInt(maxTanInputLength), mapToInt(maxTanInputLength),
mapToEnum(allowedTanFormat, AllowedTanFormat.entries) mapToEnum(allowedTanFormat, AllowedTanFormat.entries),
userSetDisplayName
) )
}.executeAsList() }.executeAsList()
private suspend fun persistTanMethods(userId: Long, tanMethods: List<TanMethod>): List<TanMethodEntity> = private suspend fun persistTanMethods(bankId: Long, tanMethods: List<TanMethod>): List<TanMethodEntity> =
tanMethods.map { persistTanMethod(userId, it) } tanMethods.map { persistTanMethod(bankId, it) }
private suspend fun persistTanMethod(userId: Long, tanMethod: TanMethod): TanMethodEntity { private suspend fun persistTanMethod(bankId: Long, tanMethod: TanMethod): TanMethodEntity {
userQueries.insertTanMethod( bankQueries.insertTanMethod(
userId, bankId,
tanMethod.displayName, tanMethod.displayName,
mapEnum(tanMethod.type), mapEnum(tanMethod.type),
tanMethod.identifier, tanMethod.identifier,
mapInt(tanMethod.maxTanInputLength), mapInt(tanMethod.maxTanInputLength),
mapEnum(tanMethod.allowedTanFormat) mapEnum(tanMethod.allowedTanFormat),
tanMethod.userSetDisplayName
) )
val tanMethodId = getLastInsertedId() 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) { val mobilePhone = if (phoneNumber != null || concealedPhoneNumber != null) {
MobilePhoneTanMedium(phoneNumber, concealedPhoneNumber) MobilePhoneTanMedium(phoneNumber, concealedPhoneNumber)
} else { } else {
@ -168,23 +213,25 @@ open class SqliteBankingRepository(
TanMediumEntity( TanMediumEntity(
id, id,
userId, bankId,
mapToEnum(type, TanMediumType.entries), mapToEnum(type, TanMediumType.entries),
mediumName, mediumName,
mapToEnum(status, TanMediumStatus.entries), mapToEnum(status, TanMediumStatus.entries),
tanGenerator, tanGenerator,
mobilePhone mobilePhone,
userSetDisplayName
) )
}.executeAsList() }.executeAsList()
private suspend fun persistTanMedia(userId: Long, tanMedia: List<TanMedium>): List<TanMediumEntity> = private suspend fun persistTanMedia(bankId: Long, tanMedia: List<TanMedium>): List<TanMediumEntity> =
tanMedia.map { persistTanMedium(userId, it) } tanMedia.map { persistTanMedium(bankId, it) }
private suspend fun persistTanMedium(userId: Long, medium: TanMedium): TanMediumEntity { private suspend fun persistTanMedium(bankId: Long, medium: TanMedium): TanMediumEntity {
userQueries.insertTanMedium( bankQueries.insertTanMedium(
userId, bankId,
mapEnum(medium.type), mapEnum(medium.type),
medium.mediumName, medium.mediumName,
@ -197,31 +244,33 @@ open class SqliteBankingRepository(
medium.tanGenerator?.cardSequenceNumber, medium.tanGenerator?.cardSequenceNumber,
mapInt(medium.tanGenerator?.cardType), mapInt(medium.tanGenerator?.cardType),
mapDate(medium.tanGenerator?.validFrom), mapDate(medium.tanGenerator?.validFrom),
mapDate(medium.tanGenerator?.validTo) mapDate(medium.tanGenerator?.validTo),
medium.userSetDisplayName
) )
val tanMediumId = getLastInsertedId() val tanMediumId = getLastInsertedId()
return TanMediumEntity(tanMediumId, userId, medium) return TanMediumEntity(tanMediumId, bankId, medium)
} }
protected open fun getAllHoldings(): List<HoldingEntity> = protected open fun getAllHoldings(): List<HoldingEntity> =
accountTransactionQueries.selectAllHoldings { id, userId, bankAccountId, name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate -> accountTransactionQueries.selectAllHoldings { id, bankId, accountId, 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)) 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() }.executeAsList()
override suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity> = override suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity> =
accountTransactionQueries.transactionWithResult { 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~ * 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( accountTransactionQueries.insertHolding(
userId, bankAccountId, bankId, accountId,
holding.name, holding.isin, holding.wkn, holding.name, holding.isin, holding.wkn,
@ -234,7 +283,7 @@ open class SqliteBankingRepository(
mapInstant(holding.pricingTime), mapDate(holding.buyingDate) 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>) { override suspend fun updateHoldings(holdings: List<HoldingEntity>) {
@ -266,16 +315,16 @@ open class SqliteBankingRepository(
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> = override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
accountTransactionQueries.selectAllTransactionsAsViewModel { id, userId, bankAccountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, category -> accountTransactionQueries.getAllTransactionsAsViewModel { id, bankId, accountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, userSetOtherPartyName ->
AccountTransactionViewModel(id, userId, bankAccountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, category) AccountTransactionViewModel(id, bankId, accountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, userSetOtherPartyName)
}.executeAsList() }.executeAsList()
override fun getAllAccountTransactions(): List<AccountTransactionEntity> { override fun getAllAccountTransactions(): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactions(::mapTransaction).executeAsList() return accountTransactionQueries.getAllTransactions(::mapTransaction).executeAsList()
} }
override fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity> { override fun getAllTransactionsForBank(bank: BankAccessEntity): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactionsOfUser(user.id, ::mapTransaction).executeAsList() return accountTransactionQueries.getAllTransactionsForBank(bank.id, ::mapTransaction).executeAsList()
} }
override fun getTransactionById(transactionId: Long): AccountTransactionEntity? = override fun getTransactionById(transactionId: Long): AccountTransactionEntity? =
@ -285,7 +334,7 @@ open class SqliteBankingRepository(
override suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity> { override suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity> {
return accountTransactionQueries.transactionWithResult { return accountTransactionQueries.transactionWithResult {
transactions.map { transaction -> 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~ * 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( accountTransactionQueries.insertTransaction(
userId, bankAccountId, bankId, accountId,
mapAmount(transaction.amount), transaction.currency, transaction.reference, mapAmount(transaction.amount), transaction.currency, transaction.reference,
mapDate(transaction.bookingDate), mapDate(transaction.valueDate), mapDate(transaction.bookingDate), mapDate(transaction.valueDate),
@ -305,7 +354,8 @@ open class SqliteBankingRepository(
mapAmount(transaction.openingBalance), mapAmount(transaction.closingBalance), 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(), transaction.statementNumber?.toLong(), transaction.sheetNumber?.toLong(),
@ -326,16 +376,16 @@ open class SqliteBankingRepository(
transaction.isReversal transaction.isReversal
) )
return AccountTransactionEntity(getLastInsertedId(), userId, bankAccountId, transaction) return AccountTransactionEntity(getLastInsertedId(), bankId, accountId, transaction)
} }
private fun getLastInsertedId(): Long = private fun getLastInsertedId(): Long =
userQueries.getLastInsertedId().executeAsOne() bankQueries.getLastInsertedId().executeAsOne()
private fun mapTransaction( private fun mapTransaction(
id: Long, userId: Long, bankAccountId: Long, id: Long, bankId: Long, accountId: Long,
amount: String, currency: String, reference: String?, amount: String, currency: String, reference: String?,
bookingDate: String, valueDate: String, bookingDate: String, valueDate: String,
@ -345,7 +395,8 @@ open class SqliteBankingRepository(
openingBalance: String?, closingBalance: String?, openingBalance: String?, closingBalance: String?,
userSetDisplayName: String?, category: String?, notes: String?, userSetDisplayName: String?, userSetReference: String?,
category: String?, notes: String?,
statementNumber: Long?, sheetNumber: Long?, statementNumber: Long?, sheetNumber: Long?,
@ -366,7 +417,7 @@ open class SqliteBankingRepository(
isReversal: Boolean isReversal: Boolean
): AccountTransactionEntity = AccountTransactionEntity( ): AccountTransactionEntity = AccountTransactionEntity(
id, id,
userId, bankAccountId, bankId, accountId,
Amount(amount), currency, reference, Amount(amount), currency, reference,
mapToDate(bookingDate), mapToDate(valueDate), mapToDate(bookingDate), mapToDate(valueDate),
@ -375,7 +426,8 @@ open class SqliteBankingRepository(
mapToAmount(openingBalance), mapToAmount(closingBalance), mapToAmount(openingBalance), mapToAmount(closingBalance),
userSetDisplayName, category, notes, userSetDisplayName, userSetReference,
category, notes,
statementNumber?.toInt(), sheetNumber?.toInt(), statementNumber?.toInt(), sheetNumber?.toInt(),

View File

@ -6,8 +6,8 @@ import net.codinux.banking.client.model.Amount
class AccountTransactionEntity( class AccountTransactionEntity(
val id: Long, val id: Long,
val userId: Long, val bankId: Long,
val bankAccountId: Long, val accountId: Long,
amount: Amount, amount: Amount,
currency: String, currency: String,
@ -25,7 +25,8 @@ class AccountTransactionEntity(
openingBalance: Amount? = null, openingBalance: Amount? = null,
closingBalance: Amount? = null, closingBalance: Amount? = null,
userSetDisplayName: String? = null, userSetReference: String? = null,
userSetOtherPartyName: String? = null,
category: String? = null, category: String? = null,
notes: String? = null, notes: String? = null,
@ -80,10 +81,10 @@ class AccountTransactionEntity(
isReversal, isReversal,
userSetDisplayName, category, notes userSetReference, userSetOtherPartyName, category, notes
) { ) {
constructor(id: Long, userId: Long, bankAccountId: Long, transaction: AccountTransaction) : this( constructor(id: Long, bankId: Long, accountId: Long, transaction: AccountTransaction) : this(
id, userId, bankAccountId, id, bankId, accountId,
transaction.amount, transaction.currency, transaction.reference, transaction.amount, transaction.currency, transaction.reference,
transaction.bookingDate, transaction.valueDate, transaction.bookingDate, transaction.valueDate,
@ -93,7 +94,8 @@ class AccountTransactionEntity(
transaction.openingBalance, transaction.closingBalance, transaction.openingBalance, transaction.closingBalance,
transaction.userSetDisplayName, transaction.category, transaction.notes, transaction.userSetReference, transaction.userSetOtherPartyName,
transaction.category, transaction.notes,
transaction.statementNumber, transaction.sheetNumber, transaction.statementNumber, transaction.sheetNumber,
@ -116,7 +118,7 @@ class AccountTransactionEntity(
override val identifier: String by lazy { 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.Instant
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.* import net.codinux.banking.client.model.*
import net.codinux.banking.client.model.securitiesaccount.Holding import kotlin.jvm.JvmName
class BankAccountEntity( class BankAccountEntity(
val id: Long, val id: Long,
val userId: Long, val bankId: Long,
identifier: String, identifier: String,
subAccountNumber: String? = null, subAccountNumber: String? = null,
@ -28,9 +28,9 @@ class BankAccountEntity(
lastAccountUpdateTime: Instant? = null, lastAccountUpdateTime: Instant? = null,
retrievedTransactionsFrom: LocalDate? = null, retrievedTransactionsFrom: LocalDate? = null,
bookedTransactions: MutableList<AccountTransactionEntity> = mutableListOf(), override val bookedTransactions: MutableList<AccountTransactionEntity> = mutableListOf(),
prebookedTransactions: MutableList<PrebookedAccountTransaction> = mutableListOf(), prebookedTransactions: MutableList<PrebookedAccountTransaction> = mutableListOf(),
override var holdings: List<HoldingEntity> = emptyList(), override val holdings: MutableList<HoldingEntity> = mutableListOf(),
userSetDisplayName: String? = null, userSetDisplayName: String? = null,
displayIndex: Int = 0, displayIndex: Int = 0,
@ -48,14 +48,14 @@ class BankAccountEntity(
serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom, serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom,
bookedTransactions as MutableList<AccountTransaction>, prebookedTransactions, bookedTransactions, prebookedTransactions,
holdings, holdings,
userSetDisplayName, displayIndex, userSetDisplayName, displayIndex,
hideAccount, includeInAutomaticAccountsUpdate hideAccount, includeInAutomaticAccountsUpdate
) { ) {
constructor(id: Long, userId: Long, account: BankAccount, transactions: List<AccountTransactionEntity> = emptyList(), holdings: List<HoldingEntity> = emptyList()) : this( constructor(id: Long, bankId: Long, account: BankAccount, transactions: List<AccountTransactionEntity> = emptyList(), holdings: List<HoldingEntity> = emptyList()) : this(
id, userId, id, bankId,
account.identifier, account.subAccountNumber, account.iban, account.productName, account.identifier, account.subAccountNumber, account.iban, account.productName,
account.accountHolderName, account.type, account.accountHolderName, account.type,
@ -68,12 +68,16 @@ class BankAccountEntity(
account.serverTransactionsRetentionDays, account.serverTransactionsRetentionDays,
account.lastAccountUpdateTime, account.retrievedTransactionsFrom, account.lastAccountUpdateTime, account.retrievedTransactionsFrom,
transactions.toMutableList(), mutableListOf(), holdings, transactions.toMutableList(), mutableListOf(), holdings.toMutableList(),
account.userSetDisplayName, account.displayIndex, account.userSetDisplayName, account.displayIndex,
account.hideAccount, account.includeInAutomaticAccountsUpdate 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( class HoldingEntity(
val id: Long, val id: Long,
val userId: Long, val bankId: Long,
val bankAccountId: Long, val accountId: Long,
name: String, name: String,
@ -30,8 +30,8 @@ class HoldingEntity(
buyingDate: LocalDate? = null buyingDate: LocalDate? = null
) : Holding(name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate) { ) : Holding(name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate) {
constructor(id: Long, userId: Long, bankAccountId: Long, holding: Holding) : this( constructor(id: Long, bankId: Long, accountId: Long, holding: Holding) : this(
id, userId, bankAccountId, id, bankId, accountId,
holding.name, holding.isin, holding.wkn, holding.name, holding.isin, holding.wkn,

View File

@ -4,17 +4,19 @@ import net.codinux.banking.client.model.tan.*
class TanMediumEntity( class TanMediumEntity(
val id: Long, val id: Long,
val userId: Long, val bankId: Long,
type: TanMediumType, type: TanMediumType,
mediumName: String?, mediumName: String?,
status: TanMediumStatus, status: TanMediumStatus,
tanGenerator: TanGeneratorTanMedium? = null, tanGenerator: TanGeneratorTanMedium? = null,
mobilePhone: MobilePhoneTanMedium? = null mobilePhone: MobilePhoneTanMedium? = null,
) : TanMedium(type, mediumName, status, tanGenerator, mobilePhone) {
constructor(id: Long, userId: Long, medium: TanMedium) userSetDisplayName: String? = null
: this(id, userId, medium.type, medium.mediumName, medium.status, medium.tanGenerator, medium.mobilePhone) ) : 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( class TanMethodEntity(
val id: Long, val id: Long,
val userId: Long, val bankId: Long,
displayName: String, displayName: String,
type: TanMethodType, type: TanMethodType,
identifier: String, identifier: String,
maxTanInputLength: Int? = null, maxTanInputLength: Int? = null,
allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric,
) : TanMethod(displayName, type, identifier, maxTanInputLength, allowedTanFormat) {
constructor(id: Long, userId: Long, tanMethod: TanMethod) userSetDisplayName: String? = null,
: this(id, userId, tanMethod.displayName, tanMethod.type, tanMethod.identifier, tanMethod.maxTanInputLength, tanMethod.allowedTanFormat) ) : 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 @Composable
fun BottomBar(showMenuDrawer: Boolean = true) { fun BottomBar(showMenuDrawer: Boolean = true) {
val users by uiState.users.collectAsState() val banks by uiState.banks.collectAsState()
val transactionsFilter by uiState.transactionsFilter.collectAsState() val transactionsFilter by uiState.transactionsFilter.collectAsState()
@ -65,7 +65,7 @@ fun BottomBar(showMenuDrawer: Boolean = true) {
} else if (selectedAccount.bankAccount != null) { } else if (selectedAccount.bankAccount != null) {
selectedAccount.bankAccount.displayName selectedAccount.bankAccount.displayName
} else { } else {
selectedAccount.user.displayName selectedAccount.bank.displayName
} }
Text(title, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis) 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) { if (showSearchbar == false) {
Row(Modifier.fillMaxHeight().widthIn(IconWidth, IconWidth), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxHeight().widthIn(IconWidth, IconWidth), verticalAlignment = Alignment.CenterVertically) {
IconButton({ showSearchbar = true }, Modifier.width(IconWidth)) { 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.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -55,7 +54,7 @@ private val VerticalSpacing = 8.dp
fun SideMenuContent() { fun SideMenuContent() {
val drawerState = uiState.drawerState.collectAsState().value val drawerState = uiState.drawerState.collectAsState().value
val accounts = uiState.users.collectAsState().value val accounts = uiState.banks.collectAsState().value
val coroutineScope = rememberCoroutineScope() 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.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp 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.client.model.BankViewInfo
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.BankInfo import net.codinux.banking.ui.model.BankInfo
@ -21,8 +21,8 @@ private val bankIconService = DI.bankIconService
private val DefaultIconModifier: Modifier = Modifier.size(16.dp) private val DefaultIconModifier: Modifier = Modifier.size(16.dp)
@Composable @Composable
fun BankIcon(user: User?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null, fallbackIconTintColor: Color? = null) { fun BankIcon(bank: BankAccess?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null, fallbackIconTintColor: Color? = null) {
val iconUrl by remember(user?.bic) { mutableStateOf(user?.let { bankIconService.findIconForBank(it) }) } val iconUrl by remember(bank?.bic) { mutableStateOf(bank?.let { bankIconService.findIconForBank(it) }) }
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon, fallbackIconTintColor = fallbackIconTintColor) BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon, fallbackIconTintColor = fallbackIconTintColor)
} }
@ -37,8 +37,8 @@ fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifi
} }
@Composable @Composable
fun BankIcon(user: BankViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) { fun BankIcon(bank: BankViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) {
val iconUrl = user?.let { bankIconService.findIconForBank(it.bankName, null, it.bankingGroup) } val iconUrl = bank?.let { bankIconService.findIconForBank(it.bankName, null, it.bankingGroup) }
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon) 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.codinux.banking.dataaccess.entities.BankAccountEntity 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 import net.codinux.banking.ui.config.DI
private val uiState = DI.uiState private val uiState = DI.uiState
@ -27,9 +27,9 @@ fun BanksList(
textColor: Color = Color.White, textColor: Color = Color.White,
itemModifier: Modifier = Modifier.height(48.dp).widthIn(min = 300.dp), itemModifier: Modifier = Modifier.height(48.dp).widthIn(min = 300.dp),
itemHorizontalPadding: Dp = 8.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) { Column(modifier) {
@ -37,16 +37,16 @@ fun BanksList(
accountSelected?.invoke(null, null) accountSelected?.invoke(null, null)
} }
users.value.sortedBy { it.displayIndex }.forEach { user -> banks.value.sortedBy { it.displayIndex }.forEach { bank ->
Spacer(Modifier.fillMaxWidth().height(12.dp)) Spacer(Modifier.fillMaxWidth().height(12.dp))
NavigationMenuItem(itemModifier, user.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, user, fallbackIcon = defaultBankIcon) { NavigationMenuItem(itemModifier, bank.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, bank, fallbackIcon = defaultBankIcon) {
accountSelected?.invoke(user, null) 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) { 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.codinux.banking.dataaccess.entities.BankAccountEntity 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.Colors
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
@ -37,7 +38,7 @@ fun NavigationMenuItem(
iconSize: Dp = 24.dp, iconSize: Dp = 24.dp,
iconTextSpacing: Dp = 24.dp, iconTextSpacing: Dp = 24.dp,
horizontalPadding: Dp = 8.dp, horizontalPadding: Dp = 8.dp,
user: UserEntity? = null, bank: BankAccessEntity? = null,
bankAccount: BankAccountEntity? = null, bankAccount: BankAccountEntity? = null,
fallbackIcon: ImageVector? = null, fallbackIcon: ImageVector? = null,
icon: (@Composable () -> Unit)? = null, icon: (@Composable () -> Unit)? = null,
@ -49,12 +50,26 @@ fun NavigationMenuItem(
val showColoredAmounts by DI.uiSettings.showColoredAmounts.collectAsState() 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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier.let {
.clickable { onClick?.invoke() } if (isUnsupportedAccountType == false) {
it.clickable {
onClick?.invoke()
}
} else {
it
}
}
.let { .let {
if (user != null && filterService.isSelected(user, transactionsFilter) if (bank != null && filterService.isSelected(bank, transactionsFilter)
|| bankAccount != null && filterService.isSelected(bankAccount, transactionsFilter)) { || bankAccount != null && filterService.isSelected(bankAccount, transactionsFilter)) {
it.background(Colors.AccentAsSelectionBackground, shape = RoundedCornerShape(4.dp)) it.background(Colors.AccentAsSelectionBackground, shape = RoundedCornerShape(4.dp))
} else { } else {
@ -68,17 +83,17 @@ fun NavigationMenuItem(
icon() icon()
} }
} else { } 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 null
} else if (bankAccount != null) { } else if (bankAccount != null) {
bankAccount.balance bankAccount.balance
} else if (user != null) { } else if (bank != null) {
calculator.calculateBalanceOfUser(user) calculator.calculateBalanceOfBankAccess(bank)
} else { } else {
null null
} }

View File

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

View File

@ -23,7 +23,7 @@ fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) {
val transactionsGrouping by uiSettings.transactionsGrouping.collectAsState() val transactionsGrouping by uiSettings.transactionsGrouping.collectAsState()
val zebraStripes by uiSettings.zebraStripes.collectAsState() val showTransactionsInAlternatingColors by uiSettings.showTransactionsInAlternatingColors.collectAsState()
val showBankIcons by uiSettings.showBankIcons.collectAsState() val showBankIcons by uiSettings.showBankIcons.collectAsState()
@ -33,7 +33,7 @@ fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) {
Column(modifier) { Column(modifier) {
BooleanOption("Kontostand anzeigen", showBalance, textColor = textColor) { uiSettings.showBalance.value = it } 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 } 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 androidx.compose.ui.unit.sp
import net.codinux.banking.client.model.Amount import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.securitiesaccount.Holding 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.Colors
import net.codinux.banking.ui.config.DI 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.forms.RoundedCornersCard
import net.codinux.banking.ui.model.AccountTransactionViewModel import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.TransactionsGrouping import net.codinux.banking.ui.model.TransactionsGrouping
@ -31,7 +32,7 @@ fun GroupedTransactionsListItems(
modifier: Modifier, modifier: Modifier,
transactionsToDisplay: List<AccountTransactionViewModel>, transactionsToDisplay: List<AccountTransactionViewModel>,
holdingsToDisplay: List<Holding>, holdingsToDisplay: List<Holding>,
usersById: Map<Long, UserEntity>, banksById: Map<Long, BankAccessEntity>,
transactionsGrouping: TransactionsGrouping transactionsGrouping: TransactionsGrouping
) { ) {
val groupingService = remember { TransactionsGroupingService() } val groupingService = remember { TransactionsGroupingService() }
@ -49,6 +50,7 @@ fun GroupedTransactionsListItems(
Column(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 16.dp)) { Column(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 16.dp)) {
Text( Text(
text = "Depotwerte", text = "Depotwerte",
color = Style.ListItemHeaderTextColor,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(bottom = 2.dp), modifier = Modifier.padding(bottom = 2.dp),
@ -77,6 +79,7 @@ fun GroupedTransactionsListItems(
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
Text( Text(
text = DI.formatUtil.formatGroupingDate(groupingDate, transactionsGrouping), text = DI.formatUtil.formatGroupingDate(groupingDate, transactionsGrouping),
color = Style.ListItemHeaderTextColor,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 8.dp, bottom = 2.dp), 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 Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed
monthTransactions.forEachIndexed { index, transaction -> monthTransactions.forEachIndexed { index, transaction ->
key(transaction.id) { 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.Amount import net.codinux.banking.client.model.Amount
@ -32,11 +31,11 @@ fun HoldingListItem(holding: Holding, isOddItem: Boolean = false, isNotLastItem:
// TODO: also regard showBalance? // TODO: also regard showBalance?
val showColoredAmounts by uiSettings.showColoredAmounts.collectAsState() 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 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 val currency = holding.currency ?: fallbackCurrency
@ -49,6 +48,7 @@ fun HoldingListItem(holding: Holding, isOddItem: Boolean = false, isNotLastItem:
Text( Text(
holding.name, holding.name,
color = Style.ListItemHeaderTextColor,
fontWeight = Style.ListItemHeaderWeight, fontWeight = Style.ListItemHeaderWeight,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,

View File

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

View File

@ -25,9 +25,9 @@ private val formatUtil = DI.formatUtil
@Composable @Composable
fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean = true) { fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean = true) {
val users by uiState.users.collectAsState() val banks by uiState.banks.collectAsState()
val usersById by remember(users) { val banksById by remember(banks) {
derivedStateOf { users.associateBy { it.id } } derivedStateOf { banks.associateBy { it.id } }
} }
val transactionsFilter by uiState.transactionsFilter.collectAsState() val transactionsFilter by uiState.transactionsFilter.collectAsState()
@ -59,13 +59,13 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
if (showBalance) { 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)) Text(formatUtil.formatAmount(balance, "EUR"), color = formatUtil.getColorForAmount(balance, showColoredAmounts))
} }
} }
if (transactionsGrouping != TransactionsGrouping.None) { if (transactionsGrouping != TransactionsGrouping.None) {
GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, holdingsToDisplay, usersById, transactionsGrouping) GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, holdingsToDisplay, banksById, transactionsGrouping)
} else { } else {
LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) { LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) {
itemsIndexed(holdingsToDisplay) { index, holding -> itemsIndexed(holdingsToDisplay) { index, holding ->
@ -76,7 +76,7 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
itemsIndexed(transactionsToDisplay) { index, transaction -> itemsIndexed(transactionsToDisplay) { index, transaction ->
key(transaction.id) { 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 Zinc200 = Color(228, 228, 231)
val Zinc500 = Color(0xFF71717a)
val Zinc700 = Color(63, 63, 70) val Zinc700 = Color(63, 63, 70)

View File

@ -36,7 +36,7 @@ object DI {
var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList()) 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)) fun setRepository(sqlDriver: SqlDriver) = setRepository(SqliteBankingRepository(sqlDriver))

View File

@ -13,6 +13,9 @@ object Style {
val HeaderFontWeight: FontWeight = FontWeight.Bold 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 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)) { 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) 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()) {
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
Row { 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( Text(
"TAN benötigt ${Internationalization.getTextForActionRequiringTan(challenge.forAction)}", "TAN benötigt ${Internationalization.getTextForActionRequiringTan(challenge.forAction)}",

View File

@ -38,11 +38,11 @@ fun TransferMoneyDialog(
data: ShowTransferMoneyDialogData, data: ShowTransferMoneyDialogData,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
val users = uiState.users.value val banks = uiState.banks.value
val accountsToUser = users.sortedBy { it.displayIndex } val accountsToBank = banks.sortedBy { it.displayIndex }
.flatMap { user -> user.accounts.sortedBy { it.displayIndex }.map { it to user } }.toMap() .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 } .filter { it.supportsMoneyTransfer }
if (accountsSupportingTransferringMoney.isEmpty()) { if (accountsSupportingTransferringMoney.isEmpty()) {
@ -98,7 +98,7 @@ fun TransferMoneyDialog(
transferMoneyJob = coroutineScope.launch(Dispatchers.IOorDefault) { transferMoneyJob = coroutineScope.launch(Dispatchers.IOorDefault) {
val successful = bankingService.transferMoney( val successful = bankingService.transferMoney(
accountsToUser[senderAccount]!!, senderAccount, accountsToBank[senderAccount]!!, senderAccount,
recipientName, recipientAccountIdentifier, recipientName, recipientAccountIdentifier,
Amount(amount), // TODO: verify entered amount is valid Amount(amount), // TODO: verify entered amount is valid
"EUR", // TODO: add input field for currency "EUR", // TODO: add input field for currency
@ -132,13 +132,13 @@ fun TransferMoneyDialog(
Select( Select(
"Konto", "Konto",
accountsSupportingTransferringMoney, senderAccount, { senderAccount = it }, accountsSupportingTransferringMoney, senderAccount, { senderAccount = it },
{ account -> "${accountsToUser[account]?.displayName} ${account.displayName}" }, { account -> "${accountsToBank[account]?.displayName} ${account.displayName}" },
leadingIcon = { BankIcon(accountsToUser[senderAccount]) } leadingIcon = { BankIcon(accountsToBank[senderAccount]) }
) { account -> ) { account ->
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { 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( data class AccountTransactionViewModel(
val id: Long, val id: Long,
val userId: Long, val bankId: Long,
val bankAccountId: Long, val accountId: Long,
val amount: Amount, val amount: Amount,
val currency: String, val currency: String,
@ -17,11 +17,11 @@ data class AccountTransactionViewModel(
val otherPartyName: String? = null, val otherPartyName: String? = null,
val postingText: String? = null, val postingText: String? = null,
val userSetDisplayName: String? = null, val userSetReference: String? = null,
val category: 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) constructor(id: Long, bankId: Long, accountId: Long, transaction: AccountTransaction)
: this(id, userId, bankAccountId, transaction.amount, transaction.currency, transaction.reference, transaction.valueDate, transaction.otherPartyName, transaction.postingText) : 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 androidx.compose.runtime.*
import net.codinux.banking.dataaccess.entities.BankAccountEntity import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserEntity import net.codinux.banking.dataaccess.entities.BankAccessEntity
class AccountTransactionsFilter { class AccountTransactionsFilter {
@ -19,7 +19,7 @@ class AccountTransactionsFilter {
val selectedAccount: BankAccountFilter? val selectedAccount: BankAccountFilter?
get() = selectedAccounts.value.firstOrNull() get() = selectedAccounts.value.firstOrNull()
fun selectedAccountChanged(user: UserEntity?, bankAccount: BankAccountEntity?) { fun selectedAccountChanged(user: BankAccessEntity?, bankAccount: BankAccountEntity?) {
selectedAccounts.value = if (user == null) { selectedAccounts.value = if (user == null) {
emptyList() emptyList()
} else { } else {

View File

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

View File

@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
class BankInfo( class BankInfo(
val name: String, val name: String,
val bankCode: String, val domesticBankCode: String,
val bic: String = "", val bic: String = "",
val postalCode: String, val postalCode: String,
val city: String, val city: String,
@ -23,5 +23,5 @@ class BankInfo(
get() = pinTanVersion == "FinTS V3.0" 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 package net.codinux.banking.ui.model.events
import net.codinux.banking.client.model.BankAccount 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.client.model.securitiesaccount.Holding
import net.codinux.banking.ui.model.AccountTransactionViewModel import net.codinux.banking.ui.model.AccountTransactionViewModel
data class AccountTransactionsRetrievedEvent( data class AccountTransactionsRetrievedEvent(
val user: User, val bank: BankAccess,
val account: BankAccount, val account: BankAccount,
val newTransactions: List<AccountTransactionViewModel>, val newTransactions: List<AccountTransactionViewModel>,
val updatedHoldings: List<Holding> = emptyList() 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.BankAccountEntity
import net.codinux.banking.dataaccess.entities.HoldingEntity 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.AccountTransactionViewModel
import net.codinux.banking.ui.model.AccountTransactionsFilter import net.codinux.banking.ui.model.AccountTransactionsFilter
import net.codinux.banking.ui.model.BankAccountFilter import net.codinux.banking.ui.model.BankAccountFilter
@ -38,9 +38,9 @@ class AccountTransactionsFilterService {
private fun matchesFilter(transaction: AccountTransactionViewModel, accountsFilter: List<BankAccountFilter>): Boolean = private fun matchesFilter(transaction: AccountTransactionViewModel, accountsFilter: List<BankAccountFilter>): Boolean =
accountsFilter.any { (user, bankAccount) -> accountsFilter.any { (user, bankAccount) ->
if (bankAccount != null) { if (bankAccount != null) {
transaction.bankAccountId == bankAccount.id transaction.accountId == bankAccount.id
} else { } else {
transaction.userId == user.id transaction.bankId == user.id
} }
} }
@ -71,9 +71,9 @@ class AccountTransactionsFilterService {
private fun matchesFilter(holding: HoldingEntity, filter: List<BankAccountFilter>): Boolean = private fun matchesFilter(holding: HoldingEntity, filter: List<BankAccountFilter>): Boolean =
filter.any { (user, bankAccount) -> filter.any { (user, bankAccount) ->
if (bankAccount != null) { if (bankAccount != null) {
holding.bankAccountId == bankAccount.id holding.accountId == bankAccount.id
} else { } else {
holding.userId == user.id holding.bankId == user.id
} }
} }
@ -83,14 +83,14 @@ class AccountTransactionsFilterService {
|| holding.wkn?.contains(searchTerm, true) == true || holding.wkn?.contains(searchTerm, true) == true
fun isSelected(user: UserEntity, transactionsFilter: AccountTransactionsFilter): Boolean { fun isSelected(user: BankAccessEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
if (transactionsFilter.showAllAccounts) { if (transactionsFilter.showAllAccounts) {
return false return false
} }
val filter = transactionsFilter.selectedAccount 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 { fun isSelected(bankAccount: BankAccountEntity, transactionsFilter: AccountTransactionsFilter): Boolean {

View File

@ -36,7 +36,7 @@ class BankFinder {
return getBankList(maxItems) return getBankList(maxItems)
} }
return getBankList().asSequence().filter { it.bankCode.startsWith(query) } return getBankList().asSequence().filter { it.domesticBankCode.startsWith(query) }
.max(maxItems) .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 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 return if (result.size > 1) { // non unique result, but should actually never happen for BICs
null null

View File

@ -1,11 +1,11 @@
package net.codinux.banking.ui.service package net.codinux.banking.ui.service
import net.codinux.banking.client.model.BankingGroup 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 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) { fun findIconForBank(bankName: String, bic: String? = null, bankingGroup: BankingGroup? = null): String? = when (bankingGroup) {
BankingGroup.Sparkasse -> "https://sparkasse.de/favicon-32x32.png" BankingGroup.Sparkasse -> "https://sparkasse.de/favicon-32x32.png"

View File

@ -1,7 +1,9 @@
package net.codinux.banking.ui.service package net.codinux.banking.ui.service
import androidx.lifecycle.viewModelScope
import bankmeister.composeapp.generated.resources.Res import bankmeister.composeapp.generated.resources.Res
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import net.codinux.banking.client.SimpleBankingClientCallback import net.codinux.banking.client.SimpleBankingClientCallback
import net.codinux.banking.client.fints4k.FinTs4kBankingClient 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.error.*
import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent
import net.codinux.banking.ui.model.events.TransferredMoneyEvent import net.codinux.banking.ui.model.events.TransferredMoneyEvent
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.banking.ui.state.UiState
import net.codinux.csv.reader.CsvReader import net.codinux.csv.reader.CsvReader
import net.codinux.log.logger import net.codinux.log.logger
@ -30,6 +34,7 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi
@OptIn(ExperimentalResourceApi::class) @OptIn(ExperimentalResourceApi::class)
class BankingService( class BankingService(
private val uiState: UiState, private val uiState: UiState,
private val uiSettings: UiSettings,
private val bankingRepository: BankingRepository, private val bankingRepository: BankingRepository,
private val bankFinder: BankFinder private val bankFinder: BankFinder
) { ) {
@ -45,21 +50,40 @@ class BankingService(
suspend fun init() { suspend fun init() {
try { 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.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) { } 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 getAllAccountTransactions() = bankingRepository.getAllAccountTransactions()
fun getAllTransactionsOfUser(user: UserEntity) = bankingRepository.getAllTransactionsOfUser(user) fun getAllTransactionsForBank(bank: BankAccessEntity) = bankingRepository.getAllTransactionsForBank(bank)
fun getAllAccountTransactionsAsViewModel() = bankingRepository.getAllAccountTransactionsAsViewModel() fun getAllAccountTransactionsAsViewModel() = bankingRepository.getAllAccountTransactionsAsViewModel()
@ -72,7 +96,7 @@ class BankingService(
suspend fun addAccount(bank: BankInfo, loginName: String, password: String, retrieveAllTransactions: Boolean = false): Boolean { suspend fun addAccount(bank: BankInfo, loginName: String, password: String, retrieveAllTransactions: Boolean = false): Boolean {
try { try {
val retrieveTransactions = if (retrieveAllTransactions) RetrieveTransactions.All else RetrieveTransactions.OfLast90Days 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) { if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulGetAccountDataResponse(response.data!!) handleSuccessfulGetAccountDataResponse(response.data!!)
@ -101,21 +125,21 @@ class BankingService(
private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) { private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) {
try { try {
val newUser = response.user val newBank = response.bank
newUser.displayIndex = uiState.users.value.size 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() val banks = uiState.banks.value.toMutableList()
users.add(newUserEntity) banks.add(newBankEntity)
uiState.users.value = users uiState.banks.value = banks
updateTransactionsInUi(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities }) updateTransactionsInUi(newBankEntity.accounts.flatMap { it.bookedTransactions })
updateHoldingsInUi(newUserEntity.accounts.flatMap { it.holdings }, emptyList()) updateHoldingsInUi(newBankEntity.accounts.flatMap { it.holdings }, emptyList())
} catch (e: Throwable) { } 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() { suspend fun updateAccountTransactions() {
val selectedAccount = uiState.transactionsFilter.value.selectedAccount val selectedAccount = uiState.transactionsFilter.value.selectedAccount
if (selectedAccount != null) { if (selectedAccount != null) {
updateAccountTransactions(selectedAccount.user, selectedAccount.bankAccount) updateAccountTransactions(selectedAccount.bank, selectedAccount.bankAccount)
} else { } else {
uiState.users.value.forEach { user -> uiState.banks.value.forEach { bank ->
updateAccountTransactions(user) updateAccountTransactions(bank)
} }
} }
} }
private suspend fun updateAccountTransactions(user: UserEntity, bankAccount: BankAccountEntity? = null) { private suspend fun updateAccountTransactions(bank: BankAccessEntity, bankAccount: BankAccountEntity? = null) {
withContext(Dispatchers.IOorDefault) { withContext(Dispatchers.IOorDefault) {
try { 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) { if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulUpdateAccountTransactionsResponse(user, response.data!!) handleSuccessfulUpdateAccountTransactionsResponse(bank, response.data!!)
} else { } else {
handleUnsuccessfulBankingClientResponse(BankingClientAction.UpdateAccountTransactions, response) handleUnsuccessfulBankingClientResponse(BankingClientAction.UpdateAccountTransactions, response)
} }
} catch (e: Throwable) { } 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 { try {
// TODO: when user gets updated by BankingClient, also update user in database // TODO: when bank gets updated by BankingClient, also update bank in database
// val newUser = response.user // val newUser = response.bank
// val newUserEntity = bankingRepository.persistUser(newUser) // val newUserEntity = bankingRepository.persistUser(newUser)
// //
// log.info { "Saved user account $newUserEntity with ${newUserEntity.accounts.flatMap { it.bookedTransactionsEntities }.size} transactions" } // 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 -> 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 // 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) val newTransactions = modelService.findNewTransactions(response.bookedTransactions, existingAccountTransactions)
@ -181,18 +205,16 @@ class BankingService(
bankingRepository.updateHoldings(updateHoldings(updatedExistingHoldings, updatedRetrievedHoldings)) bankingRepository.updateHoldings(updateHoldings(updatedExistingHoldings, updatedRetrievedHoldings))
bankingRepository.deleteHoldings(deletedHoldings) bankingRepository.deleteHoldings(deletedHoldings)
account.holdings = account.holdings.toMutableList().apply { account.holdings.removeAll(deletedHoldings)
addAll(persistedNewHoldings) account.addHoldings(persistedNewHoldings)
removeAll(deletedHoldings)
}
updateHoldingsInUi(persistedNewHoldings, deletedHoldings) updateHoldingsInUi(persistedNewHoldings, deletedHoldings)
val transactionsViewModel = updateTransactionsInUi(newTransactionsEntities) val transactionsViewModel = updateTransactionsInUi(newTransactionsEntities)
uiState.dispatchNewTransactionsRetrieved(AccountTransactionsRetrievedEvent(user, account, transactionsViewModel, response.holdings)) uiState.dispatchNewTransactionsRetrieved(AccountTransactionsRetrievedEvent(bank, account, transactionsViewModel, response.holdings))
} }
} catch (e: Throwable) { } 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, recipientName: String, recipientAccountIdentifier: String, amount: Amount, currency: String,
paymentReference: String? = null, instantTransfer: Boolean = false, recipientBankIdentifier: String? = null): Boolean { paymentReference: String? = null, instantTransfer: Boolean = false, recipientBankIdentifier: String? = null): Boolean {
val response = client.transferMoneyAsync(TransferMoneyRequestForUser( 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 BankAccountIdentifier(account.identifier, account.subAccountNumber, account.iban), // TODO: use BankingClient's one
recipientName, recipientAccountIdentifier, recipientBankIdentifier, recipientName, recipientAccountIdentifier, recipientBankIdentifier,
amount, "EUR", amount, "EUR",
@ -251,7 +273,7 @@ class BankingService(
} else if (response.type == ResponseType.Success) { } else if (response.type == ResponseType.Success) {
uiState.dispatchTransferredMoney(TransferredMoneyEvent(recipientName, amount, currency)) uiState.dispatchTransferredMoney(TransferredMoneyEvent(recipientName, amount, currency))
updateAccountTransactions(user, account) updateAccountTransactions(bank, account)
} }
return response.type == ResponseType.Success 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> { private suspend fun readTransactionsFromCsv(): List<AccountTransaction> {
val csv = Res.readBytes("files/transactions.csv").decodeToString() val csv = Res.readBytes("files/transactions.csv").decodeToString()
val csvReader = CsvReader(hasHeaderRow = true, reuseRowInstance = true, skipEmptyRows = true).read(csv) val csvReader = CsvReader(hasHeaderRow = true, reuseRowInstance = true, skipEmptyRows = true).read(csv)

View File

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

View File

@ -10,7 +10,7 @@ class UiSettings : ViewModel() {
val transactionsGrouping = MutableStateFlow(TransactionsGrouping.Month) val transactionsGrouping = MutableStateFlow(TransactionsGrouping.Month)
val zebraStripes = MutableStateFlow(true) val showTransactionsInAlternatingColors = MutableStateFlow(true)
val showBankIcons = 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.EnterTanResult
import net.codinux.banking.client.model.tan.TanChallenge import net.codinux.banking.client.model.tan.TanChallenge
import net.codinux.banking.dataaccess.entities.HoldingEntity 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.*
import net.codinux.banking.ui.model.error.ApplicationError import net.codinux.banking.ui.model.error.ApplicationError
import net.codinux.banking.ui.model.error.BankingClientError import net.codinux.banking.ui.model.error.BankingClientError
import net.codinux.banking.ui.model.error.ErroneousAction import net.codinux.banking.ui.model.error.ErroneousAction
import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent import net.codinux.banking.ui.model.events.AccountTransactionsRetrievedEvent
import net.codinux.banking.ui.model.events.TransferredMoneyEvent import net.codinux.banking.ui.model.events.TransferredMoneyEvent
import net.codinux.banking.ui.model.settings.AppSettings
class UiState : ViewModel() { 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()) val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())

View File

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

View File

@ -1,24 +1,25 @@
import kotlin.Boolean; import kotlin.Boolean;
CREATE TABLE IF NOT EXISTS User ( CREATE TABLE IF NOT EXISTS BankAccess (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
bankCode TEXT NOT NULL, domesticBankCode TEXT NOT NULL,
loginName TEXT NOT NULL, loginName TEXT NOT NULL,
password TEXT, password TEXT,
bankName TEXT NOT NULL, bankName TEXT NOT NULL,
bic TEXT NOT NULL, bic TEXT,
customerName TEXT NOT NULL, customerName TEXT NOT NULL,
userId TEXT, userId TEXT,
selectedTanMethodId TEXT, selectedTanMethodIdentifier TEXT,
selectedTanMediumName TEXT, selectedTanMediumIdentifier TEXT,
bankingGroup TEXT, bankingGroup TEXT,
serverAddress TEXT, serverAddress TEXT,
countryCode TEXT NOT NULL,
clientData TEXT, clientData TEXT,
@ -31,20 +32,21 @@ CREATE TABLE IF NOT EXISTS User (
); );
insertUser: insertBank:
INSERT INTO User( INSERT INTO BankAccess(
bankCode, loginName, password, domesticBankCode, loginName, password,
bankName, bic, bankName, bic,
customerName, userId, customerName, userId,
selectedTanMethodId, selectedTanMethodIdentifier,
selectedTanMediumName, selectedTanMediumIdentifier,
bankingGroup, bankingGroup,
serverAddress, serverAddress,
countryCode,
clientData, clientData,
@ -61,7 +63,7 @@ VALUES(
?, ?,
?, ?, ?, ?, ?,
?, ?,
@ -72,16 +74,16 @@ VALUES(
); );
selectAllUsers: getAllBanks:
SELECT User.* SELECT BankAccess.*
FROM User; FROM BankAccess;
CREATE TABLE IF NOT EXISTS BankAccount ( CREATE TABLE IF NOT EXISTS BankAccount (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL, bankId INTEGER NOT NULL,
identifier TEXT NOT NULL, identifier TEXT NOT NULL,
subAccountNumber TEXT, subAccountNumber TEXT,
@ -109,10 +111,13 @@ CREATE TABLE IF NOT EXISTS BankAccount (
includeInAutomaticAccountsUpdate INTEGER AS Boolean NOT NULL includeInAutomaticAccountsUpdate INTEGER AS Boolean NOT NULL
); );
CREATE INDEX idx_BankAccount_bankId
ON BankAccount (bankId);
insertBankAccount: insertBankAccount:
INSERT INTO BankAccount( INSERT INTO BankAccount(
userId, bankId,
identifier, accountHolderName, type, identifier, accountHolderName, type,
iban, subAccountNumber, productName, currency, accountLimit, iban, subAccountNumber, productName, currency, accountLimit,
@ -145,7 +150,7 @@ VALUES(
); );
selectAllBankAccounts: getAllBankAccounts:
SELECT BankAccount.* SELECT BankAccount.*
FROM BankAccount; FROM BankAccount;
@ -154,25 +159,32 @@ FROM BankAccount;
CREATE TABLE IF NOT EXISTS TanMethod ( CREATE TABLE IF NOT EXISTS TanMethod (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL, bankId INTEGER NOT NULL,
displayName TEXT NOT NULL, displayName TEXT NOT NULL,
type TEXT NOT NULL, type TEXT NOT NULL,
identifier TEXT NOT NULL, identifier TEXT NOT NULL,
maxTanInputLength INTEGER , maxTanInputLength INTEGER ,
allowedTanFormat TEXT NOT NULL allowedTanFormat TEXT NOT NULL,
userSetDisplayName TEXT
); );
CREATE INDEX idx_TanMethod_bankId
ON TanMethod (bankId);
insertTanMethod: insertTanMethod:
INSERT INTO TanMethod( INSERT INTO TanMethod(
userId, bankId,
displayName, displayName,
type, type,
identifier, identifier,
maxTanInputLength, maxTanInputLength,
allowedTanFormat allowedTanFormat,
userSetDisplayName
) )
VALUES ( VALUES (
?, ?,
@ -181,11 +193,13 @@ VALUES (
?, ?,
?, ?,
?, ?,
?,
? ?
); );
selectAllTanMethods: getAllTanMethods:
SELECT TanMethod.* SELECT TanMethod.*
FROM TanMethod; FROM TanMethod;
@ -194,7 +208,7 @@ FROM TanMethod;
CREATE TABLE IF NOT EXISTS TanMedium ( CREATE TABLE IF NOT EXISTS TanMedium (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL, bankId INTEGER NOT NULL,
type TEXT NOT NULL, type TEXT NOT NULL,
mediumName TEXT, mediumName TEXT,
@ -209,13 +223,18 @@ CREATE TABLE IF NOT EXISTS TanMedium (
cardSequenceNumber TEXT, cardSequenceNumber TEXT,
cardType INTEGER, cardType INTEGER,
validFrom TEXT, validFrom TEXT,
validTo TEXT validTo TEXT,
userSetDisplayName TEXT
); );
CREATE INDEX idx_TanMedium_bankId
ON TanMedium (bankId);
insertTanMedium: insertTanMedium:
INSERT INTO TanMedium( INSERT INTO TanMedium(
userId, bankId,
type, type,
mediumName, mediumName,
@ -228,7 +247,9 @@ INSERT INTO TanMedium(
cardSequenceNumber, cardSequenceNumber,
cardType, cardType,
validFrom, validFrom,
validTo validTo,
userSetDisplayName
) )
VALUES ( VALUES (
?, ?,
@ -244,11 +265,13 @@ VALUES (
?, ?,
?, ?,
?, ?,
?,
? ?
); );
selectAllTanMedia: getAllTanMedia:
SELECT TanMedium.* SELECT TanMedium.*
FROM 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 @Composable
fun EnterTanDialogPreview_EnterTan() { fun EnterTanDialogPreview_EnterTan() {
val tanMethods = listOf(TanMethod("Zeig mich an", TanMethodType.AppTan, "902")) val tanMethods = listOf(TanMethod("Zeig mich an", TanMethodType.AppTan, "902"))
val user = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank) val bank = 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 tanChallenge = TanChallenge(TanChallengeType.EnterTan, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { } EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
} }
@ -27,9 +27,9 @@ fun EnterTanDialogPreview_TanImage() {
val tanMethod = TanMethod("photoTAN-Verfahren", TanMethodType.photoTan, "902", 6, AllowedTanFormat.Numeric) val tanMethod = TanMethod("photoTAN-Verfahren", TanMethodType.photoTan, "902", 6, AllowedTanFormat.Numeric)
val tanImage = TanImage("image/png", tanImageBytes) 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) { }) { } EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
} }
@ -50,10 +50,10 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
TanMedium(TanMediumType.TanGenerator, "SparkassenCard (Debitkarte)", TanMediumStatus.Used, TanGeneratorTanMedium("5432109876")) 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 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) { }) { } EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
} }

View File

@ -17,8 +17,8 @@ class SqliteBankingRepositoryTest {
} }
private val underTest = object : SqliteBankingRepository(sqlDriver) { private val underTest = object : SqliteBankingRepository(sqlDriver) {
override public suspend fun persistTransaction(userId: Long, bankAccountId: Long, transaction: AccountTransaction): AccountTransactionEntity = override public suspend fun persistTransaction(bankId: Long, accountId: Long, transaction: AccountTransaction): AccountTransactionEntity =
super.persistTransaction(userId, bankAccountId, transaction) super.persistTransaction(bankId, accountId, transaction)
} }
@ -27,35 +27,35 @@ class SqliteBankingRepositoryTest {
val bankAccounts = listOf( 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) 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 wrongCredentialsEntered = true
displayIndex = 99 displayIndex = 99
} }
val persisted = underTest.persistUser(user) val persisted = underTest.persistBank(bank)
assertNotNull(persisted.id) assertNotNull(persisted.id)
assertEquals(user.bankCode, persisted.bankCode) assertEquals(bank.domesticBankCode, persisted.domesticBankCode)
assertEquals(user.loginName, persisted.loginName) assertEquals(bank.loginName, persisted.loginName)
assertEquals(user.password, persisted.password) assertEquals(bank.password, persisted.password)
assertEquals(user.bankName, persisted.bankName) assertEquals(bank.bankName, persisted.bankName)
assertEquals(user.bic, persisted.bic) assertEquals(bank.bic, persisted.bic)
assertEquals(user.customerName, persisted.customerName) assertEquals(bank.customerName, persisted.customerName)
assertEquals(user.userId, persisted.userId) assertEquals(bank.userId, persisted.userId)
assertEquals(user.bankingGroup, persisted.bankingGroup) assertEquals(bank.bankingGroup, persisted.bankingGroup)
assertEquals(user.wrongCredentialsEntered, persisted.wrongCredentialsEntered) assertEquals(bank.wrongCredentialsEntered, persisted.wrongCredentialsEntered)
assertEquals(user.displayIndex, persisted.displayIndex) assertEquals(bank.displayIndex, persisted.displayIndex)
assertEquals(1, persisted.accounts.size) assertEquals(1, persisted.accounts.size)
val persistedBankAccount = persisted.accounts.first() val persistedBankAccount = persisted.accounts.first()
assertNotNull(persistedBankAccount.id) assertNotNull(persistedBankAccount.id)
assertEquals(persisted.id, persistedBankAccount.userId) assertEquals(persisted.id, persistedBankAccount.bankId)
assertEquals(bankAccounts.first().identifier, persistedBankAccount.identifier) assertEquals(bankAccounts.first().identifier, persistedBankAccount.identifier)
assertEquals(bankAccounts.first().accountHolderName, persistedBankAccount.accountHolderName) assertEquals(bankAccounts.first().accountHolderName, persistedBankAccount.accountHolderName)

View File

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