Extracted AccountTransactionViewModel that only the fetches the properties from database that are really displayed in UI

This commit is contained in:
dankito 2024-08-28 00:41:16 +02:00
parent e8e304f574
commit d356d45db7
10 changed files with 100 additions and 24 deletions

View File

@ -4,6 +4,7 @@ import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.UserAccount import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
interface BankingRepository { interface BankingRepository {
@ -12,8 +13,10 @@ interface BankingRepository {
suspend fun persistUserAccount(userAccount: UserAccount): Long suspend fun persistUserAccount(userAccount: UserAccount): Long
fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel>
fun getAllAccountTransactions(): List<AccountTransactionEntity> fun getAllAccountTransactions(): List<AccountTransactionEntity>
suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>) suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>): Map<Long, AccountTransaction>
} }

View File

@ -4,6 +4,7 @@ import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.UserAccount import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
class InMemoryBankingRepository( class InMemoryBankingRepository(
userAccounts: Collection<UserAccount> = emptyList(), userAccounts: Collection<UserAccount> = emptyList(),
@ -26,10 +27,17 @@ class InMemoryBankingRepository(
} }
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
transactions.map { AccountTransactionViewModel(it.id, it) }
override fun getAllAccountTransactions(): List<AccountTransactionEntity> = transactions.toList() override fun getAllAccountTransactions(): List<AccountTransactionEntity> = transactions.toList()
override suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>) { override suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>): Map<Long, AccountTransaction> {
this.transactions.addAll(transactions.map { map(it) }) val entities = transactions.map { map(it) }
this.transactions.addAll(entities)
return entities.associateBy { it.id }
} }

View File

@ -6,6 +6,7 @@ import net.codinux.banking.client.model.*
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import kotlin.enums.EnumEntries import kotlin.enums.EnumEntries
import kotlin.js.JsName import kotlin.js.JsName
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -62,9 +63,12 @@ class SqliteBankingRepository(
) )
}.executeAsList() }.executeAsList()
suspend fun persistBankAccounts(userAccountId: Long, bankAccounts: Collection<BankAccount>): Map<Long, BankAccount> = private suspend fun persistBankAccounts(userAccountId: Long, bankAccounts: Collection<BankAccount>): Map<Long, BankAccount> =
bankAccounts.associate { persistBankAccount(userAccountId, it) } bankAccounts.associate { persistBankAccount(userAccountId, 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): Pair<Long, BankAccount> { private suspend fun persistBankAccount(userAccountId: Long, account: BankAccount): Pair<Long, BankAccount> {
userAccountQueries.insertBankAccount( userAccountQueries.insertBankAccount(
userAccountId, userAccountId,
@ -86,6 +90,11 @@ class SqliteBankingRepository(
} }
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
accountTransactionQueries.selectAllTransactionsAsViewModel { id, amount, currency, reference, valueDate, otherPartyName, bookingText, sepaReference, userSetDisplayName, category ->
AccountTransactionViewModel(id, mapToAmount(amount), currency, sepaReference ?: reference, mapToDate(valueDate), otherPartyName, bookingText, userSetDisplayName, category)
}.executeAsList()
override fun getAllAccountTransactions(): List<AccountTransactionEntity> { override fun getAllAccountTransactions(): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactions { id, amount, currency, reference, bookingDate, valueDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, 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 -> return accountTransactionQueries.selectAllTransactions { id, amount, currency, reference, bookingDate, valueDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, 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 ->
AccountTransactionEntity( AccountTransactionEntity(
@ -119,13 +128,17 @@ class SqliteBankingRepository(
}.executeAsList() }.executeAsList()
} }
override suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>) { override suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>): Map<Long, AccountTransaction> =
transactions.forEach { accountTransactionQueries.transactionWithResult {
saveAccountTransaction(it) transactions.associate {
saveAccountTransaction(it)
}
} }
}
private suspend fun saveAccountTransaction(transaction: AccountTransaction) { /**
* Has to be executed in a transaction in order that getting persisted BankAccount's id works~
*/
private suspend fun saveAccountTransaction(transaction: AccountTransaction): Pair<Long, AccountTransaction> {
accountTransactionQueries.insertTransaction( accountTransactionQueries.insertTransaction(
mapAmount(transaction.amount), transaction.currency, transaction.reference, mapAmount(transaction.amount), transaction.currency, transaction.reference,
mapDate(transaction.bookingDate), mapDate(transaction.valueDate), mapDate(transaction.bookingDate), mapDate(transaction.valueDate),
@ -151,6 +164,8 @@ class SqliteBankingRepository(
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber transaction.transactionReferenceNumber, transaction.relatedReferenceNumber
) )
return Pair(getLastInsertedId(), transaction)
} }

View File

@ -9,13 +9,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.AccountTransactionViewModel
private val formatUtil = DI.formatUtil private val formatUtil = DI.formatUtil
@Composable @Composable
fun TransactionListItem(transaction: AccountTransaction, backgroundColor: Color) { fun TransactionListItem(transaction: AccountTransactionViewModel, backgroundColor: Color) {
Row( Row(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.background(color = backgroundColor) .background(color = backgroundColor)

View File

@ -63,7 +63,7 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
Row { Row {
Text("${challenge.customer.bankName}, Nutzer ${challenge.customer.loginName}${challenge.account?.let { ", Konto ${it.productName ?: it.identifier}" } ?: ""}") Text("${challenge.user.bankName}, Nutzer ${challenge.user.loginName}${challenge.account?.let { ", Konto ${it.productName ?: it.identifier}" } ?: ""}")
} }
Text( Text(
"TAN benötigt ${Internationalization.getTextForActionRequiringTan(challenge.forAction)}", "TAN benötigt ${Internationalization.getTextForActionRequiringTan(challenge.forAction)}",

View File

@ -0,0 +1,22 @@
package net.codinux.banking.ui.model
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
data class AccountTransactionViewModel(
val id: Long,
val amount: Amount,
val currency: String,
val reference: String,
val valueDate: LocalDate,
val otherPartyName: String? = null,
val bookingText: String? = null,
val userSetDisplayName: String? = null,
val category: String? = null
) {
constructor(id: Long, transaction: AccountTransaction)
: this(id, transaction.amount, transaction.currency, transaction.reference, transaction.valueDate, transaction.otherPartyName, transaction.bookingText)
}

View File

@ -14,6 +14,7 @@ import net.codinux.banking.dataaccess.BankingRepository
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity
import net.codinux.banking.fints.config.FinTsClientConfiguration import net.codinux.banking.fints.config.FinTsClientConfiguration
import net.codinux.banking.fints.config.FinTsClientOptions import net.codinux.banking.fints.config.FinTsClientOptions
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.BankInfo import net.codinux.banking.ui.model.BankInfo
import net.codinux.banking.ui.model.TanChallengeReceived import net.codinux.banking.ui.model.TanChallengeReceived
import net.codinux.banking.ui.model.error.BankingClientAction import net.codinux.banking.ui.model.error.BankingClientAction
@ -42,7 +43,7 @@ class BankingService(
try { try {
uiState.userAccounts.value = bankingRepository.getAllUserAccounts() uiState.userAccounts.value = bankingRepository.getAllUserAccounts()
uiState.transactions.value = bankingRepository.getAllAccountTransactions() uiState.transactions.value = bankingRepository.getAllAccountTransactionsAsViewModel()
} catch (e: Throwable) { } catch (e: Throwable) {
log.error(e) { "Could not read all user accounts and account transactions from repository" } log.error(e) { "Could not read all user accounts and account transactions from repository" }
} }
@ -72,25 +73,27 @@ class BankingService(
} }
private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) { private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) {
val transactions = uiState.transactions.value.toMutableList()
transactions.addAll(response.bookedTransactions)
uiState.transactions.value = transactions.sortedByDescending { it.valueDate }
try { try {
bankingRepository.persistAccountTransactions(response.bookedTransactions) val newTransactions = response.bookedTransactions
val createdIds = bankingRepository.persistAccountTransactions(newTransactions)
log.info { "Saved ${response.bookedTransactions.size} transactions" } log.info { "Saved ${newTransactions.size} transactions" }
val transactions = uiState.transactions.value.toMutableList()
transactions.addAll(createdIds.map { AccountTransactionViewModel(it.key, it.value) })
uiState.transactions.value = transactions.sortedByDescending { it.valueDate }
} catch (e: Throwable) { } catch (e: Throwable) {
log.error(e) { "Could not save account transactions ${response.bookedTransactions}" } log.error(e) { "Could not save account transactions ${response.bookedTransactions}" }
} }
try { try {
val newUserAccountId = bankingRepository.persistUserAccount(response.user) val newUser = response.user
val newUserAccountId = bankingRepository.persistUserAccount(newUser)
log.info { "Saved user account ${response.user}" } log.info { "Saved user account $newUser" }
val userAccounts = uiState.userAccounts.value.toMutableList() val userAccounts = uiState.userAccounts.value.toMutableList()
userAccounts.add(UserAccountEntity(newUserAccountId, response.user)) userAccounts.add(UserAccountEntity(newUserAccountId, newUser))
uiState.userAccounts.value = userAccounts uiState.userAccounts.value = userAccounts
} catch (e: Throwable) { } catch (e: Throwable) {
log.error(e) { "Could not save user account ${response.user}" } log.error(e) { "Could not save user account ${response.user}" }

View File

@ -2,8 +2,8 @@ package net.codinux.banking.ui.state
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.TanChallengeReceived import net.codinux.banking.ui.model.TanChallengeReceived
import net.codinux.banking.ui.model.error.ApplicationError import net.codinux.banking.ui.model.error.ApplicationError
import net.codinux.banking.ui.model.error.BankingClientError import net.codinux.banking.ui.model.error.BankingClientError
@ -13,7 +13,7 @@ class UiState : ViewModel() {
val userAccounts = MutableStateFlow<List<UserAccountEntity>>(emptyList()) val userAccounts = MutableStateFlow<List<UserAccountEntity>>(emptyList())
val transactions = MutableStateFlow<List<AccountTransaction>>(emptyList()) val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())
val applicationErrorOccurred = MutableStateFlow<ApplicationError?>(null) val applicationErrorOccurred = MutableStateFlow<ApplicationError?>(null)

View File

@ -108,4 +108,8 @@ VALUES(
selectAllTransactions: selectAllTransactions:
SELECT AccountTransaction.* SELECT AccountTransaction.*
FROM AccountTransaction;
selectAllTransactionsAsViewModel:
SELECT id, amount, currency, reference, valueDate, otherPartyName, bookingText, sepaReference, userSetDisplayName, category
FROM AccountTransaction; FROM AccountTransaction;

View File

@ -73,6 +73,7 @@ class SqliteBankingRepositoryTest {
assertEquals(bankAccounts.first().includeInAutomaticAccountsUpdate, persistedBankAccount.includeInAutomaticAccountsUpdate) assertEquals(bankAccounts.first().includeInAutomaticAccountsUpdate, persistedBankAccount.includeInAutomaticAccountsUpdate)
} }
@Test @Test
fun saveTransaction() = runTest { fun saveTransaction() = runTest {
val transaction = AccountTransaction(Amount("12.45"), "EUR", "Lohn", LocalDate(2024, 5, 7), LocalDate(2024, 6, 15), "Dein Boss") val transaction = AccountTransaction(Amount("12.45"), "EUR", "Lohn", LocalDate(2024, 5, 7), LocalDate(2024, 6, 15), "Dein Boss")
@ -94,4 +95,24 @@ class SqliteBankingRepositoryTest {
assertEquals(transaction.otherPartyName, persisted.otherPartyName) assertEquals(transaction.otherPartyName, persisted.otherPartyName)
} }
@Test
fun saveTransaction_GetAsViewModel() = runTest {
val transaction = AccountTransaction(Amount("12.45"), "EUR", "Lohn", LocalDate(2024, 5, 7), LocalDate(2024, 6, 15), "Dein Boss")
underTest.persistAccountTransactions(listOf(transaction))
val result = underTest.getAllAccountTransactionsAsViewModel()
assertEquals(1, result.size)
val persisted = result.first()
assertNotNull(persisted.id)
assertEquals(transaction.amount, persisted.amount)
assertEquals(transaction.currency, persisted.currency)
assertEquals(transaction.reference, persisted.reference)
assertEquals(transaction.valueDate, persisted.valueDate)
assertEquals(transaction.otherPartyName, persisted.otherPartyName)
}
} }