Updated to new BankingClient model

This commit is contained in:
dankito 2024-09-09 00:25:18 +02:00
parent cd8e0a53ad
commit 234084e627
30 changed files with 346 additions and 313 deletions

View File

@ -1,17 +1,17 @@
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.UserAccount import net.codinux.banking.client.model.User
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.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel import net.codinux.banking.ui.model.AccountTransactionViewModel
interface BankingRepository { interface BankingRepository {
fun getAllUserAccounts(): List<UserAccountEntity> fun getAllUsers(): List<UserEntity>
suspend fun persistUserAccount(userAccount: UserAccount): UserAccountEntity suspend fun persistUser(user: User): UserEntity
suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity> suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity>
@ -20,7 +20,7 @@ interface BankingRepository {
fun getAllAccountTransactions(): List<AccountTransactionEntity> fun getAllAccountTransactions(): List<AccountTransactionEntity>
fun getAllTransactionsOfUserAccount(userAccount: UserAccountEntity): List<AccountTransactionEntity> fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity>
fun getTransactionById(transactionId: Long): AccountTransactionEntity? fun getTransactionById(transactionId: Long): AccountTransactionEntity?

View File

@ -1,29 +1,29 @@
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.UserAccount import net.codinux.banking.client.model.User
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.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel import net.codinux.banking.ui.model.AccountTransactionViewModel
class InMemoryBankingRepository( class InMemoryBankingRepository(
userAccounts: Collection<UserAccount> = emptyList(), users: Collection<User> = emptyList(),
transactions: Collection<AccountTransaction> = emptyList() transactions: Collection<AccountTransaction> = emptyList()
) : BankingRepository { ) : BankingRepository {
private var nextId = 0L // TODO: make thread-safe private var nextId = 0L // TODO: make thread-safe
private val userAccounts = userAccounts.map { map(it) }.toMutableList() private val users = users.map { map(it) }.toMutableList()
private val transactions = transactions.map { map(it) }.toMutableList() private val transactions = transactions.map { map(it) }.toMutableList()
override fun getAllUserAccounts(): List<UserAccountEntity> = userAccounts.toList() override fun getAllUsers(): List<UserEntity> = users.toList()
override suspend fun persistUserAccount(userAccount: UserAccount): UserAccountEntity { override suspend fun persistUser(user: User): UserEntity {
val entity = map(userAccount) // TODO: may fix someday and add also BankAccounts and their id val entity = map(user) // TODO: may fix someday and add also BankAccounts and their id
this.userAccounts.add(entity) this.users.add(entity)
return entity return entity
} }
@ -37,29 +37,31 @@ class InMemoryBankingRepository(
override fun getAllAccountTransactions(): List<AccountTransactionEntity> = transactions.toList() override fun getAllAccountTransactions(): List<AccountTransactionEntity> = transactions.toList()
override fun getAllTransactionsOfUserAccount(userAccount: UserAccountEntity): List<AccountTransactionEntity> = override fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity> =
getAllAccountTransactions().filter { it.userAccountId == userAccount.id } getAllAccountTransactions().filter { it.userId == user.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: UserAccount) = UserAccountEntity( private fun map(account: User) = UserEntity(
nextId++, nextId++,
account.bankCode, account.loginName, account.password, account.bankName, account.bic, account.customerName, account.userId, account.bankCode, account.loginName, account.password, account.bankName, account.bic, account.customerName, account.userId,
// TODO: may fix someday and also add BankAccounts // TODO: may fix someday and also add BankAccounts
emptyList(), account.selectedTanMethodId, emptyList(), account.selectedTanMediumName, emptyList(), emptyList(), account.selectedTanMethodIdentifier, emptyList(), account.selectedTanMediumIdentifier, emptyList(),
account.bankingGroup, account.iconUrl, account.wrongCredentialsEntered, account.userSetDisplayName, account.displayIndex account.bankingGroup, account.finTsServerAddress,
account.userSetDisplayName, account.displayIndex,
account.iconUrl, account.wrongCredentialsEntered,
) )
// TODO: someday may fix and get userAccountId and bankAccountId // TODO: someday may fix and get userAccountId and bankAccountId
private fun map(transaction: AccountTransaction, userAccountId: Long = nextId++, bankAccountId: Long = nextId++) = AccountTransactionEntity( private fun map(transaction: AccountTransaction, userId: Long = nextId++, bankAccountId: Long = nextId++) = AccountTransactionEntity(
nextId++, nextId++,
userAccountId, bankAccountId, userId, bankAccountId,
transaction.amount, transaction.currency, transaction.reference, transaction.amount, transaction.currency, transaction.reference,
transaction.bookingDate, transaction.valueDate, transaction.bookingDate, transaction.valueDate,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId,
transaction.bookingText transaction.postingText
) )
} }

View File

@ -6,7 +6,7 @@ import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.* import net.codinux.banking.client.model.*
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.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.log.logger import net.codinux.log.logger
import kotlin.enums.EnumEntries import kotlin.enums.EnumEntries
@ -25,42 +25,46 @@ open class SqliteBankingRepository(
private val log by logger() private val log by logger()
override fun getAllUserAccounts(): List<UserAccountEntity> {
val bankAccounts = getAllBankAccounts().groupBy { it.userAccountId }
return userAccountQueries.selectAllUserAccounts { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodId, selectedTanMediumName, bankingGroup, iconUrl, wrongCredentialsEntered, userSetDisplayName, displayIndex -> override fun getAllUsers(): List<UserEntity> {
UserAccountEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, bankAccounts[id] ?: emptyList(), selectedTanMethodId, emptyList(), selectedTanMediumName, emptyList(), val bankAccounts = getAllBankAccounts().groupBy { it.userId }
bankingGroup?.let { BankingGroup.valueOf(it) }, iconUrl, wrongCredentialsEntered, userSetDisplayName, displayIndex.toInt())
return userAccountQueries.selectAllUsers { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodId, selectedTanMediumName, bankingGroup, iconUrl, wrongCredentialsEntered, userSetDisplayName, displayIndex ->
UserEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, bankAccounts[id] ?: emptyList(), selectedTanMethodId, emptyList(), selectedTanMediumName, emptyList(),
bankingGroup?.let { BankingGroup.valueOf(it) }, "", userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered)
}.executeAsList() }.executeAsList()
} }
override suspend fun persistUserAccount(userAccount: UserAccount): UserAccountEntity { override suspend fun persistUser(user: User): UserEntity {
return userAccountQueries.transactionWithResult { return userAccountQueries.transactionWithResult {
userAccountQueries.insertUserAccount(userAccount.bankCode, userAccount.loginName, userAccount.password, userAccount.bankName, userAccount.bic, userAccountQueries.insertUser(user.bankCode, user.loginName, user.password, user.bankName, user.bic,
userAccount.customerName, userAccount.userId, userAccount.selectedTanMethodId, userAccount.selectedTanMediumName, user.customerName, user.userId, user.selectedTanMethodIdentifier, user.selectedTanMediumIdentifier,
userAccount.bankingGroup?.name, userAccount.iconUrl, userAccount.wrongCredentialsEntered, userAccount.userSetDisplayName, userAccount.displayIndex.toLong() user.bankingGroup?.name, user.iconUrl, user.wrongCredentialsEntered, user.userSetDisplayName, user.displayIndex.toLong()
) )
val userAccountId = getLastInsertedId() // getLastInsertedId() / last_insert_rowid() has to be called in a transaction with the insert operation, otherwise it will not work val userId = getLastInsertedId() // getLastInsertedId() / last_insert_rowid() has to be called in a transaction with the insert operation, otherwise it will not work
val bankAccounts = persistBankAccounts(userAccountId, userAccount.accounts) val bankAccounts = persistBankAccounts(userId, user.accounts)
UserAccountEntity(userAccountId, userAccount, bankAccounts) UserEntity(userId, user, bankAccounts)
} }
} }
fun getAllBankAccounts(): List<BankAccountEntity> = userAccountQueries.selectAllBankAccounts { id, userAccountId, identifier, accountHolderName, type, iban, subAccountNumber, productName, currency, accountLimit, balance, isAccountTypeSupportedByApplication, features, countDaysForWhichTransactionsAreKept, lastTransactionsRetrievalTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate -> fun getAllBankAccounts(): List<BankAccountEntity> = userAccountQueries.selectAllBankAccounts { id, userId, identifier, accountHolderName, type, iban, subAccountNumber, productName, currency, accountLimit, balance, isAccountTypeSupportedByApplication, features, serverTransactionsRetentionDays, lastTransactionsRetrievalTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ->
BankAccountEntity( BankAccountEntity(
id, userAccountId, id, userId,
identifier, accountHolderName, BankAccountType.valueOf(type),
iban, subAccountNumber, productName, currency, accountLimit, identifier, subAccountNumber, iban, productName,
accountHolderName, BankAccountType.valueOf(type),
currency, accountLimit,
mapToAmount(balance),
isAccountTypeSupportedByApplication, mapEnumSet(features, BankAccountFeatures.entries), isAccountTypeSupportedByApplication, mapEnumSet(features, BankAccountFeatures.entries),
mapToInt(countDaysForWhichTransactionsAreKept), mapToAmount(balance),
mapToInt(serverTransactionsRetentionDays),
mapToInstant(lastTransactionsRetrievalTime), mapToDate(retrievedTransactionsFrom), mapToInstant(lastTransactionsRetrievalTime), mapToDate(retrievedTransactionsFrom),
mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(),
@ -70,22 +74,22 @@ open class SqliteBankingRepository(
) )
}.executeAsList() }.executeAsList()
private suspend fun persistBankAccounts(userAccountId: Long, bankAccounts: Collection<BankAccount>): List<BankAccountEntity> = private suspend fun persistBankAccounts(userId: Long, bankAccounts: Collection<BankAccount>): List<BankAccountEntity> =
bankAccounts.map { persistBankAccount(userAccountId, it) } bankAccounts.map { persistBankAccount(userId, 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(userAccountId: Long, account: BankAccount): BankAccountEntity { private suspend fun persistBankAccount(userId: Long, account: BankAccount): BankAccountEntity {
userAccountQueries.insertBankAccount( userAccountQueries.insertBankAccount(
userAccountId, userId,
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,
mapAmount(account.balance), mapAmount(account.balance),
account.isAccountTypeSupportedByApplication, mapEnumCollectionToString(account.features), account.isAccountTypeSupportedByApplication, mapEnumCollectionToString(account.features),
mapInt(account.countDaysForWhichTransactionsAreKept), mapInt(account.serverTransactionsRetentionDays),
mapInstant(account.lastTransactionsRetrievalTime), mapDate(account.retrievedTransactionsFrom), mapInstant(account.lastTransactionsRetrievalTime), mapDate(account.retrievedTransactionsFrom),
account.userSetDisplayName, mapInt(account.displayIndex), account.userSetDisplayName, mapInt(account.displayIndex),
@ -95,25 +99,24 @@ open class SqliteBankingRepository(
val accountId = getLastInsertedId() val accountId = getLastInsertedId()
val accountTransactionEntities = account.bookedTransactions.map { transaction -> val accountTransactionEntities = account.bookedTransactions.map { transaction ->
persistTransaction(userAccountId, accountId, transaction) persistTransaction(userId, accountId, transaction)
} }
return BankAccountEntity(accountId, userAccountId, account, accountTransactionEntities) return BankAccountEntity(accountId, userId, account, accountTransactionEntities)
} }
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> = override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
// TODO: find a better way to express the reference value to display, sepaReference or (unparsed)reference accountTransactionQueries.selectAllTransactionsAsViewModel { id, userId, bankAccountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, category ->
accountTransactionQueries.selectAllTransactionsAsViewModel { id, userAccountId, bankAccountId, amount, currency, unparsedReference, valueDate, otherPartyName, bookingText, sepaReference, userSetDisplayName, category -> AccountTransactionViewModel(id, userId, bankAccountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, category)
AccountTransactionViewModel(id, userAccountId, bankAccountId, mapToAmount(amount), currency, sepaReference ?: unparsedReference, mapToDate(valueDate), otherPartyName, bookingText, userSetDisplayName, category)
}.executeAsList() }.executeAsList()
override fun getAllAccountTransactions(): List<AccountTransactionEntity> { override fun getAllAccountTransactions(): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactions(::mapTransaction).executeAsList() return accountTransactionQueries.selectAllTransactions(::mapTransaction).executeAsList()
} }
override fun getAllTransactionsOfUserAccount(userAccount: UserAccountEntity): List<AccountTransactionEntity> { override fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactionsOfUserAccount(userAccount.id, ::mapTransaction).executeAsList() return accountTransactionQueries.selectAllTransactionsOfUser(user.id, ::mapTransaction).executeAsList()
} }
override fun getTransactionById(transactionId: Long): AccountTransactionEntity? = override fun getTransactionById(transactionId: Long): AccountTransactionEntity? =
@ -123,7 +126,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.userAccountId, bankAccount.id, transaction) persistTransaction(bankAccount.userId, bankAccount.id, transaction)
} }
} }
} }
@ -131,36 +134,34 @@ 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(userAccountId: Long, bankAccountId: Long, transaction: AccountTransaction): AccountTransactionEntity { protected open suspend fun persistTransaction(userId: Long, bankAccountId: Long, transaction: AccountTransaction): AccountTransactionEntity {
accountTransactionQueries.insertTransaction( accountTransactionQueries.insertTransaction(
userAccountId, bankAccountId, userId, bankAccountId,
mapAmount(transaction.amount), transaction.currency, transaction.unparsedReference, mapAmount(transaction.amount), transaction.currency, transaction.reference,
mapDate(transaction.bookingDate), mapDate(transaction.valueDate), mapDate(transaction.bookingDate), mapDate(transaction.valueDate),
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
transaction.bookingText,
transaction.userSetDisplayName, null, transaction.notes, // TODO: add category to AccountTransaction transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId,
transaction.postingText,
transaction.information, transaction.userSetDisplayName, transaction.category, transaction.notes,
transaction.statementNumber?.toLong(), transaction.sequenceNumber?.toLong(),
transaction.statementNumber?.toLong(), transaction.sheetNumber?.toLong(),
mapAmount(transaction.openingBalance), mapAmount(transaction.closingBalance), mapAmount(transaction.openingBalance), mapAmount(transaction.closingBalance),
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.endToEndReference, transaction.customerReference, transaction.mandateReference,
transaction.creditorIdentifier, transaction.originatorsIdentificationCode, transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount, transaction.compensationAmount, transaction.originalAmount,
transaction.sepaReference,
transaction.deviantOriginator, transaction.deviantRecipient, transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement, transaction.referenceWithNoSpecialType,
transaction.currencyType, transaction.bookingKey, transaction.journalNumber, transaction.textKeyAddition,
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution,
transaction.supplementaryDetails,
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber transaction.orderReferenceNumber, transaction.referenceNumber
) )
return AccountTransactionEntity(getLastInsertedId(), userAccountId, bankAccountId, transaction) return AccountTransactionEntity(getLastInsertedId(), userId, bankAccountId, transaction)
} }
@ -169,45 +170,58 @@ open class SqliteBankingRepository(
private fun mapTransaction( private fun mapTransaction(
id: Long, userAccountId: Long, bankAccountId: Long, id: Long, userId: Long, bankAccountId: Long,
amount: String, currency: String, reference: String, bookingDate: String, valueDate: String,
otherPartyName: String?, otherPartyBankCode: String?, otherPartyAccountId: String?, bookingText: String?, amount: String, currency: String, reference: String?,
bookingDate: String, valueDate: String,
otherPartyName: String?, otherPartyBankId: String?, otherPartyAccountId: String?,
postingText: String?,
userSetDisplayName: String?, category: String?, notes: String?, userSetDisplayName: String?, category: String?, notes: String?,
information: String?, statementNumber: Long?, sequenceNumber: Long?,
statementNumber: Long?, sheetNumber: Long?,
openingBalance: String?, closingBalance: String?, openingBalance: String?, closingBalance: String?,
endToEndReference: String?, customerReference: String?, mandateReference: String?, creditorIdentifier: String?, originatorsIdentificationCode: String?, endToEndReference: String?, customerReference: String?, mandateReference: String?, creditorIdentifier: String?, originatorsIdentificationCode: String?,
compensationAmount: String?, originalAmount: String?, sepaReference: String?, deviantOriginator: String?, deviantRecipient: String?, referenceWithNoSpecialType: String?,
primaNotaNumber: String?, textKeySupplement: String?, currencyType: String?, bookingKey: String?, referenceForTheAccountOwner: String?, referenceOfTheAccountServicingInstitution: String?, compensationAmount: String?, originalAmount: String?, deviantOriginator: String?, deviantRecipient: String?, referenceWithNoSpecialType: String?,
supplementaryDetails: String?, transactionReferenceNumber: String?, relatedReferenceNumber: String?): AccountTransactionEntity =
AccountTransactionEntity( journalNumber: String?, textKeyAddition: String?,
referenceForTheAccountOwner: String?, referenceOfTheAccountServicingInstitution: String?, supplementaryDetails: String?,
transactionReferenceNumber: String?, relatedReferenceNumber: String?
): AccountTransactionEntity = AccountTransactionEntity(
id, id,
userAccountId, bankAccountId, userId, bankAccountId,
Amount(amount), currency, reference, Amount(amount), currency, reference,
mapToDate(bookingDate), mapToDate(valueDate), mapToDate(bookingDate), mapToDate(valueDate),
otherPartyName, otherPartyBankCode, otherPartyAccountId, otherPartyName, otherPartyBankId, otherPartyAccountId,
bookingText, postingText,
userSetDisplayName, category, notes,
information,
statementNumber?.toInt(), sequenceNumber?.toInt(),
mapToAmount(openingBalance), mapToAmount(closingBalance), mapToAmount(openingBalance), mapToAmount(closingBalance),
endToEndReference, customerReference, mandateReference, userSetDisplayName, category, notes,
creditorIdentifier, originatorsIdentificationCode,
compensationAmount, originalAmount, statementNumber?.toInt(), sheetNumber?.toInt(),
sepaReference,
deviantOriginator, deviantRecipient,
referenceWithNoSpecialType, primaNotaNumber,
textKeySupplement,
currencyType, bookingKey,
referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution, referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution,
supplementaryDetails, supplementaryDetails,
endToEndReference, mandateReference,
creditorIdentifier, originatorsIdentificationCode,
compensationAmount, originalAmount,
deviantOriginator, deviantRecipient,
referenceWithNoSpecialType,
journalNumber, textKeyAddition,
transactionReferenceNumber, relatedReferenceNumber transactionReferenceNumber, relatedReferenceNumber
// TODO: add isReversal
) )

View File

@ -6,108 +6,117 @@ import net.codinux.banking.client.model.Amount
class AccountTransactionEntity( class AccountTransactionEntity(
val id: Long, val id: Long,
val userAccountId: Long, val userId: Long,
val bankAccountId: Long, val bankAccountId: Long,
amount: Amount, amount: Amount,
currency: String, currency: String,
reference: String, reference: String?,
bookingDate: LocalDate, bookingDate: LocalDate,
valueDate: LocalDate, valueDate: LocalDate,
otherPartyName: String? = null, otherPartyName: String? = null,
otherPartyBankCode: String? = null, otherPartyBankId: String? = null,
otherPartyAccountId: String? = null, otherPartyAccountId: String? = null,
bookingText: String? = null, postingText: String? = null,
userSetDisplayName: String? = null,
category: String? = null,
notes: String? = null,
information: String? = null,
statementNumber: Int? = null,
sequenceNumber: Int? = null,
openingBalance: Amount? = null, openingBalance: Amount? = null,
closingBalance: Amount? = null, closingBalance: Amount? = null,
endToEndReference: String? = null, userSetDisplayName: String? = null,
category: String? = null,
notes: String? = null,
statementNumber: Int? = null,
sheetNumber: Int? = null,
customerReference: String? = null, customerReference: String? = null,
bankReference: String? = null,
furtherInformation: String? = null,
endToEndReference: String? = null,
mandateReference: String? = null, mandateReference: String? = null,
creditorIdentifier: String? = null, creditorIdentifier: String? = null,
originatorsIdentificationCode: String? = null, originatorsIdentificationCode: String? = null,
compensationAmount: String? = null, compensationAmount: String? = null,
originalAmount: String? = null, originalAmount: String? = null,
sepaReference: String? = null,
deviantOriginator: String? = null, deviantOriginator: String? = null,
deviantRecipient: String? = null, deviantRecipient: String? = null,
referenceWithNoSpecialType: String? = null, referenceWithNoSpecialType: String? = null,
primaNotaNumber: String? = null,
textKeySupplement: String? = null,
currencyType: String? = null, journalNumber: String? = null,
bookingKey: String? = null, textKeyAddition: String? = null,
referenceForTheAccountOwner: String? = null,
referenceOfTheAccountServicingInstitution: String? = null,
supplementaryDetails: String? = null,
transactionReferenceNumber: String? = null, orderReferenceNumber: String? = null,
relatedReferenceNumber: String? = null referenceNumber: String? = null,
isReversal: Boolean = false,
) : AccountTransaction( ) : AccountTransaction(
amount, currency, reference, amount, currency, reference,
bookingDate, valueDate, bookingDate, valueDate,
otherPartyName, otherPartyBankCode, otherPartyAccountId, otherPartyName, otherPartyBankId, otherPartyAccountId,
bookingText, postingText,
information,
statementNumber, sequenceNumber,
openingBalance, closingBalance, openingBalance, closingBalance,
endToEndReference, customerReference, mandateReference, statementNumber, sheetNumber,
customerReference, bankReference, furtherInformation,
endToEndReference, mandateReference,
creditorIdentifier, originatorsIdentificationCode, creditorIdentifier, originatorsIdentificationCode,
compensationAmount, originalAmount, compensationAmount, originalAmount,
sepaReference,
deviantOriginator, deviantRecipient, deviantOriginator, deviantRecipient,
referenceWithNoSpecialType, primaNotaNumber, referenceWithNoSpecialType,
textKeySupplement,
currencyType, bookingKey, journalNumber, textKeyAddition,
referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution,
supplementaryDetails,
transactionReferenceNumber, relatedReferenceNumber, orderReferenceNumber, referenceNumber,
isReversal,
userSetDisplayName, category, notes userSetDisplayName, category, notes
) { ) {
constructor(id: Long, userAccountId: Long, bankAccountId: Long, transaction: AccountTransaction) : this( constructor(id: Long, userId: Long, bankAccountId: Long, transaction: AccountTransaction) : this(
id, userAccountId, bankAccountId, id, userId, bankAccountId,
transaction.amount, transaction.currency, transaction.unparsedReference, transaction.bookingDate, transaction.valueDate, transaction.amount, transaction.currency, transaction.reference,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText, transaction.bookingDate, transaction.valueDate,
transaction.userSetDisplayName, transaction.category, transaction.notes, transaction.information, transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId,
transaction.postingText,
transaction.statementNumber, transaction.sequenceNumber,
transaction.openingBalance, transaction.closingBalance, transaction.openingBalance, transaction.closingBalance,
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.userSetDisplayName, transaction.category, transaction.notes,
transaction.statementNumber, transaction.sheetNumber,
transaction.customerReference, transaction.bankReference,
transaction.furtherInformation,
transaction.endToEndReference, transaction.mandateReference,
transaction.creditorIdentifier, transaction.originatorsIdentificationCode, transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount, transaction.compensationAmount, transaction.originalAmount,
transaction.sepaReference,
transaction.deviantOriginator, transaction.deviantRecipient, transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement, transaction.referenceWithNoSpecialType,
transaction.currencyType, transaction.bookingKey, transaction.journalNumber, transaction.textKeyAddition,
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution,
transaction.supplementaryDetails,
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber transaction.orderReferenceNumber, transaction.referenceNumber,
transaction.isReversal,
) )
override val identifier: String by lazy { override val identifier: String by lazy {
"$userAccountId ${super.identifier}" "$userId ${super.identifier}"
} }
} }

View File

@ -6,28 +6,29 @@ import net.codinux.banking.client.model.*
class BankAccountEntity( class BankAccountEntity(
val id: Long, val id: Long,
val userAccountId: Long, val userId: Long,
identifier: String, identifier: String,
subAccountNumber: String? = null,
iban: String? = null,
productName: String? = null,
accountHolderName: String, accountHolderName: String,
type: BankAccountType = BankAccountType.Other, type: BankAccountType = BankAccountType.Other,
iban: String? = null,
subAccountNumber: String? = null,
productName: String? = null,
currency: String = "EUR", currency: String = "EUR",
accountLimit: String? = null, accountLimit: String? = null,
balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
isAccountTypeSupportedByApplication: Boolean = true, isAccountTypeSupportedByApplication: Boolean = true,
features: Set<BankAccountFeatures> = emptySet(), features: Set<BankAccountFeatures> = emptySet(),
countDaysForWhichTransactionsAreKept: Int? = null, balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
serverTransactionsRetentionDays: Int? = null,
lastTransactionsRetrievalTime: Instant? = null, lastTransactionsRetrievalTime: Instant? = null,
retrievedTransactionsFrom: LocalDate? = null, retrievedTransactionsFrom: LocalDate? = null,
bookedTransactions: MutableList<AccountTransactionEntity> = mutableListOf(), bookedTransactions: MutableList<AccountTransactionEntity> = mutableListOf(),
unbookedTransactions: MutableList<UnbookedAccountTransaction> = mutableListOf(), prebookedTransactions: MutableList<PrebookedAccountTransaction> = mutableListOf(),
userSetDisplayName: String? = null, userSetDisplayName: String? = null,
displayIndex: Int = 0, displayIndex: Int = 0,
@ -35,21 +36,39 @@ class BankAccountEntity(
hideAccount: Boolean = false, hideAccount: Boolean = false,
includeInAutomaticAccountsUpdate: Boolean = true includeInAutomaticAccountsUpdate: Boolean = true
) : BankAccount( ) : BankAccount(
identifier, accountHolderName, type, iban, subAccountNumber, productName, currency, accountLimit, identifier, subAccountNumber, iban, productName,
balance, isAccountTypeSupportedByApplication, features,
lastTransactionsRetrievalTime, retrievedTransactionsFrom, countDaysForWhichTransactionsAreKept, accountHolderName, type, currency, accountLimit,
bookedTransactions as MutableList<AccountTransaction>, unbookedTransactions,
userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate isAccountTypeSupportedByApplication, features,
balance,
serverTransactionsRetentionDays, lastTransactionsRetrievalTime, retrievedTransactionsFrom,
bookedTransactions as MutableList<AccountTransaction>, prebookedTransactions,
userSetDisplayName, displayIndex,
hideAccount, includeInAutomaticAccountsUpdate
) { ) {
constructor(id: Long, userAccountId: Long, account: BankAccount, transactions: List<AccountTransactionEntity> = emptyList()) : this( constructor(id: Long, userId: Long, account: BankAccount, transactions: List<AccountTransactionEntity> = emptyList()) : this(
id, userAccountId, id, userId,
account.identifier, account.accountHolderName, account.type, account.iban, account.subAccountNumber, account.identifier, account.subAccountNumber, account.iban, account.productName,
account.productName, account.currency, account.accountLimit,
account.balance, account.accountHolderName, account.type,
account.currency, account.accountLimit,
account.isAccountTypeSupportedByApplication, account.features, account.isAccountTypeSupportedByApplication, account.features,
account.countDaysForWhichTransactionsAreKept, account.lastTransactionsRetrievalTime, account.retrievedTransactionsFrom,
account.balance,
account.serverTransactionsRetentionDays,
account.lastTransactionsRetrievalTime, account.retrievedTransactionsFrom,
transactions.toMutableList(), mutableListOf(), transactions.toMutableList(), mutableListOf(),
account.userSetDisplayName, account.displayIndex, account.hideAccount, account.includeInAutomaticAccountsUpdate
account.userSetDisplayName, account.displayIndex,
account.hideAccount, account.includeInAutomaticAccountsUpdate
) )
val bookedTransactionsEntities: MutableList<AccountTransactionEntity> = bookedTransactions val bookedTransactionsEntities: MutableList<AccountTransactionEntity> = bookedTransactions

View File

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

View File

@ -1,7 +1,6 @@
package net.codinux.banking.ui.appskeleton package net.codinux.banking.ui.appskeleton
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -18,13 +17,9 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.*
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
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 androidx.compose.ui.unit.toSize
import bankmeister.composeapp.generated.resources.Res import bankmeister.composeapp.generated.resources.Res
import bankmeister.composeapp.generated.resources.filter_alt import bankmeister.composeapp.generated.resources.filter_alt
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -39,7 +34,7 @@ private val IconWidth = 48.dp
@Composable @Composable
fun BottomBar(showMenuDrawer: Boolean = true) { fun BottomBar(showMenuDrawer: Boolean = true) {
val userAccounts by uiState.userAccounts.collectAsState() val users by uiState.users.collectAsState()
val transactionsFilter by uiState.transactionsFilter.collectAsState() val transactionsFilter by uiState.transactionsFilter.collectAsState()
@ -75,7 +70,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.userAccount.displayName selectedAccount.user.displayName
} }
Text(title, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis) Text(title, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis)
@ -132,7 +127,7 @@ fun BottomBar(showMenuDrawer: Boolean = true) {
} }
if (userAccounts.isNotEmpty()) { if (users.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

@ -54,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.userAccounts.collectAsState().value val accounts = uiState.users.collectAsState().value
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()

View File

@ -9,8 +9,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.UserAccount import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.UserAccountViewInfo 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
import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper
@ -22,8 +22,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(userAccount: UserAccount?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, iconResource: DrawableResource? = null) { fun BankIcon(user: User?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, iconResource: DrawableResource? = null) {
val iconUrl by remember(userAccount?.bic) { mutableStateOf(userAccount?.let { bankIconService.findIconForBank(it) }) } val iconUrl by remember(user?.bic) { mutableStateOf(user?.let { bankIconService.findIconForBank(it) }) }
BankIcon(iconUrl, modifier, iconModifier, iconResource = iconResource) BankIcon(iconUrl, modifier, iconModifier, iconResource = iconResource)
} }
@ -38,8 +38,8 @@ fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifi
} }
@Composable @Composable
fun BankIcon(userAccount: UserAccountViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, iconResource: DrawableResource? = null) { fun BankIcon(user: BankViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, iconResource: DrawableResource? = null) {
val iconUrl = userAccount?.let { bankIconService.findIconForBank(it.bankName, it.bankingGroup) } val iconUrl = user?.let { bankIconService.findIconForBank(it.bankName, it.bankingGroup) }
BankIcon(iconUrl, modifier, iconModifier, iconResource = iconResource) BankIcon(iconUrl, modifier, iconModifier, iconResource = iconResource)
} }

View File

@ -10,7 +10,7 @@ import androidx.compose.ui.unit.dp
import bankmeister.composeapp.generated.resources.Res import bankmeister.composeapp.generated.resources.Res
import bankmeister.composeapp.generated.resources.account import bankmeister.composeapp.generated.resources.account
import net.codinux.banking.dataaccess.entities.BankAccountEntity import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
private val uiState = DI.uiState private val uiState = DI.uiState
@ -24,9 +24,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: ((UserAccountEntity?, BankAccountEntity?) -> Unit)? = null accountSelected: ((UserEntity?, BankAccountEntity?) -> Unit)? = null
) { ) {
val userAccounts = uiState.userAccounts.collectAsState() val users = uiState.users.collectAsState()
Column(modifier) { Column(modifier) {
@ -34,16 +34,16 @@ fun BanksList(
accountSelected?.invoke(null, null) accountSelected?.invoke(null, null)
} }
userAccounts.value.sortedBy { it.displayIndex }.forEach { userAccount -> users.value.sortedBy { it.displayIndex }.forEach { user ->
Spacer(Modifier.fillMaxWidth().height(12.dp)) Spacer(Modifier.fillMaxWidth().height(12.dp))
NavigationMenuItem(itemModifier, userAccount.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, userAccount, iconResource = Res.drawable.account) { NavigationMenuItem(itemModifier, user.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, user, iconResource = Res.drawable.account) {
accountSelected?.invoke(userAccount, null) accountSelected?.invoke(user, null)
} }
userAccount.accounts.sortedBy { it.displayIndex }.forEach { account -> user.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(userAccount, account) accountSelected?.invoke(user, account)
} }
} }
} }

View File

@ -18,7 +18,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.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
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 org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.DrawableResource
@ -37,7 +37,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,
userAccount: UserAccountEntity? = null, user: UserEntity? = null,
bankAccount: BankAccountEntity? = null, bankAccount: BankAccountEntity? = null,
iconResource: DrawableResource? = null, iconResource: DrawableResource? = null,
icon: (@Composable () -> Unit)? = null, icon: (@Composable () -> Unit)? = null,
@ -54,7 +54,7 @@ fun NavigationMenuItem(
modifier = modifier modifier = modifier
.clickable { onClick?.invoke() } .clickable { onClick?.invoke() }
.let { .let {
if (userAccount != null && filterService.isSelected(userAccount, transactionsFilter) if (user != null && filterService.isSelected(user, 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,7 +68,7 @@ fun NavigationMenuItem(
icon() icon()
} }
} else { } else {
BankIcon(userAccount, Modifier.padding(end = iconTextSpacing), Modifier.size(iconSize), iconResource = iconResource) BankIcon(user, Modifier.padding(end = iconTextSpacing), Modifier.size(iconSize), iconResource = iconResource)
} }
Text(text, color = textColor, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis) Text(text, color = textColor, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
@ -77,8 +77,8 @@ fun NavigationMenuItem(
null null
} else if (bankAccount != null) { } else if (bankAccount != null) {
bankAccount.balance bankAccount.balance
} else if (userAccount != null) { } else if (user != null) {
calculator.calculateBalanceOfUserAccount(userAccount) calculator.calculateBalanceOfUser(user)
} else { } else {
null null
} }

View File

@ -13,7 +13,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp 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.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
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
@ -28,7 +28,7 @@ private val formatUtil = DI.formatUtil
fun GroupedTransactionsListItems( fun GroupedTransactionsListItems(
modifier: Modifier, modifier: Modifier,
transactionsToDisplay: List<AccountTransactionViewModel>, transactionsToDisplay: List<AccountTransactionViewModel>,
userAccountsId: Map<Long, UserAccountEntity>, userAccountsId: Map<Long, UserEntity>,
transactionsGrouping: TransactionsGrouping transactionsGrouping: TransactionsGrouping
) { ) {
val groupingService = remember { TransactionsGroupingService() } val groupingService = remember { TransactionsGroupingService() }
@ -57,7 +57,9 @@ fun GroupedTransactionsListItems(
RoundedCornersCard { RoundedCornersCard {
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 ->
TransactionListItem(userAccountsId[transaction.userAccountId], transaction, index, monthTransactions.size) key(transaction.id) {
TransactionListItem(userAccountsId[transaction.userId], transaction, index, monthTransactions.size)
}
} }
} }
} }

View File

@ -13,7 +13,7 @@ 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.UserAccount import net.codinux.banking.client.model.User
import net.codinux.banking.ui.composables.BankIcon import net.codinux.banking.ui.composables.BankIcon
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
@ -25,7 +25,7 @@ private val uiSettings = DI.uiSettings
private val formatUtil = DI.formatUtil private val formatUtil = DI.formatUtil
@Composable @Composable
fun TransactionListItem(userAccount: UserAccount?, transaction: AccountTransactionViewModel, itemIndex: Int, countItems: Int) { fun TransactionListItem(user: User?, transaction: AccountTransactionViewModel, itemIndex: Int, countItems: Int) {
val zebraStripes by uiSettings.zebraStripes.collectAsState() val zebraStripes by uiSettings.zebraStripes.collectAsState()
val showBankIcons by uiSettings.showBankIcons.collectAsState() val showBankIcons by uiSettings.showBankIcons.collectAsState()
@ -48,9 +48,9 @@ fun TransactionListItem(userAccount: UserAccount?, transaction: AccountTransacti
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.userAccounts.value.firstNotNullOf { it.accounts.firstOrNull { it.id == transaction.bankAccountId } }, DI.uiState.users.value.firstNotNullOf { it.accounts.firstOrNull { it.id == transaction.bankAccountId } },
transaction.otherPartyName, transaction.otherPartyName,
transactionEntity?.otherPartyBankCode, transactionEntity?.otherPartyBankId,
transactionEntity?.otherPartyAccountId, transactionEntity?.otherPartyAccountId,
if (withSameData) transaction.amount else null, if (withSameData) transaction.amount else null,
if (withSameData) transaction.reference else null if (withSameData) transaction.reference else null
@ -76,7 +76,7 @@ fun TransactionListItem(userAccount: UserAccount?, transaction: AccountTransacti
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Row { Row {
if (showBankIcons) { if (showBankIcons) {
BankIcon(userAccount, Modifier.padding(end = 6.dp)) BankIcon(user, Modifier.padding(end = 6.dp))
} }
Text( Text(
@ -90,7 +90,7 @@ fun TransactionListItem(userAccount: UserAccount?, transaction: AccountTransacti
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
Text( Text(
text = transaction.reference, text = transaction.reference ?: "",
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
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 userAccounts by uiState.userAccounts.collectAsState() val users by uiState.users.collectAsState()
val userAccountsId by remember(userAccounts) { val usersById by remember(users) {
derivedStateOf { userAccounts.associateBy { it.id } } derivedStateOf { users.associateBy { it.id } }
} }
val transactionsFilter by uiState.transactionsFilter.collectAsState() val transactionsFilter by uiState.transactionsFilter.collectAsState()
@ -53,17 +53,19 @@ 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, userAccounts, transactionsFilter) val balance = calculator.calculateBalanceOfDisplayedTransactions(transactionsToDisplay, users, 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, userAccountsId, transactionsGrouping) GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, usersById, transactionsGrouping)
} else { } else {
LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) { LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) {
itemsIndexed(transactionsToDisplay) { index, transaction -> itemsIndexed(transactionsToDisplay) { index, transaction ->
TransactionListItem(userAccountsId[transaction.userAccountId], transaction, index, transactionsToDisplay.size) key(transaction.id) {
TransactionListItem(usersById[transaction.userId], transaction, index, transactionsToDisplay.size)
}
} }
} }
} }

View File

@ -39,12 +39,12 @@ fun TransferMoneyDialog(
data: ShowTransferMoneyDialogData, data: ShowTransferMoneyDialogData,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
val userAccounts = uiState.userAccounts.value val userAccounts = uiState.users.value
val accountsToUserAccount = userAccounts.sortedBy { it.displayIndex } val accountsToUserAccount = userAccounts.sortedBy { it.displayIndex }
.flatMap { user -> user.accounts.sortedBy { it.displayIndex }.map { it to user } }.toMap() .flatMap { user -> user.accounts.sortedBy { it.displayIndex }.map { it to user } }.toMap()
val accountsSupportingTransferringMoney = userAccounts.flatMap { it.accounts } val accountsSupportingTransferringMoney = userAccounts.flatMap { it.accounts }
.filter { it.supportsAnyFeature(BankAccountFeatures.TransferMoney, BankAccountFeatures.InstantPayment) } .filter { it.supportsMoneyTransfer }
if (accountsSupportingTransferringMoney.isEmpty()) { if (accountsSupportingTransferringMoney.isEmpty()) {
uiState.applicationErrorOccurred(ErroneousAction.TransferMoney, "Keines Ihrer Konten unterstützt das Überweisen von Geld") uiState.applicationErrorOccurred(ErroneousAction.TransferMoney, "Keines Ihrer Konten unterstützt das Überweisen von Geld")
@ -59,7 +59,7 @@ fun TransferMoneyDialog(
var recipientAccountIdentifier by remember { mutableStateOf(data.recipientAccountIdentifier ?: "") } var recipientAccountIdentifier by remember { mutableStateOf(data.recipientAccountIdentifier ?: "") }
var amount by remember { mutableStateOf(data.amount?.amount ?: "") } var amount by remember { mutableStateOf(data.amount?.amount ?: "") }
var paymentReference by remember { mutableStateOf(data.reference ?: "") } var paymentReference by remember { mutableStateOf(data.reference ?: "") }
val accountSupportsInstantTransfer by remember(senderAccount) { derivedStateOf { senderAccount.supportsAnyFeature(BankAccountFeatures.InstantPayment) } } val accountSupportsInstantTransfer by remember(senderAccount) { derivedStateOf { senderAccount.supportsInstantTransfer } }
var instantTransfer by remember { mutableStateOf(false) } var instantTransfer by remember { mutableStateOf(false) }
val isRequiredDataEntered by remember(recipientName, recipientAccountIdentifier, amount) { val isRequiredDataEntered by remember(recipientName, recipientAccountIdentifier, amount) {

View File

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

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.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
class AccountTransactionsFilter { class AccountTransactionsFilter {
@ -19,11 +19,11 @@ class AccountTransactionsFilter {
val selectedAccount: BankAccountFilter? val selectedAccount: BankAccountFilter?
get() = selectedAccounts.value.firstOrNull() get() = selectedAccounts.value.firstOrNull()
fun selectedAccountChanged(userAccount: UserAccountEntity?, bankAccount: BankAccountEntity?) { fun selectedAccountChanged(user: UserEntity?, bankAccount: BankAccountEntity?) {
selectedAccounts.value = if (userAccount == null) { selectedAccounts.value = if (user == null) {
emptyList() emptyList()
} else { } else {
listOf(BankAccountFilter(userAccount, bankAccount)) listOf(BankAccountFilter(user, bankAccount))
} }
} }

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.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
data class BankAccountFilter( data class BankAccountFilter(
val userAccount: UserAccountEntity, val user: UserEntity,
val bankAccount: BankAccountEntity? = null val bankAccount: BankAccountEntity? = null
) )

View File

@ -1,11 +1,11 @@
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.UserAccount import net.codinux.banking.client.model.User
import net.codinux.banking.ui.model.AccountTransactionViewModel import net.codinux.banking.ui.model.AccountTransactionViewModel
data class AccountTransactionsRetrievedEvent( data class AccountTransactionsRetrievedEvent(
val user: UserAccount, val user: User,
val account: BankAccount, val account: BankAccount,
val newTransactions: List<AccountTransactionViewModel> val newTransactions: List<AccountTransactionViewModel>
) )

View File

@ -1,7 +1,7 @@
package net.codinux.banking.ui.service 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.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
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
@ -30,27 +30,27 @@ class AccountTransactionsFilterService {
} }
private fun matchesFilter(transaction: AccountTransactionViewModel, accountsFilter: List<BankAccountFilter>): Boolean = private fun matchesFilter(transaction: AccountTransactionViewModel, accountsFilter: List<BankAccountFilter>): Boolean =
accountsFilter.any { (userAccount, bankAccount) -> accountsFilter.any { (user, bankAccount) ->
if (bankAccount != null) { if (bankAccount != null) {
transaction.bankAccountId == bankAccount.id transaction.bankAccountId == bankAccount.id
} else { } else {
transaction.userAccountId == userAccount.id transaction.userId == user.id
} }
} }
private fun matchesSearchTerm(transaction: AccountTransactionViewModel, searchTerm: String): Boolean = private fun matchesSearchTerm(transaction: AccountTransactionViewModel, searchTerm: String): Boolean =
transaction.reference.contains(searchTerm, true) transaction.reference?.contains(searchTerm, true) == true
|| (transaction.otherPartyName != null && transaction.otherPartyName.contains(searchTerm, true)) || (transaction.otherPartyName != null && transaction.otherPartyName.contains(searchTerm, true))
fun isSelected(userAccount: UserAccountEntity, transactionsFilter: AccountTransactionsFilter): Boolean { fun isSelected(user: UserEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
if (transactionsFilter.showAllAccounts) { if (transactionsFilter.showAllAccounts) {
return false return false
} }
val filter = transactionsFilter.selectedAccount val filter = transactionsFilter.selectedAccount
return filter?.userAccount == userAccount && filter.bankAccount == null return filter?.user == user && filter.bankAccount == null
} }
fun isSelected(bankAccount: BankAccountEntity, transactionsFilter: AccountTransactionsFilter): Boolean { fun isSelected(bankAccount: BankAccountEntity, transactionsFilter: AccountTransactionsFilter): Boolean {

View File

@ -22,7 +22,7 @@ class BankDataImporterAndExporter {
formatAmount(transaction.amount, decimalSeparator), transaction.currency, formatAmount(transaction.amount, decimalSeparator), transaction.currency,
transaction.valueDate.toString(), transaction.bookingDate.toString(), transaction.valueDate.toString(), transaction.bookingDate.toString(),
transaction.reference, transaction.reference,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId
// TODO: export all columns / transaction data // TODO: export all columns / transaction data
) )
} }

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.UserAccount import net.codinux.banking.client.model.User
class BankIconService { // TODO: extract to a common library class BankIconService { // TODO: extract to a common library
fun findIconForBank(user: UserAccount) = findIconForBank(user.bankName, user.bankingGroup) fun findIconForBank(user: User) = findIconForBank(user.bankName, user.bankingGroup)
fun findIconForBank(bankName: String, bankingGroup: BankingGroup? = null): String? = when (bankingGroup) { fun findIconForBank(bankName: String, bankingGroup: BankingGroup? = null): String? = when (bankingGroup) {
BankingGroup.Sparkasse -> "https://sparkasse.de/favicon-32x32.png" BankingGroup.Sparkasse -> "https://sparkasse.de/favicon-32x32.png"

View File

@ -15,7 +15,7 @@ import net.codinux.banking.client.service.BankingModelService
import net.codinux.banking.dataaccess.BankingRepository import net.codinux.banking.dataaccess.BankingRepository
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.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.fints.config.FinTsClientConfiguration import net.codinux.banking.fints.config.FinTsClientConfiguration
import net.codinux.banking.fints.config.FinTsClientOptions import net.codinux.banking.fints.config.FinTsClientOptions
import net.codinux.banking.ui.IOorDefault import net.codinux.banking.ui.IOorDefault
@ -57,11 +57,11 @@ class BankingService(
} }
fun getAllUserAccounts() = bankingRepository.getAllUserAccounts() fun getAllUsers() = bankingRepository.getAllUsers()
fun getAllAccountTransactions() = bankingRepository.getAllAccountTransactions() fun getAllAccountTransactions() = bankingRepository.getAllAccountTransactions()
fun getAllTransactionsOfUserAccount(userAccount: UserAccountEntity) = bankingRepository.getAllTransactionsOfUserAccount(userAccount) fun getAllTransactionsOfUser(user: UserEntity) = bankingRepository.getAllTransactionsOfUser(user)
fun getAllAccountTransactionsAsViewModel() = bankingRepository.getAllAccountTransactionsAsViewModel() fun getAllAccountTransactionsAsViewModel() = bankingRepository.getAllAccountTransactionsAsViewModel()
@ -95,15 +95,15 @@ class BankingService(
private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) { private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) {
try { try {
val newUser = response.user val newUser = response.user
newUser.displayIndex = uiState.userAccounts.value.size newUser.displayIndex = uiState.users.value.size
val newUserEntity = bankingRepository.persistUserAccount(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 userAccounts = uiState.userAccounts.value.toMutableList() val users = uiState.users.value.toMutableList()
userAccounts.add(newUserEntity) users.add(newUserEntity)
uiState.userAccounts.value = userAccounts uiState.users.value = users
updateTransactionsInUi(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities }) updateTransactionsInUi(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities })
} catch (e: Throwable) { } catch (e: Throwable) {
@ -115,15 +115,15 @@ 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.userAccount, selectedAccount.bankAccount) updateAccountTransactions(selectedAccount.user, selectedAccount.bankAccount)
} else { } else {
uiState.userAccounts.value.forEach { user -> uiState.users.value.forEach { user ->
updateAccountTransactions(user) updateAccountTransactions(user)
} }
} }
} }
private suspend fun updateAccountTransactions(user: UserAccountEntity, bankAccount: BankAccountEntity? = null) { private suspend fun updateAccountTransactions(user: UserEntity, bankAccount: BankAccountEntity? = null) {
withContext(Dispatchers.IOorDefault) { withContext(Dispatchers.IOorDefault) {
try { try {
val response = client.updateAccountTransactionsAsync(user, bankAccount?.let { listOf(it) }) val response = client.updateAccountTransactionsAsync(user, bankAccount?.let { listOf(it) })
@ -138,22 +138,22 @@ class BankingService(
} }
} }
private suspend fun handleSuccessfulUpdateAccountTransactionsResponse(user: UserAccountEntity, responses: List<GetTransactionsResponse>) { private suspend fun handleSuccessfulUpdateAccountTransactionsResponse(user: UserEntity, responses: List<GetTransactionsResponse>) {
try { try {
// TODO: when user gets updated by BankingClient, also update user in database // TODO: when user gets updated by BankingClient, also update user in database
// val newUser = response.user // val newUser = response.user
// val newUserEntity = bankingRepository.persistUserAccount(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 userAccountTransactions = getAllTransactionsOfUserAccount(user) val userTransactions = getAllTransactionsOfUser(user)
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) ?: user.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 = userAccountTransactions.filter { it.bankAccountId == account.id } val existingAccountTransactions = userTransactions.filter { it.bankAccountId == account.id }
val newTransactions = modelService.findNewTransactions(response.bookedTransactions, existingAccountTransactions) val newTransactions = modelService.findNewTransactions(response.bookedTransactions, existingAccountTransactions)
@ -182,7 +182,7 @@ class BankingService(
} }
suspend fun transferMoney(user: UserAccountEntity, account: BankAccountEntity, suspend fun transferMoney(user: UserEntity, 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(

View File

@ -1,8 +1,8 @@
package net.codinux.banking.ui.service package net.codinux.banking.ui.service
import net.codinux.banking.client.model.Amount import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.UserAccount import net.codinux.banking.client.model.User
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.ui.extensions.toBigDecimal import net.codinux.banking.ui.extensions.toBigDecimal
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
@ -13,8 +13,8 @@ class CalculatorService {
// TODO: find a better solution // TODO: find a better solution
Amount(transactions.sumOf { it.amount.toBigDecimal() }.toString()) Amount(transactions.sumOf { it.amount.toBigDecimal() }.toString())
fun calculateBalanceOfUserAccount(userAccount: UserAccount): Amount = fun calculateBalanceOfUser(user: User): Amount =
sumAmounts(userAccount.accounts.map { it.balance }) sumAmounts(user.accounts.map { it.balance })
fun sumAmounts(amounts: Collection<Amount>): Amount = fun sumAmounts(amounts: Collection<Amount>): Amount =
// TODO: find a better solution // TODO: find a better solution
@ -28,9 +28,9 @@ class CalculatorService {
// TODO: find a better solution // TODO: find a better solution
Amount(transactions.map { it.amount.toBigDecimal() }.filter { it < 0 }.sum().toString()) Amount(transactions.map { it.amount.toBigDecimal() }.filter { it < 0 }.sum().toString())
fun calculateBalanceOfDisplayedTransactions(transactions: Collection<AccountTransactionViewModel>, userAccounts: Collection<UserAccountEntity>, filter: AccountTransactionsFilter): Amount { fun calculateBalanceOfDisplayedTransactions(transactions: Collection<AccountTransactionViewModel>, users: Collection<UserEntity>, filter: AccountTransactionsFilter): Amount {
if (filter.noFiltersApplied) { if (filter.noFiltersApplied) {
return sumAmounts(userAccounts.flatMap { it.accounts.map { it.balance } }) return sumAmounts(users.flatMap { it.accounts.map { it.balance } })
} }
val selectedAccount = filter.selectedAccount val selectedAccount = filter.selectedAccount
@ -39,7 +39,7 @@ class CalculatorService {
if (selectedAccount.bankAccount != null) { if (selectedAccount.bankAccount != null) {
selectedAccount.bankAccount.balance selectedAccount.bankAccount.balance
} else { } else {
calculateBalanceOfUserAccount(selectedAccount.userAccount) calculateBalanceOfUser(selectedAccount.user)
} }
} else { } else {
sumTransactions(transactions) sumTransactions(transactions)

View File

@ -33,7 +33,7 @@ class RecipientFinder(private val bankFinder: BankFinder) {
suspend fun updateData(transactions: List<AccountTransaction>) { suspend fun updateData(transactions: List<AccountTransaction>) {
availableRecipients = transactions.mapNotNull { availableRecipients = transactions.mapNotNull {
if (it.otherPartyName != null && it.otherPartyAccountId != null) { if (it.otherPartyName != null && it.otherPartyAccountId != null) {
RecipientSuggestion(it.otherPartyName!!, it.otherPartyBankCode, it.otherPartyAccountId!!) RecipientSuggestion(it.otherPartyName!!, it.otherPartyBankId, it.otherPartyAccountId!!)
} else { } else {
null null
} }
@ -45,7 +45,7 @@ class RecipientFinder(private val bankFinder: BankFinder) {
transactionsByIban = transactions.filter { it.otherPartyAccountId != null }.groupBy { it.otherPartyAccountId!! } transactionsByIban = transactions.filter { it.otherPartyAccountId != null }.groupBy { it.otherPartyAccountId!! }
.mapValues { it.value.map { .mapValues { it.value.map {
PaymentDataSuggestion(it.reference, Amount(it.amount.amount.replace("-", "")), it.currency, it.valueDate) PaymentDataSuggestion(it.reference ?: "", Amount(it.amount.amount.replace("-", "")), it.currency, it.valueDate)
}.toSet().sortedByDescending { it.valueDate } } }.toSet().sortedByDescending { it.valueDate } }
} }

View File

@ -5,7 +5,7 @@ import androidx.compose.material.DrawerValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserEntity
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
@ -15,7 +15,7 @@ import net.codinux.banking.ui.model.events.TransferredMoneyEvent
class UiState : ViewModel() { class UiState : ViewModel() {
val userAccounts = MutableStateFlow<List<UserAccountEntity>>(emptyList()) val users = MutableStateFlow<List<UserEntity>>(emptyList())
val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList()) val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())

View File

@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS AccountTransaction (
amount TEXT NOT NULL, amount TEXT NOT NULL,
currency TEXT NOT NULL, currency TEXT NOT NULL,
unparsedReference TEXT NOT NULL, unparsedReference TEXT,
bookingDate TEXT NOT NULL, bookingDate TEXT NOT NULL,
valueDate TEXT NOT NULL, valueDate TEXT NOT NULL,
@ -21,8 +21,6 @@ CREATE TABLE IF NOT EXISTS AccountTransaction (
category TEXT, category TEXT,
notes TEXT, notes TEXT,
information TEXT,
statementNumber INTEGER, statementNumber INTEGER,
sequenceNumber INTEGER, sequenceNumber INTEGER,
@ -34,17 +32,16 @@ CREATE TABLE IF NOT EXISTS AccountTransaction (
mandateReference TEXT, mandateReference TEXT,
creditorIdentifier TEXT, creditorIdentifier TEXT,
originatorsIdentificationCode TEXT, originatorsIdentificationCode TEXT,
compensationAmount TEXT, compensationAmount TEXT,
originalAmount TEXT, originalAmount TEXT,
sepaReference TEXT,
deviantOriginator TEXT, deviantOriginator TEXT,
deviantRecipient TEXT, deviantRecipient TEXT,
referenceWithNoSpecialType TEXT, referenceWithNoSpecialType TEXT,
primaNotaNumber TEXT, primaNotaNumber TEXT,
textKeySupplement TEXT, textKeySupplement TEXT,
currencyType TEXT,
bookingKey TEXT,
referenceForTheAccountOwner TEXT, referenceForTheAccountOwner TEXT,
referenceOfTheAccountServicingInstitution TEXT, referenceOfTheAccountServicingInstitution TEXT,
supplementaryDetails TEXT, supplementaryDetails TEXT,
@ -65,22 +62,16 @@ INSERT INTO AccountTransaction(
userSetDisplayName, category, notes, userSetDisplayName, category, notes,
information,
statementNumber, sequenceNumber, statementNumber, sequenceNumber,
openingBalance, closingBalance, openingBalance, closingBalance,
endToEndReference, customerReference, mandateReference, endToEndReference, customerReference, mandateReference,
creditorIdentifier, originatorsIdentificationCode, creditorIdentifier, originatorsIdentificationCode,
compensationAmount, originalAmount, compensationAmount, originalAmount,
sepaReference,
deviantOriginator, deviantRecipient, deviantOriginator, deviantRecipient,
referenceWithNoSpecialType, referenceWithNoSpecialType,
primaNotaNumber, textKeySupplement, primaNotaNumber, textKeySupplement,
currencyType, bookingKey,
referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution,
supplementaryDetails,
transactionReferenceNumber, relatedReferenceNumber transactionReferenceNumber, relatedReferenceNumber
) )
VALUES( VALUES(
@ -93,22 +84,16 @@ VALUES(
?, ?, ?, ?, ?, ?,
?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?,
?, ?, ?, ?,
?, ?,
?, ?, ?, ?,
?, ?,
?, ?,
?,
?, ? ?, ?
); );
@ -118,11 +103,11 @@ SELECT AccountTransaction.*
FROM AccountTransaction; FROM AccountTransaction;
selectAllTransactionsAsViewModel: selectAllTransactionsAsViewModel:
SELECT id, userAccountId, bankAccountId, amount, currency, unparsedReference, valueDate, otherPartyName, bookingText, sepaReference, userSetDisplayName, category SELECT id, userAccountId, bankAccountId, amount, currency, unparsedReference, valueDate, otherPartyName, bookingText, userSetDisplayName, category
FROM AccountTransaction; FROM AccountTransaction;
selectAllTransactionsOfUserAccount: selectAllTransactionsOfUser:
SELECT AccountTransaction.* SELECT AccountTransaction.*
FROM AccountTransaction WHERE userAccountId = ?; FROM AccountTransaction WHERE userAccountId = ?;

View File

@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS UserAccount (
bic TEXT NOT NULL, bic TEXT NOT NULL,
customerName TEXT NOT NULL, customerName TEXT NOT NULL,
userId TEXT NOT NULL, userId TEXT,
selectedTanMethodId TEXT, selectedTanMethodId TEXT,
@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS UserAccount (
); );
insertUserAccount: insertUser:
INSERT INTO UserAccount( INSERT INTO UserAccount(
bankCode, loginName, password, bankCode, loginName, password,
@ -58,7 +58,7 @@ VALUES(
); );
selectAllUserAccounts: selectAllUsers:
SELECT UserAccount.* SELECT UserAccount.*
FROM UserAccount; FROM UserAccount;

View File

@ -5,7 +5,7 @@ import androidx.compose.runtime.Composable
import net.codinux.banking.client.model.BankAccountType import net.codinux.banking.client.model.BankAccountType
import net.codinux.banking.client.model.BankAccountViewInfo import net.codinux.banking.client.model.BankAccountViewInfo
import net.codinux.banking.client.model.BankingGroup import net.codinux.banking.client.model.BankingGroup
import net.codinux.banking.client.model.UserAccountViewInfo import net.codinux.banking.client.model.BankViewInfo
import net.codinux.banking.client.model.tan.* import net.codinux.banking.client.model.tan.*
import net.codinux.banking.ui.model.TanChallengeReceived import net.codinux.banking.ui.model.TanChallengeReceived
@ -13,7 +13,7 @@ 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 = UserAccountViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank) val user = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
val tanChallenge = TanChallenge(TanChallengeType.EnterTan, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, user = user) val tanChallenge = TanChallenge(TanChallengeType.EnterTan, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, user = user)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { } EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
@ -27,7 +27,7 @@ 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 = UserAccountViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank", BankingGroup.Comdirect) val user = 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, user)
@ -50,7 +50,7 @@ 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 = UserAccountViewInfo("10010010", "Ihr krasser Login Name", "Eine ganz gewöhnliche Sparkasse", BankingGroup.Sparkasse) val user = 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, user, account)

View File

@ -25,14 +25,14 @@ class SqliteBankingRepositoryTest {
@Test @Test
fun saveUserAccount() = runTest { fun saveUserAccount() = runTest {
val bankAccounts = listOf( val bankAccounts = listOf(
BankAccount("12345", "Monika Tester", BankAccountType.CheckingAccount, balance = Amount("12.34"), retrievedTransactionsFrom = LocalDate(2024, 5, 7), features = setOf(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.InstantPayment), countDaysForWhichTransactionsAreKept = 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 userAccount = UserAccount("12345678", "SupiDupiUser", "geheim", "Abzock-Bank", "ABCDDEBBXXX", "Monika Tester", accounts = bankAccounts, bankingGroup = BankingGroup.DKB).apply { val userAccount = User("12345678", "SupiDupiUser", "geheim", "Abzock-Bank", "ABCDDEBBXXX", "Monika Tester", accounts = bankAccounts, bankingGroup = BankingGroup.DKB, finTsServerAddress = "").apply {
wrongCredentialsEntered = true wrongCredentialsEntered = true
displayIndex = 99 displayIndex = 99
} }
val persisted = underTest.persistUserAccount(userAccount) val persisted = underTest.persistUser(userAccount)
assertNotNull(persisted.id) assertNotNull(persisted.id)
@ -55,7 +55,7 @@ class SqliteBankingRepositoryTest {
val persistedBankAccount = persisted.accounts.first() val persistedBankAccount = persisted.accounts.first()
assertNotNull(persistedBankAccount.id) assertNotNull(persistedBankAccount.id)
assertEquals(persisted.id, persistedBankAccount.userAccountId) assertEquals(persisted.id, persistedBankAccount.userId)
assertEquals(bankAccounts.first().identifier, persistedBankAccount.identifier) assertEquals(bankAccounts.first().identifier, persistedBankAccount.identifier)
assertEquals(bankAccounts.first().accountHolderName, persistedBankAccount.accountHolderName) assertEquals(bankAccounts.first().accountHolderName, persistedBankAccount.accountHolderName)
@ -67,7 +67,7 @@ class SqliteBankingRepositoryTest {
assertEquals(bankAccounts.first().features, persistedBankAccount.features) assertEquals(bankAccounts.first().features, persistedBankAccount.features)
assertEquals(bankAccounts.first().countDaysForWhichTransactionsAreKept, persistedBankAccount.countDaysForWhichTransactionsAreKept) assertEquals(bankAccounts.first().serverTransactionsRetentionDays, persistedBankAccount.serverTransactionsRetentionDays)
assertEquals(bankAccounts.first().hideAccount, persistedBankAccount.hideAccount) assertEquals(bankAccounts.first().hideAccount, persistedBankAccount.hideAccount)
assertEquals(bankAccounts.first().includeInAutomaticAccountsUpdate, persistedBankAccount.includeInAutomaticAccountsUpdate) assertEquals(bankAccounts.first().includeInAutomaticAccountsUpdate, persistedBankAccount.includeInAutomaticAccountsUpdate)
} }
@ -88,7 +88,7 @@ class SqliteBankingRepositoryTest {
assertEquals(transaction.amount, persisted.amount) assertEquals(transaction.amount, persisted.amount)
assertEquals(transaction.currency, persisted.currency) assertEquals(transaction.currency, persisted.currency)
assertEquals(transaction.unparsedReference, persisted.unparsedReference) assertEquals(transaction.reference, persisted.reference)
assertEquals(transaction.bookingDate, persisted.bookingDate) assertEquals(transaction.bookingDate, persisted.bookingDate)
assertEquals(transaction.valueDate, persisted.valueDate) assertEquals(transaction.valueDate, persisted.valueDate)
assertEquals(transaction.otherPartyName, persisted.otherPartyName) assertEquals(transaction.otherPartyName, persisted.otherPartyName)
@ -109,7 +109,7 @@ class SqliteBankingRepositoryTest {
assertEquals(transaction.amount, persisted.amount) assertEquals(transaction.amount, persisted.amount)
assertEquals(transaction.currency, persisted.currency) assertEquals(transaction.currency, persisted.currency)
assertEquals(transaction.unparsedReference, persisted.reference) assertEquals(transaction.reference, persisted.reference)
assertEquals(transaction.valueDate, persisted.valueDate) assertEquals(transaction.valueDate, persisted.valueDate)
assertEquals(transaction.otherPartyName, persisted.otherPartyName) assertEquals(transaction.otherPartyName, persisted.otherPartyName)
} }