Updated to model changes: User has been renamed to BankAccess and bic is now nullable

This commit is contained in:
dankito 2024-09-16 17:08:43 +02:00
parent 41586b0148
commit 9330b72726
27 changed files with 220 additions and 223 deletions

View File

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

View File

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

View File

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

View File

@ -19,53 +19,53 @@ open class SqliteBankingRepository(
private val database = BankmeisterDb(sqlDriver)
private val userQueries = database.userQueries
private val bankQueries = database.userQueries
private val accountTransactionQueries = database.accountTransactionQueries
private val log by logger()
override fun getAllUsers(): List<UserEntity> {
override fun getAllBanks(): List<BankAccessEntity> {
val bankAccounts = getAllBankAccounts().groupBy { it.userId }
val tanMethods = getAllTanMethods().groupBy { it.userId }
val tanMedia = getAllTanMedia().groupBy { it.userId }
val holdings = getAllHoldings().groupBy { it.bankAccountId }
return userQueries.selectAllUsers { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, userSetDisplayName, clientData, displayIndex, iconUrl, wrongCredentialsEntered ->
UserEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, getAccountsOfUser(id, bankAccounts, holdings), selectedTanMethodIdentifier, tanMethods[id] ?: emptyList(), selectedTanMediumIdentifier, tanMedia[id] ?: emptyList(),
return bankQueries.selectAllBanks { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, userSetDisplayName, clientData, displayIndex, iconUrl, wrongCredentialsEntered ->
BankAccessEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, getAccountsOfBank(id, bankAccounts, holdings), selectedTanMethodIdentifier, tanMethods[id] ?: emptyList(), selectedTanMediumIdentifier, tanMedia[id] ?: emptyList(),
bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered)
}.executeAsList()
}
protected open fun getAccountsOfUser(userId: Long, bankAccounts: Map<Long, List<BankAccountEntity>>, holdings: Map<Long, List<HoldingEntity>>): List<BankAccountEntity> {
return bankAccounts[userId].orEmpty().onEach {
protected open fun getAccountsOfBank(bankId: Long, bankAccounts: Map<Long, List<BankAccountEntity>>, holdings: Map<Long, List<HoldingEntity>>): List<BankAccountEntity> {
return bankAccounts[bankId].orEmpty().onEach {
it.holdings = holdings[it.id].orEmpty()
}
}
override suspend fun persistUser(user: User): UserEntity {
return userQueries.transactionWithResult {
userQueries.insertUser(user.bankCode, user.loginName, user.password, user.bankName, user.bic,
user.customerName, user.userId, user.selectedTanMethodIdentifier, user.selectedTanMediumIdentifier,
user.bankingGroup?.name, user.serverAddress, null, user.userSetDisplayName, user.displayIndex.toLong(), user.iconUrl, user.wrongCredentialsEntered
override suspend fun persistBank(bank: BankAccess): BankAccessEntity {
return bankQueries.transactionWithResult {
bankQueries.insertBank(bank.bankCode, bank.loginName, bank.password, bank.bankName, bank.bic,
bank.customerName, bank.userId, bank.selectedTanMethodIdentifier, bank.selectedTanMediumIdentifier,
bank.bankingGroup?.name, bank.serverAddress, null, bank.userSetDisplayName, bank.displayIndex.toLong(), bank.iconUrl, bank.wrongCredentialsEntered
)
val userId = getLastInsertedId() // getLastInsertedId() / last_insert_rowid() has to be called in a transaction with the insert operation, otherwise it will not work
val bankId = getLastInsertedId() // getLastInsertedId() / last_insert_rowid() has to be called in a transaction with the insert operation, otherwise it will not work
val bankAccounts = persistBankAccounts(userId, user.accounts)
val bankAccounts = persistBankAccounts(bankId, bank.accounts)
val tanMethods = persistTanMethods(userId, user.tanMethods)
val tanMedia = persistTanMedia(userId, user.tanMedia)
val tanMethods = persistTanMethods(bankId, bank.tanMethods)
val tanMedia = persistTanMedia(bankId, bank.tanMedia)
UserEntity(userId, user, bankAccounts, tanMethods, tanMedia)
BankAccessEntity(bankId, bank, bankAccounts, tanMethods, tanMedia)
}
}
fun getAllBankAccounts(): List<BankAccountEntity> = userQueries.selectAllBankAccounts { id, userId, identifier, subAccountNumber, iban, productName, accountHolderName, type, currency, accountLimit, isAccountTypeSupportedByApplication, features, balance, serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ->
fun getAllBankAccounts(): List<BankAccountEntity> = bankQueries.selectAllBankAccounts { id, bankId, identifier, subAccountNumber, iban, productName, accountHolderName, type, currency, accountLimit, isAccountTypeSupportedByApplication, features, balance, serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ->
BankAccountEntity(
id, userId,
id, bankId,
identifier, subAccountNumber, iban, productName,
@ -86,15 +86,15 @@ open class SqliteBankingRepository(
)
}.executeAsList()
private suspend fun persistBankAccounts(userId: Long, bankAccounts: Collection<BankAccount>): List<BankAccountEntity> =
bankAccounts.map { persistBankAccount(userId, it) }
private suspend fun persistBankAccounts(bankId: Long, bankAccounts: Collection<BankAccount>): List<BankAccountEntity> =
bankAccounts.map { persistBankAccount(bankId, it) }
/**
* Has to be executed in a transaction in order that getting persisted BankAccount's id works~
*/
private suspend fun persistBankAccount(userId: Long, account: BankAccount): BankAccountEntity {
userQueries.insertBankAccount(
userId,
private suspend fun persistBankAccount(bankId: Long, account: BankAccount): BankAccountEntity {
bankQueries.insertBankAccount(
bankId,
account.identifier, account.accountHolderName, mapEnum(account.type),
account.iban, account.subAccountNumber, account.productName, account.currency, account.accountLimit,
@ -111,19 +111,19 @@ open class SqliteBankingRepository(
val accountId = getLastInsertedId()
val accountTransactionEntities = account.bookedTransactions.map { transaction ->
persistTransaction(userId, accountId, transaction)
persistTransaction(bankId, accountId, transaction)
}
val holdings = account.holdings.map { holding -> persistHolding(userId, accountId, holding) }
val holdings = account.holdings.map { holding -> persistHolding(bankId, accountId, holding) }
return BankAccountEntity(accountId, userId, account, accountTransactionEntities, holdings)
return BankAccountEntity(accountId, bankId, account, accountTransactionEntities, holdings)
}
private fun getAllTanMethods(): List<TanMethodEntity> = userQueries.selectAllTanMethods { id, userId, displayName, type, identifier, maxTanInputLength, allowedTanFormat ->
private fun getAllTanMethods(): List<TanMethodEntity> = bankQueries.selectAllTanMethods { id, bankId, displayName, type, identifier, maxTanInputLength, allowedTanFormat ->
TanMethodEntity(
id,
userId,
bankId,
displayName,
mapToEnum(type, TanMethodType.entries),
@ -133,12 +133,12 @@ open class SqliteBankingRepository(
)
}.executeAsList()
private suspend fun persistTanMethods(userId: Long, tanMethods: List<TanMethod>): List<TanMethodEntity> =
tanMethods.map { persistTanMethod(userId, it) }
private suspend fun persistTanMethods(bankId: Long, tanMethods: List<TanMethod>): List<TanMethodEntity> =
tanMethods.map { persistTanMethod(bankId, it) }
private suspend fun persistTanMethod(userId: Long, tanMethod: TanMethod): TanMethodEntity {
userQueries.insertTanMethod(
userId,
private suspend fun persistTanMethod(bankId: Long, tanMethod: TanMethod): TanMethodEntity {
bankQueries.insertTanMethod(
bankId,
tanMethod.displayName,
mapEnum(tanMethod.type),
@ -149,11 +149,11 @@ open class SqliteBankingRepository(
val tanMethodId = getLastInsertedId()
return TanMethodEntity(tanMethodId, userId, tanMethod)
return TanMethodEntity(tanMethodId, bankId, tanMethod)
}
private fun getAllTanMedia(): List<TanMediumEntity> = userQueries.selectAllTanMedia { id, userId, type, mediumName, status, phoneNumber, concealedPhoneNumber, cardNumber, cardSequenceNumber, cardType, validFrom, validTo ->
private fun getAllTanMedia(): List<TanMediumEntity> = bankQueries.selectAllTanMedia { id, bankId, type, mediumName, status, phoneNumber, concealedPhoneNumber, cardNumber, cardSequenceNumber, cardType, validFrom, validTo ->
val mobilePhone = if (phoneNumber != null || concealedPhoneNumber != null) {
MobilePhoneTanMedium(phoneNumber, concealedPhoneNumber)
} else {
@ -168,7 +168,7 @@ open class SqliteBankingRepository(
TanMediumEntity(
id,
userId,
bankId,
mapToEnum(type, TanMediumType.entries),
mediumName,
@ -179,12 +179,12 @@ open class SqliteBankingRepository(
)
}.executeAsList()
private suspend fun persistTanMedia(userId: Long, tanMedia: List<TanMedium>): List<TanMediumEntity> =
tanMedia.map { persistTanMedium(userId, it) }
private suspend fun persistTanMedia(bankId: Long, tanMedia: List<TanMedium>): List<TanMediumEntity> =
tanMedia.map { persistTanMedium(bankId, it) }
private suspend fun persistTanMedium(userId: Long, medium: TanMedium): TanMediumEntity {
userQueries.insertTanMedium(
userId,
private suspend fun persistTanMedium(bankId: Long, medium: TanMedium): TanMediumEntity {
bankQueries.insertTanMedium(
bankId,
mapEnum(medium.type),
medium.mediumName,
@ -202,13 +202,13 @@ open class SqliteBankingRepository(
val tanMediumId = getLastInsertedId()
return TanMediumEntity(tanMediumId, userId, medium)
return TanMediumEntity(tanMediumId, bankId, medium)
}
protected open fun getAllHoldings(): List<HoldingEntity> =
accountTransactionQueries.selectAllHoldings { id, userId, bankAccountId, name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate ->
HoldingEntity(id, userId, bankAccountId, name, isin, wkn, mapToInt(quantity), currency, mapToAmount(totalBalance), mapToAmount(marketValue), performancePercentage?.toFloat(), mapToAmount(totalCostPrice), mapToAmount(averageCostPrice), mapToInstant(pricingTime), mapToDate(buyingDate))
accountTransactionQueries.selectAllHoldings { id, bankId, bankAccountId, name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate ->
HoldingEntity(id, bankId, bankAccountId, name, isin, wkn, mapToInt(quantity), currency, mapToAmount(totalBalance), mapToAmount(marketValue), performancePercentage?.toFloat(), mapToAmount(totalCostPrice), mapToAmount(averageCostPrice), mapToInstant(pricingTime), mapToDate(buyingDate))
}.executeAsList()
override suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity> =
@ -219,9 +219,9 @@ open class SqliteBankingRepository(
/**
* Has to be executed in a transaction in order that getting persisted Holding's id works~
*/
protected open suspend fun persistHolding(userId: Long, bankAccountId: Long, holding: Holding): HoldingEntity {
protected open suspend fun persistHolding(bankId: Long, bankAccountId: Long, holding: Holding): HoldingEntity {
accountTransactionQueries.insertHolding(
userId, bankAccountId,
bankId, bankAccountId,
holding.name, holding.isin, holding.wkn,
@ -234,7 +234,7 @@ open class SqliteBankingRepository(
mapInstant(holding.pricingTime), mapDate(holding.buyingDate)
)
return HoldingEntity(getLastInsertedId(), userId, bankAccountId, holding)
return HoldingEntity(getLastInsertedId(), bankId, bankAccountId, holding)
}
override suspend fun updateHoldings(holdings: List<HoldingEntity>) {
@ -266,16 +266,16 @@ open class SqliteBankingRepository(
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
accountTransactionQueries.selectAllTransactionsAsViewModel { id, userId, bankAccountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, category ->
AccountTransactionViewModel(id, userId, bankAccountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, category)
accountTransactionQueries.selectAllTransactionsAsViewModel { id, bankId, bankAccountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, category ->
AccountTransactionViewModel(id, bankId, bankAccountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, category)
}.executeAsList()
override fun getAllAccountTransactions(): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactions(::mapTransaction).executeAsList()
}
override fun getAllTransactionsOfUser(user: UserEntity): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactionsOfUser(user.id, ::mapTransaction).executeAsList()
override fun getAllTransactionsForBank(bank: BankAccessEntity): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactionsOfUser(bank.id, ::mapTransaction).executeAsList()
}
override fun getTransactionById(transactionId: Long): AccountTransactionEntity? =
@ -293,9 +293,9 @@ open class SqliteBankingRepository(
/**
* Has to be executed in a transaction in order that getting persisted AccountTransaction's id works~
*/
protected open suspend fun persistTransaction(userId: Long, bankAccountId: Long, transaction: AccountTransaction): AccountTransactionEntity {
protected open suspend fun persistTransaction(bankId: Long, bankAccountId: Long, transaction: AccountTransaction): AccountTransactionEntity {
accountTransactionQueries.insertTransaction(
userId, bankAccountId,
bankId, bankAccountId,
mapAmount(transaction.amount), transaction.currency, transaction.reference,
mapDate(transaction.bookingDate), mapDate(transaction.valueDate),
@ -326,16 +326,16 @@ open class SqliteBankingRepository(
transaction.isReversal
)
return AccountTransactionEntity(getLastInsertedId(), userId, bankAccountId, transaction)
return AccountTransactionEntity(getLastInsertedId(), bankId, bankAccountId, transaction)
}
private fun getLastInsertedId(): Long =
userQueries.getLastInsertedId().executeAsOne()
bankQueries.getLastInsertedId().executeAsOne()
private fun mapTransaction(
id: Long, userId: Long, bankAccountId: Long,
id: Long, bankId: Long, bankAccountId: Long,
amount: String, currency: String, reference: String?,
bookingDate: String, valueDate: String,
@ -366,7 +366,7 @@ open class SqliteBankingRepository(
isReversal: Boolean
): AccountTransactionEntity = AccountTransactionEntity(
id,
userId, bankAccountId,
bankId, bankAccountId,
Amount(amount), currency, reference,
mapToDate(bookingDate), mapToDate(valueDate),

View File

@ -1,11 +1,10 @@
package net.codinux.banking.dataaccess.entities
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.BankingGroup
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.tan.TanMedium
import net.codinux.banking.client.model.tan.TanMethod
class UserEntity(
class BankAccessEntity(
val id: Long,
bankCode: String,
@ -13,7 +12,7 @@ class UserEntity(
password: String?,
bankName: String,
bic: String,
bic: String?,
customerName: String,
userId: String? = null,
@ -34,7 +33,7 @@ class UserEntity(
iconUrl: String? = null,
wrongCredentialsEntered: Boolean = false
) : User(bankCode, loginName, password, bankName, bic, customerName, userId, accounts, selectedTanMethodIdentifier, tanMethods, selectedTanMediumIdentifier, tanMedia, bankingGroup, serverAddress) {
) : BankAccess(bankCode, loginName, password, bankName, bic, customerName, userId, accounts, selectedTanMethodIdentifier, tanMethods, selectedTanMediumIdentifier, tanMedia, bankingGroup, serverAddress) {
init {
this.userSetDisplayName = userSetDisplayName
@ -45,14 +44,14 @@ class UserEntity(
}
constructor(id: Long, user: User, bankAccounts: List<BankAccountEntity>, tanMethods: List<TanMethodEntity>, tanMedia: List<TanMediumEntity>) : this(
constructor(id: Long, bank: BankAccess, bankAccounts: List<BankAccountEntity>, tanMethods: List<TanMethodEntity>, tanMedia: List<TanMediumEntity>) : this(
id,
user.bankCode, user.loginName, user.password, user.bankName, user.bic, user.customerName, user.userId,
bank.bankCode, bank.loginName, bank.password, bank.bankName, bank.bic, bank.customerName, bank.userId,
bankAccounts,
user.selectedTanMethodIdentifier, tanMethods, user.selectedTanMediumIdentifier, tanMedia,
user.bankingGroup, user.serverAddress,
user.userSetDisplayName, user.displayIndex,
user.iconUrl, user.wrongCredentialsEntered,
bank.selectedTanMethodIdentifier, tanMethods, bank.selectedTanMediumIdentifier, tanMedia,
bank.bankingGroup, bank.serverAddress,
bank.userSetDisplayName, bank.displayIndex,
bank.iconUrl, bank.wrongCredentialsEntered,
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
@ -37,7 +37,7 @@ fun NavigationMenuItem(
iconSize: Dp = 24.dp,
iconTextSpacing: Dp = 24.dp,
horizontalPadding: Dp = 8.dp,
user: UserEntity? = null,
bank: BankAccessEntity? = null,
bankAccount: BankAccountEntity? = null,
fallbackIcon: ImageVector? = null,
icon: (@Composable () -> Unit)? = null,
@ -54,7 +54,7 @@ fun NavigationMenuItem(
modifier = modifier
.clickable { onClick?.invoke() }
.let {
if (user != null && filterService.isSelected(user, transactionsFilter)
if (bank != null && filterService.isSelected(bank, transactionsFilter)
|| bankAccount != null && filterService.isSelected(bankAccount, transactionsFilter)) {
it.background(Colors.AccentAsSelectionBackground, shape = RoundedCornerShape(4.dp))
} else {
@ -68,7 +68,7 @@ fun NavigationMenuItem(
icon()
}
} else {
BankIcon(user, Modifier.padding(end = iconTextSpacing), Modifier.size(iconSize), fallbackIcon = fallbackIcon, fallbackIconTintColor = textColor)
BankIcon(bank, Modifier.padding(end = iconTextSpacing), Modifier.size(iconSize), fallbackIcon = fallbackIcon, fallbackIconTintColor = textColor)
}
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 (user != null) {
calculator.calculateBalanceOfUser(user)
} else if (bank != null) {
calculator.calculateBalanceOfBankAccess(bank)
} else {
null
}

View File

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

View File

@ -14,7 +14,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.forms.RoundedCornersCard
@ -31,7 +31,7 @@ fun GroupedTransactionsListItems(
modifier: Modifier,
transactionsToDisplay: List<AccountTransactionViewModel>,
holdingsToDisplay: List<Holding>,
usersById: Map<Long, UserEntity>,
banksById: Map<Long, BankAccessEntity>,
transactionsGrouping: TransactionsGrouping
) {
val groupingService = remember { TransactionsGroupingService() }
@ -90,7 +90,7 @@ fun GroupedTransactionsListItems(
Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed
monthTransactions.forEachIndexed { index, transaction ->
key(transaction.id) {
TransactionListItem(usersById[transaction.userId], transaction, index, monthTransactions.size)
TransactionListItem(banksById[transaction.userId], transaction, index, monthTransactions.size)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ package net.codinux.banking.ui.service
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.HoldingEntity
import net.codinux.banking.dataaccess.entities.UserEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.AccountTransactionsFilter
import net.codinux.banking.ui.model.BankAccountFilter
@ -83,14 +83,14 @@ class AccountTransactionsFilterService {
|| holding.wkn?.contains(searchTerm, true) == true
fun isSelected(user: UserEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
fun isSelected(user: BankAccessEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
if (transactionsFilter.showAllAccounts) {
return false
}
val filter = transactionsFilter.selectedAccount
return filter?.user == user && filter.bankAccount == null
return filter?.bank == user && filter.bankAccount == null
}
fun isSelected(bankAccount: BankAccountEntity, transactionsFilter: AccountTransactionsFilter): Boolean {

View File

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

View File

@ -45,21 +45,21 @@ class BankingService(
suspend fun init() {
try {
uiState.users.value = getAllUsers()
uiState.banks.value = getAllBanks()
uiState.transactions.value = getAllAccountTransactionsAsViewModel()
uiState.holdings.value = uiState.users.value.flatMap { it.accounts }.flatMap { it.holdings }
uiState.holdings.value = uiState.banks.value.flatMap { it.accounts }.flatMap { it.holdings }
} catch (e: Throwable) {
log.error(e) { "Could not read all user accounts and account transactions from repository" }
log.error(e) { "Could not read all banks and account transactions from repository" }
}
}
fun getAllUsers() = bankingRepository.getAllUsers()
fun getAllBanks() = bankingRepository.getAllBanks()
fun getAllAccountTransactions() = bankingRepository.getAllAccountTransactions()
fun getAllTransactionsOfUser(user: UserEntity) = bankingRepository.getAllTransactionsOfUser(user)
fun getAllTransactionsForBank(bank: BankAccessEntity) = bankingRepository.getAllTransactionsForBank(bank)
fun getAllAccountTransactionsAsViewModel() = bankingRepository.getAllAccountTransactionsAsViewModel()
@ -101,21 +101,21 @@ class BankingService(
private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) {
try {
val newUser = response.user
newUser.displayIndex = uiState.users.value.size
val newBank = response.bank
newBank.displayIndex = uiState.banks.value.size
val newUserEntity = bankingRepository.persistUser(newUser)
val newBankEntity = bankingRepository.persistBank(newBank)
log.info { "Saved user account $newUserEntity with ${newUserEntity.accounts.flatMap { it.bookedTransactionsEntities }.size} transactions" }
log.info { "Saved bank $newBankEntity with ${newBankEntity.accounts.flatMap { it.bookedTransactionsEntities }.size} transactions" }
val users = uiState.users.value.toMutableList()
users.add(newUserEntity)
uiState.users.value = users
val banks = uiState.banks.value.toMutableList()
banks.add(newBankEntity)
uiState.banks.value = banks
updateTransactionsInUi(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities })
updateHoldingsInUi(newUserEntity.accounts.flatMap { it.holdings }, emptyList())
updateTransactionsInUi(newBankEntity.accounts.flatMap { it.bookedTransactionsEntities })
updateHoldingsInUi(newBankEntity.accounts.flatMap { it.holdings }, emptyList())
} catch (e: Throwable) {
log.error(e) { "Could not save user account ${response.user}" }
log.error(e) { "Could not save bank ${response.bank}" }
}
}
@ -123,45 +123,45 @@ class BankingService(
suspend fun updateAccountTransactions() {
val selectedAccount = uiState.transactionsFilter.value.selectedAccount
if (selectedAccount != null) {
updateAccountTransactions(selectedAccount.user, selectedAccount.bankAccount)
updateAccountTransactions(selectedAccount.bank, selectedAccount.bankAccount)
} else {
uiState.users.value.forEach { user ->
updateAccountTransactions(user)
uiState.banks.value.forEach { bank ->
updateAccountTransactions(bank)
}
}
}
private suspend fun updateAccountTransactions(user: UserEntity, bankAccount: BankAccountEntity? = null) {
private suspend fun updateAccountTransactions(bank: BankAccessEntity, bankAccount: BankAccountEntity? = null) {
withContext(Dispatchers.IOorDefault) {
try {
val response = client.updateAccountTransactionsAsync(user, bankAccount?.let { listOf(it) })
val response = client.updateAccountTransactionsAsync(bank, bankAccount?.let { listOf(it) })
if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulUpdateAccountTransactionsResponse(user, response.data!!)
handleSuccessfulUpdateAccountTransactionsResponse(bank, response.data!!)
} else {
handleUnsuccessfulBankingClientResponse(BankingClientAction.UpdateAccountTransactions, response)
}
} catch (e: Throwable) {
log.error(e) { "Could not update account transactions for $user" }
log.error(e) { "Could not update account transactions for $bank" }
}
}
}
private suspend fun handleSuccessfulUpdateAccountTransactionsResponse(user: UserEntity, responses: List<GetTransactionsResponse>) {
private suspend fun handleSuccessfulUpdateAccountTransactionsResponse(bank: BankAccessEntity, responses: List<GetTransactionsResponse>) {
try {
// TODO: when user gets updated by BankingClient, also update user in database
// val newUser = response.user
// TODO: when bank gets updated by BankingClient, also update bank in database
// val newUser = response.bank
// val newUserEntity = bankingRepository.persistUser(newUser)
//
// log.info { "Saved user account $newUserEntity with ${newUserEntity.accounts.flatMap { it.bookedTransactionsEntities }.size} transactions" }
val userTransactions = getAllTransactionsOfUser(user)
val transactionsForBank = getAllTransactionsForBank(bank)
responses.forEach { response ->
val account = (response.account as? BankAccountEntity) ?: user.accounts.first { it.identifier == response.account.identifier && it.subAccountNumber == response.account.subAccountNumber }
val account = (response.account as? BankAccountEntity) ?: bank.accounts.first { it.identifier == response.account.identifier && it.subAccountNumber == response.account.subAccountNumber }
// TODO: update BankAccount and may updated Transactions in database
val existingAccountTransactions = userTransactions.filter { it.bankAccountId == account.id }
val existingAccountTransactions = transactionsForBank.filter { it.bankAccountId == account.id }
val newTransactions = modelService.findNewTransactions(response.bookedTransactions, existingAccountTransactions)
@ -189,10 +189,10 @@ class BankingService(
updateHoldingsInUi(persistedNewHoldings, deletedHoldings)
val transactionsViewModel = updateTransactionsInUi(newTransactionsEntities)
uiState.dispatchNewTransactionsRetrieved(AccountTransactionsRetrievedEvent(user, account, transactionsViewModel, response.holdings))
uiState.dispatchNewTransactionsRetrieved(AccountTransactionsRetrievedEvent(bank, account, transactionsViewModel, response.holdings))
}
} catch (e: Throwable) {
log.error(e) { "Could not save updated account transactions for user $user" }
log.error(e) { "Could not save updated account transactions for bank $bank" }
}
}
@ -235,11 +235,11 @@ class BankingService(
}
suspend fun transferMoney(user: UserEntity, account: BankAccountEntity,
suspend fun transferMoney(bank: BankAccessEntity, account: BankAccountEntity,
recipientName: String, recipientAccountIdentifier: String, amount: Amount, currency: String,
paymentReference: String? = null, instantTransfer: Boolean = false, recipientBankIdentifier: String? = null): Boolean {
val response = client.transferMoneyAsync(TransferMoneyRequestForUser(
user.bankCode, user.loginName, user.password!!,
bank.bankCode, bank.loginName, bank.password!!,
BankAccountIdentifier(account.identifier, account.subAccountNumber, account.iban), // TODO: use BankingClient's one
recipientName, recipientAccountIdentifier, recipientBankIdentifier,
amount, "EUR",
@ -251,7 +251,7 @@ class BankingService(
} else if (response.type == ResponseType.Success) {
uiState.dispatchTransferredMoney(TransferredMoneyEvent(recipientName, amount, currency))
updateAccountTransactions(user, account)
updateAccountTransactions(bank, account)
}
return response.type == ResponseType.Success

View File

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

View File

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

View File

@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS User (
password TEXT,
bankName TEXT NOT NULL,
bic TEXT NOT NULL,
bic TEXT,
customerName TEXT NOT NULL,
userId TEXT,
@ -31,7 +31,7 @@ CREATE TABLE IF NOT EXISTS User (
);
insertUser:
insertBank:
INSERT INTO User(
bankCode, loginName, password,
@ -72,7 +72,7 @@ VALUES(
);
selectAllUsers:
selectAllBanks:
SELECT User.*
FROM User;

View File

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

View File

@ -27,29 +27,29 @@ class SqliteBankingRepositoryTest {
val bankAccounts = listOf(
BankAccount("12345", null, null, null, "Monika Tester", BankAccountType.CheckingAccount, balance = Amount("12.34"), retrievedTransactionsFrom = LocalDate(2024, 5, 7), features = setOf(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.InstantTransfer), serverTransactionsRetentionDays = 320)
)
val user = User("12345678", "SupiDupiUser", "geheim", "Abzock-Bank", "ABCDDEBBXXX", "Monika Tester", accounts = bankAccounts, bankingGroup = BankingGroup.DKB, serverAddress = "").apply {
val bank = BankAccess("12345678", "SupiDupiUser", "geheim", "Abzock-Bank", "ABCDDEBBXXX", "Monika Tester", accounts = bankAccounts, bankingGroup = BankingGroup.DKB, serverAddress = "").apply {
wrongCredentialsEntered = true
displayIndex = 99
}
val persisted = underTest.persistUser(user)
val persisted = underTest.persistBank(bank)
assertNotNull(persisted.id)
assertEquals(user.bankCode, persisted.bankCode)
assertEquals(user.loginName, persisted.loginName)
assertEquals(user.password, persisted.password)
assertEquals(bank.domesticBankCode, persisted.domesticBankCode)
assertEquals(bank.loginName, persisted.loginName)
assertEquals(bank.password, persisted.password)
assertEquals(user.bankName, persisted.bankName)
assertEquals(user.bic, persisted.bic)
assertEquals(bank.bankName, persisted.bankName)
assertEquals(bank.bic, persisted.bic)
assertEquals(user.customerName, persisted.customerName)
assertEquals(user.userId, persisted.userId)
assertEquals(bank.customerName, persisted.customerName)
assertEquals(bank.userId, persisted.userId)
assertEquals(user.bankingGroup, persisted.bankingGroup)
assertEquals(bank.bankingGroup, persisted.bankingGroup)
assertEquals(user.wrongCredentialsEntered, persisted.wrongCredentialsEntered)
assertEquals(user.displayIndex, persisted.displayIndex)
assertEquals(bank.wrongCredentialsEntered, persisted.wrongCredentialsEntered)
assertEquals(bank.displayIndex, persisted.displayIndex)
assertEquals(1, persisted.accounts.size)