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
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.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserAccountEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
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>
@ -20,7 +20,7 @@ interface BankingRepository {
fun getAllAccountTransactions(): List<AccountTransactionEntity>
fun getAllTransactionsOfUserAccount(userAccount: UserAccountEntity): List<AccountTransactionEntity>
fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity>
fun getTransactionById(transactionId: Long): AccountTransactionEntity?

View File

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

View File

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

View File

@ -6,108 +6,117 @@ import net.codinux.banking.client.model.Amount
class AccountTransactionEntity(
val id: Long,
val userAccountId: Long,
val userId: Long,
val bankAccountId: Long,
amount: Amount,
currency: String,
reference: String,
reference: String?,
bookingDate: LocalDate,
valueDate: LocalDate,
otherPartyName: String? = null,
otherPartyBankCode: String? = null,
otherPartyBankId: String? = null,
otherPartyAccountId: String? = null,
bookingText: String? = null,
userSetDisplayName: String? = null,
category: String? = null,
notes: String? = null,
information: String? = null,
statementNumber: Int? = null,
sequenceNumber: Int? = null,
postingText: String? = null,
openingBalance: 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,
bankReference: String? = null,
furtherInformation: String? = null,
endToEndReference: String? = null,
mandateReference: String? = null,
creditorIdentifier: String? = null,
originatorsIdentificationCode: String? = null,
compensationAmount: String? = null,
originalAmount: String? = null,
sepaReference: String? = null,
deviantOriginator: String? = null,
deviantRecipient: String? = null,
referenceWithNoSpecialType: String? = null,
primaNotaNumber: String? = null,
textKeySupplement: String? = null,
currencyType: String? = null,
bookingKey: String? = null,
referenceForTheAccountOwner: String? = null,
referenceOfTheAccountServicingInstitution: String? = null,
supplementaryDetails: String? = null,
journalNumber: String? = null,
textKeyAddition: String? = null,
transactionReferenceNumber: String? = null,
relatedReferenceNumber: String? = null
orderReferenceNumber: String? = null,
referenceNumber: String? = null,
isReversal: Boolean = false,
) : AccountTransaction(
amount, currency, reference,
bookingDate, valueDate,
otherPartyName, otherPartyBankCode, otherPartyAccountId,
bookingText,
otherPartyName, otherPartyBankId, otherPartyAccountId,
postingText,
information,
statementNumber, sequenceNumber,
openingBalance, closingBalance,
endToEndReference, customerReference, mandateReference,
statementNumber, sheetNumber,
customerReference, bankReference, furtherInformation,
endToEndReference, mandateReference,
creditorIdentifier, originatorsIdentificationCode,
compensationAmount, originalAmount,
sepaReference,
deviantOriginator, deviantRecipient,
referenceWithNoSpecialType, primaNotaNumber,
textKeySupplement,
referenceWithNoSpecialType,
currencyType, bookingKey,
referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution,
supplementaryDetails,
journalNumber, textKeyAddition,
transactionReferenceNumber, relatedReferenceNumber,
orderReferenceNumber, referenceNumber,
isReversal,
userSetDisplayName, category, notes
) {
constructor(id: Long, userAccountId: Long, bankAccountId: Long, transaction: AccountTransaction) : this(
id, userAccountId, bankAccountId,
transaction.amount, transaction.currency, transaction.unparsedReference, transaction.bookingDate, transaction.valueDate,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText,
constructor(id: Long, userId: Long, bankAccountId: Long, transaction: AccountTransaction) : this(
id, userId, bankAccountId,
transaction.amount, transaction.currency, transaction.reference,
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.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.compensationAmount, transaction.originalAmount,
transaction.sepaReference,
transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement,
transaction.referenceWithNoSpecialType,
transaction.currencyType, transaction.bookingKey,
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution,
transaction.supplementaryDetails,
transaction.journalNumber, transaction.textKeyAddition,
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber
transaction.orderReferenceNumber, transaction.referenceNumber,
transaction.isReversal,
)
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(
val id: Long,
val userAccountId: Long,
val userId: Long,
identifier: String,
subAccountNumber: String? = null,
iban: String? = null,
productName: String? = null,
accountHolderName: String,
type: BankAccountType = BankAccountType.Other,
iban: String? = null,
subAccountNumber: String? = null,
productName: String? = null,
currency: String = "EUR",
accountLimit: String? = null,
balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
isAccountTypeSupportedByApplication: Boolean = true,
features: Set<BankAccountFeatures> = emptySet(),
countDaysForWhichTransactionsAreKept: Int? = null,
balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
serverTransactionsRetentionDays: Int? = null,
lastTransactionsRetrievalTime: Instant? = null,
retrievedTransactionsFrom: LocalDate? = null,
bookedTransactions: MutableList<AccountTransactionEntity> = mutableListOf(),
unbookedTransactions: MutableList<UnbookedAccountTransaction> = mutableListOf(),
prebookedTransactions: MutableList<PrebookedAccountTransaction> = mutableListOf(),
userSetDisplayName: String? = null,
displayIndex: Int = 0,
@ -35,21 +36,39 @@ class BankAccountEntity(
hideAccount: Boolean = false,
includeInAutomaticAccountsUpdate: Boolean = true
) : BankAccount(
identifier, accountHolderName, type, iban, subAccountNumber, productName, currency, accountLimit,
balance, isAccountTypeSupportedByApplication, features,
lastTransactionsRetrievalTime, retrievedTransactionsFrom, countDaysForWhichTransactionsAreKept,
bookedTransactions as MutableList<AccountTransaction>, unbookedTransactions,
userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate
identifier, subAccountNumber, iban, productName,
accountHolderName, type, currency, accountLimit,
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(
id, userAccountId,
account.identifier, account.accountHolderName, account.type, account.iban, account.subAccountNumber,
account.productName, account.currency, account.accountLimit,
account.balance,
constructor(id: Long, userId: Long, account: BankAccount, transactions: List<AccountTransactionEntity> = emptyList()) : this(
id, userId,
account.identifier, account.subAccountNumber, account.iban, account.productName,
account.accountHolderName, account.type,
account.currency, account.accountLimit,
account.isAccountTypeSupportedByApplication, account.features,
account.countDaysForWhichTransactionsAreKept, account.lastTransactionsRetrievalTime, account.retrievedTransactionsFrom,
account.balance,
account.serverTransactionsRetentionDays,
account.lastTransactionsRetrievalTime, account.retrievedTransactionsFrom,
transactions.toMutableList(), mutableListOf(),
account.userSetDisplayName, account.displayIndex, account.hideAccount, account.includeInAutomaticAccountsUpdate
account.userSetDisplayName, account.displayIndex,
account.hideAccount, account.includeInAutomaticAccountsUpdate
)
val bookedTransactionsEntities: MutableList<AccountTransactionEntity> = bookedTransactions

View File

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

View File

@ -1,7 +1,6 @@
package net.codinux.banking.ui.appskeleton
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.graphics.Color
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.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import bankmeister.composeapp.generated.resources.Res
import bankmeister.composeapp.generated.resources.filter_alt
import kotlinx.coroutines.launch
@ -39,7 +34,7 @@ private val IconWidth = 48.dp
@Composable
fun BottomBar(showMenuDrawer: Boolean = true) {
val userAccounts by uiState.userAccounts.collectAsState()
val users by uiState.users.collectAsState()
val transactionsFilter by uiState.transactionsFilter.collectAsState()
@ -75,7 +70,7 @@ fun BottomBar(showMenuDrawer: Boolean = true) {
} else if (selectedAccount.bankAccount != null) {
selectedAccount.bankAccount.displayName
} else {
selectedAccount.userAccount.displayName
selectedAccount.user.displayName
}
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) {
Row(Modifier.fillMaxHeight().widthIn(IconWidth, IconWidth), verticalAlignment = Alignment.CenterVertically) {
IconButton({ showSearchbar = true }, Modifier.width(IconWidth)) {

View File

@ -54,7 +54,7 @@ private val VerticalSpacing = 8.dp
fun SideMenuContent() {
val drawerState = uiState.drawerState.collectAsState().value
val accounts = uiState.userAccounts.collectAsState().value
val accounts = uiState.users.collectAsState().value
val coroutineScope = rememberCoroutineScope()

View File

@ -9,8 +9,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.UserAccountViewInfo
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.BankViewInfo
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.BankInfo
import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper
@ -22,8 +22,8 @@ private val bankIconService = DI.bankIconService
private val DefaultIconModifier: Modifier = Modifier.size(16.dp)
@Composable
fun BankIcon(userAccount: UserAccount?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, iconResource: DrawableResource? = null) {
val iconUrl by remember(userAccount?.bic) { mutableStateOf(userAccount?.let { bankIconService.findIconForBank(it) }) }
fun BankIcon(user: User?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, iconResource: DrawableResource? = null) {
val iconUrl by remember(user?.bic) { mutableStateOf(user?.let { bankIconService.findIconForBank(it) }) }
BankIcon(iconUrl, modifier, iconModifier, iconResource = iconResource)
}
@ -38,8 +38,8 @@ fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifi
}
@Composable
fun BankIcon(userAccount: UserAccountViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, iconResource: DrawableResource? = null) {
val iconUrl = userAccount?.let { bankIconService.findIconForBank(it.bankName, it.bankingGroup) }
fun BankIcon(user: BankViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, iconResource: DrawableResource? = null) {
val iconUrl = user?.let { bankIconService.findIconForBank(it.bankName, it.bankingGroup) }
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.account
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
private val uiState = DI.uiState
@ -24,9 +24,9 @@ fun BanksList(
textColor: Color = Color.White,
itemModifier: Modifier = Modifier.height(48.dp).widthIn(min = 300.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) {
@ -34,16 +34,16 @@ fun BanksList(
accountSelected?.invoke(null, null)
}
userAccounts.value.sortedBy { it.displayIndex }.forEach { userAccount ->
users.value.sortedBy { it.displayIndex }.forEach { user ->
Spacer(Modifier.fillMaxWidth().height(12.dp))
NavigationMenuItem(itemModifier, userAccount.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, userAccount, iconResource = Res.drawable.account) {
accountSelected?.invoke(userAccount, null)
NavigationMenuItem(itemModifier, user.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, user, iconResource = Res.drawable.account) {
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) {
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 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.DI
import org.jetbrains.compose.resources.DrawableResource
@ -37,7 +37,7 @@ fun NavigationMenuItem(
iconSize: Dp = 24.dp,
iconTextSpacing: Dp = 24.dp,
horizontalPadding: Dp = 8.dp,
userAccount: UserAccountEntity? = null,
user: UserEntity? = null,
bankAccount: BankAccountEntity? = null,
iconResource: DrawableResource? = null,
icon: (@Composable () -> Unit)? = null,
@ -54,7 +54,7 @@ fun NavigationMenuItem(
modifier = modifier
.clickable { onClick?.invoke() }
.let {
if (userAccount != null && filterService.isSelected(userAccount, transactionsFilter)
if (user != null && filterService.isSelected(user, transactionsFilter)
|| bankAccount != null && filterService.isSelected(bankAccount, transactionsFilter)) {
it.background(Colors.AccentAsSelectionBackground, shape = RoundedCornerShape(4.dp))
} else {
@ -68,7 +68,7 @@ fun NavigationMenuItem(
icon()
}
} 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)
@ -77,8 +77,8 @@ fun NavigationMenuItem(
null
} else if (bankAccount != null) {
bankAccount.balance
} else if (userAccount != null) {
calculator.calculateBalanceOfUserAccount(userAccount)
} else if (user != null) {
calculator.calculateBalanceOfUser(user)
} else {
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.sp
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.forms.RoundedCornersCard
import net.codinux.banking.ui.model.AccountTransactionViewModel
@ -28,7 +28,7 @@ private val formatUtil = DI.formatUtil
fun GroupedTransactionsListItems(
modifier: Modifier,
transactionsToDisplay: List<AccountTransactionViewModel>,
userAccountsId: Map<Long, UserAccountEntity>,
userAccountsId: Map<Long, UserEntity>,
transactionsGrouping: TransactionsGrouping
) {
val groupingService = remember { TransactionsGroupingService() }
@ -57,7 +57,9 @@ fun GroupedTransactionsListItems(
RoundedCornersCard {
Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed
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.dp
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.config.Colors
import net.codinux.banking.ui.config.DI
@ -25,7 +25,7 @@ private val uiSettings = DI.uiSettings
private val formatUtil = DI.formatUtil
@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 showBankIcons by uiSettings.showBankIcons.collectAsState()
@ -48,9 +48,9 @@ fun TransactionListItem(userAccount: UserAccount?, transaction: AccountTransacti
val transactionEntity = DI.bankingService.getTransaction(transaction.id)
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,
transactionEntity?.otherPartyBankCode,
transactionEntity?.otherPartyBankId,
transactionEntity?.otherPartyAccountId,
if (withSameData) transaction.amount else null,
if (withSameData) transaction.reference else null
@ -76,7 +76,7 @@ fun TransactionListItem(userAccount: UserAccount?, transaction: AccountTransacti
Column(Modifier.weight(1f)) {
Row {
if (showBankIcons) {
BankIcon(userAccount, Modifier.padding(end = 6.dp))
BankIcon(user, Modifier.padding(end = 6.dp))
}
Text(
@ -90,7 +90,7 @@ fun TransactionListItem(userAccount: UserAccount?, transaction: AccountTransacti
Spacer(modifier = Modifier.height(6.dp))
Text(
text = transaction.reference,
text = transaction.reference ?: "",
Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis

View File

@ -25,9 +25,9 @@ private val formatUtil = DI.formatUtil
@Composable
fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean = true) {
val userAccounts by uiState.userAccounts.collectAsState()
val userAccountsId by remember(userAccounts) {
derivedStateOf { userAccounts.associateBy { it.id } }
val users by uiState.users.collectAsState()
val usersById by remember(users) {
derivedStateOf { users.associateBy { it.id } }
}
val transactionsFilter by uiState.transactionsFilter.collectAsState()
@ -53,17 +53,19 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
Spacer(Modifier.weight(1f))
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))
}
}
if (transactionsGrouping != TransactionsGrouping.None) {
GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, userAccountsId, transactionsGrouping)
GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, usersById, transactionsGrouping)
} else {
LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) {
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,
onDismiss: () -> Unit,
) {
val userAccounts = uiState.userAccounts.value
val userAccounts = uiState.users.value
val accountsToUserAccount = userAccounts.sortedBy { it.displayIndex }
.flatMap { user -> user.accounts.sortedBy { it.displayIndex }.map { it to user } }.toMap()
val accountsSupportingTransferringMoney = userAccounts.flatMap { it.accounts }
.filter { it.supportsAnyFeature(BankAccountFeatures.TransferMoney, BankAccountFeatures.InstantPayment) }
.filter { it.supportsMoneyTransfer }
if (accountsSupportingTransferringMoney.isEmpty()) {
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 amount by remember { mutableStateOf(data.amount?.amount ?: "") }
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) }
val isRequiredDataEntered by remember(recipientName, recipientAccountIdentifier, amount) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package net.codinux.banking.ui.service
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.AccountTransactionsFilter
import net.codinux.banking.ui.model.BankAccountFilter
@ -30,27 +30,27 @@ class AccountTransactionsFilterService {
}
private fun matchesFilter(transaction: AccountTransactionViewModel, accountsFilter: List<BankAccountFilter>): Boolean =
accountsFilter.any { (userAccount, bankAccount) ->
accountsFilter.any { (user, bankAccount) ->
if (bankAccount != null) {
transaction.bankAccountId == bankAccount.id
} else {
transaction.userAccountId == userAccount.id
transaction.userId == user.id
}
}
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))
fun isSelected(userAccount: UserAccountEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
fun isSelected(user: UserEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
if (transactionsFilter.showAllAccounts) {
return false
}
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 {

View File

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

View File

@ -1,11 +1,11 @@
package net.codinux.banking.ui.service
import net.codinux.banking.client.model.BankingGroup
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.User
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) {
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.entities.AccountTransactionEntity
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.FinTsClientOptions
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 getAllTransactionsOfUserAccount(userAccount: UserAccountEntity) = bankingRepository.getAllTransactionsOfUserAccount(userAccount)
fun getAllTransactionsOfUser(user: UserEntity) = bankingRepository.getAllTransactionsOfUser(user)
fun getAllAccountTransactionsAsViewModel() = bankingRepository.getAllAccountTransactionsAsViewModel()
@ -95,15 +95,15 @@ class BankingService(
private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) {
try {
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" }
val userAccounts = uiState.userAccounts.value.toMutableList()
userAccounts.add(newUserEntity)
uiState.userAccounts.value = userAccounts
val users = uiState.users.value.toMutableList()
users.add(newUserEntity)
uiState.users.value = users
updateTransactionsInUi(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities })
} catch (e: Throwable) {
@ -115,15 +115,15 @@ class BankingService(
suspend fun updateAccountTransactions() {
val selectedAccount = uiState.transactionsFilter.value.selectedAccount
if (selectedAccount != null) {
updateAccountTransactions(selectedAccount.userAccount, selectedAccount.bankAccount)
updateAccountTransactions(selectedAccount.user, selectedAccount.bankAccount)
} else {
uiState.userAccounts.value.forEach { user ->
uiState.users.value.forEach { user ->
updateAccountTransactions(user)
}
}
}
private suspend fun updateAccountTransactions(user: UserAccountEntity, bankAccount: BankAccountEntity? = null) {
private suspend fun updateAccountTransactions(user: UserEntity, bankAccount: BankAccountEntity? = null) {
withContext(Dispatchers.IOorDefault) {
try {
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 {
// TODO: when user gets updated by BankingClient, also update user in database
// 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" }
val userAccountTransactions = getAllTransactionsOfUserAccount(user)
val userTransactions = getAllTransactionsOfUser(user)
responses.forEach { response ->
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
val existingAccountTransactions = userAccountTransactions.filter { it.bankAccountId == account.id }
val existingAccountTransactions = userTransactions.filter { it.bankAccountId == account.id }
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,
paymentReference: String? = null, instantTransfer: Boolean = false, recipientBankIdentifier: String? = null): Boolean {
val response = client.transferMoneyAsync(TransferMoneyRequestForUser(

View File

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

View File

@ -33,7 +33,7 @@ class RecipientFinder(private val bankFinder: BankFinder) {
suspend fun updateData(transactions: List<AccountTransaction>) {
availableRecipients = transactions.mapNotNull {
if (it.otherPartyName != null && it.otherPartyAccountId != null) {
RecipientSuggestion(it.otherPartyName!!, it.otherPartyBankCode, it.otherPartyAccountId!!)
RecipientSuggestion(it.otherPartyName!!, it.otherPartyBankId, it.otherPartyAccountId!!)
} else {
null
}
@ -45,7 +45,7 @@ class RecipientFinder(private val bankFinder: BankFinder) {
transactionsByIban = transactions.filter { it.otherPartyAccountId != null }.groupBy { it.otherPartyAccountId!! }
.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 } }
}

View File

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

View File

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

View File

@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS UserAccount (
bic TEXT NOT NULL,
customerName TEXT NOT NULL,
userId TEXT NOT NULL,
userId TEXT,
selectedTanMethodId TEXT,
@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS UserAccount (
);
insertUserAccount:
insertUser:
INSERT INTO UserAccount(
bankCode, loginName, password,
@ -58,7 +58,7 @@ VALUES(
);
selectAllUserAccounts:
selectAllUsers:
SELECT 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.BankAccountViewInfo
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.ui.model.TanChallengeReceived
@ -13,7 +13,7 @@ import net.codinux.banking.ui.model.TanChallengeReceived
@Composable
fun EnterTanDialogPreview_EnterTan() {
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)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
@ -27,7 +27,7 @@ fun EnterTanDialogPreview_TanImage() {
val tanMethod = TanMethod("photoTAN-Verfahren", TanMethodType.photoTan, "902", 6, AllowedTanFormat.Numeric)
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)
@ -50,7 +50,7 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
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 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
fun saveUserAccount() = runTest {
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
displayIndex = 99
}
val persisted = underTest.persistUserAccount(userAccount)
val persisted = underTest.persistUser(userAccount)
assertNotNull(persisted.id)
@ -55,7 +55,7 @@ class SqliteBankingRepositoryTest {
val persistedBankAccount = persisted.accounts.first()
assertNotNull(persistedBankAccount.id)
assertEquals(persisted.id, persistedBankAccount.userAccountId)
assertEquals(persisted.id, persistedBankAccount.userId)
assertEquals(bankAccounts.first().identifier, persistedBankAccount.identifier)
assertEquals(bankAccounts.first().accountHolderName, persistedBankAccount.accountHolderName)
@ -67,7 +67,7 @@ class SqliteBankingRepositoryTest {
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().includeInAutomaticAccountsUpdate, persistedBankAccount.includeInAutomaticAccountsUpdate)
}
@ -88,7 +88,7 @@ class SqliteBankingRepositoryTest {
assertEquals(transaction.amount, persisted.amount)
assertEquals(transaction.currency, persisted.currency)
assertEquals(transaction.unparsedReference, persisted.unparsedReference)
assertEquals(transaction.reference, persisted.reference)
assertEquals(transaction.bookingDate, persisted.bookingDate)
assertEquals(transaction.valueDate, persisted.valueDate)
assertEquals(transaction.otherPartyName, persisted.otherPartyName)
@ -109,7 +109,7 @@ class SqliteBankingRepositoryTest {
assertEquals(transaction.amount, persisted.amount)
assertEquals(transaction.currency, persisted.currency)
assertEquals(transaction.unparsedReference, persisted.reference)
assertEquals(transaction.reference, persisted.reference)
assertEquals(transaction.valueDate, persisted.valueDate)
assertEquals(transaction.otherPartyName, persisted.otherPartyName)
}