From d356d45db78ad184afb26e9253ea785c3bf6fbec Mon Sep 17 00:00:00 2001 From: dankito Date: Wed, 28 Aug 2024 00:41:16 +0200 Subject: [PATCH] Extracted AccountTransactionViewModel that only the fetches the properties from database that are really displayed in UI --- .../banking/dataaccess/BankingRepository.kt | 5 +++- .../dataaccess/InMemoryBankingRepository.kt | 12 +++++++-- .../dataaccess/SqliteBankingRepository.kt | 27 ++++++++++++++----- .../ui/composables/TransactionListItem.kt | 4 +-- .../banking/ui/dialogs/EnterTanDialog.kt | 2 +- .../ui/model/AccountTransactionViewModel.kt | 22 +++++++++++++++ .../banking/ui/service/BankingService.kt | 23 +++++++++------- .../net/codinux/banking/ui/state/UiState.kt | 4 +-- .../codinux/banking/ui/AccountTransaction.sq | 4 +++ .../dataaccess/SqliteBankingRepositoryTest.kt | 21 +++++++++++++++ 10 files changed, 100 insertions(+), 24 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/BankingRepository.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/BankingRepository.kt index 28a6fc8..c1c78f7 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/BankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/BankingRepository.kt @@ -4,6 +4,7 @@ import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.UserAccount import net.codinux.banking.dataaccess.entities.AccountTransactionEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity +import net.codinux.banking.ui.model.AccountTransactionViewModel interface BankingRepository { @@ -12,8 +13,10 @@ interface BankingRepository { suspend fun persistUserAccount(userAccount: UserAccount): Long + fun getAllAccountTransactionsAsViewModel(): List + fun getAllAccountTransactions(): List - suspend fun persistAccountTransactions(transactions: Collection) + suspend fun persistAccountTransactions(transactions: Collection): Map } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/InMemoryBankingRepository.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/InMemoryBankingRepository.kt index f1d0dad..ae9c572 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/InMemoryBankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/InMemoryBankingRepository.kt @@ -4,6 +4,7 @@ import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.UserAccount import net.codinux.banking.dataaccess.entities.AccountTransactionEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity +import net.codinux.banking.ui.model.AccountTransactionViewModel class InMemoryBankingRepository( userAccounts: Collection = emptyList(), @@ -26,10 +27,17 @@ class InMemoryBankingRepository( } + override fun getAllAccountTransactionsAsViewModel(): List = + transactions.map { AccountTransactionViewModel(it.id, it) } + override fun getAllAccountTransactions(): List = transactions.toList() - override suspend fun persistAccountTransactions(transactions: Collection) { - this.transactions.addAll(transactions.map { map(it) }) + override suspend fun persistAccountTransactions(transactions: Collection): Map { + val entities = transactions.map { map(it) } + + this.transactions.addAll(entities) + + return entities.associateBy { it.id } } diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt index a9c155b..c61afd1 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt @@ -6,6 +6,7 @@ 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.ui.model.AccountTransactionViewModel import kotlin.enums.EnumEntries import kotlin.js.JsName import kotlin.jvm.JvmName @@ -62,9 +63,12 @@ class SqliteBankingRepository( ) }.executeAsList() - suspend fun persistBankAccounts(userAccountId: Long, bankAccounts: Collection): Map = + private suspend fun persistBankAccounts(userAccountId: Long, bankAccounts: Collection): Map = 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 { userAccountQueries.insertBankAccount( userAccountId, @@ -86,6 +90,11 @@ class SqliteBankingRepository( } + override fun getAllAccountTransactionsAsViewModel(): List = + 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 { 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( @@ -119,13 +128,17 @@ class SqliteBankingRepository( }.executeAsList() } - override suspend fun persistAccountTransactions(transactions: Collection) { - transactions.forEach { - saveAccountTransaction(it) + override suspend fun persistAccountTransactions(transactions: Collection): Map = + accountTransactionQueries.transactionWithResult { + 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 { accountTransactionQueries.insertTransaction( mapAmount(transaction.amount), transaction.currency, transaction.reference, mapDate(transaction.bookingDate), mapDate(transaction.valueDate), @@ -151,6 +164,8 @@ class SqliteBankingRepository( transaction.transactionReferenceNumber, transaction.relatedReferenceNumber ) + + return Pair(getLastInsertedId(), transaction) } diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionListItem.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionListItem.kt index 5513d64..554d9b7 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionListItem.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionListItem.kt @@ -9,13 +9,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow 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.model.AccountTransactionViewModel private val formatUtil = DI.formatUtil @Composable -fun TransactionListItem(transaction: AccountTransaction, backgroundColor: Color) { +fun TransactionListItem(transaction: AccountTransactionViewModel, backgroundColor: Color) { Row( modifier = Modifier.fillMaxWidth() .background(color = backgroundColor) diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/EnterTanDialog.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/EnterTanDialog.kt index a2aa6f5..5957fb2 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/EnterTanDialog.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/EnterTanDialog.kt @@ -63,7 +63,7 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () -> Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) { 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( "TAN benötigt ${Internationalization.getTextForActionRequiringTan(challenge.forAction)}", diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionViewModel.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionViewModel.kt new file mode 100644 index 0000000..3da5dfa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionViewModel.kt @@ -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) +} diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt index 5452eca..f87a42f 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt @@ -14,6 +14,7 @@ import net.codinux.banking.dataaccess.BankingRepository import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.fints.config.FinTsClientConfiguration 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.TanChallengeReceived import net.codinux.banking.ui.model.error.BankingClientAction @@ -42,7 +43,7 @@ class BankingService( try { uiState.userAccounts.value = bankingRepository.getAllUserAccounts() - uiState.transactions.value = bankingRepository.getAllAccountTransactions() + uiState.transactions.value = bankingRepository.getAllAccountTransactionsAsViewModel() } catch (e: Throwable) { 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) { - val transactions = uiState.transactions.value.toMutableList() - transactions.addAll(response.bookedTransactions) - uiState.transactions.value = transactions.sortedByDescending { it.valueDate } - 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) { log.error(e) { "Could not save account transactions ${response.bookedTransactions}" } } 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() - userAccounts.add(UserAccountEntity(newUserAccountId, response.user)) + userAccounts.add(UserAccountEntity(newUserAccountId, newUser)) uiState.userAccounts.value = userAccounts } catch (e: Throwable) { log.error(e) { "Could not save user account ${response.user}" } diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt index 6e5dc8c..84e0c22 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt @@ -2,8 +2,8 @@ package net.codinux.banking.ui.state import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow -import net.codinux.banking.client.model.AccountTransaction 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.error.ApplicationError import net.codinux.banking.ui.model.error.BankingClientError @@ -13,7 +13,7 @@ class UiState : ViewModel() { val userAccounts = MutableStateFlow>(emptyList()) - val transactions = MutableStateFlow>(emptyList()) + val transactions = MutableStateFlow>(emptyList()) val applicationErrorOccurred = MutableStateFlow(null) diff --git a/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/AccountTransaction.sq b/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/AccountTransaction.sq index 68eb0b6..4c56f4b 100644 --- a/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/AccountTransaction.sq +++ b/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/AccountTransaction.sq @@ -108,4 +108,8 @@ VALUES( selectAllTransactions: SELECT AccountTransaction.* +FROM AccountTransaction; + +selectAllTransactionsAsViewModel: +SELECT id, amount, currency, reference, valueDate, otherPartyName, bookingText, sepaReference, userSetDisplayName, category FROM AccountTransaction; \ No newline at end of file diff --git a/composeApp/src/desktopTest/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepositoryTest.kt b/composeApp/src/desktopTest/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepositoryTest.kt index 874a398..0b9ae94 100644 --- a/composeApp/src/desktopTest/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepositoryTest.kt +++ b/composeApp/src/desktopTest/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepositoryTest.kt @@ -73,6 +73,7 @@ class SqliteBankingRepositoryTest { assertEquals(bankAccounts.first().includeInAutomaticAccountsUpdate, persistedBankAccount.includeInAutomaticAccountsUpdate) } + @Test fun saveTransaction() = runTest { 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) } + @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) + } + } \ No newline at end of file