From 55aad5242a703d894b8e8bf0d3a72a33ba99661c Mon Sep 17 00:00:00 2001 From: dankito Date: Tue, 27 Aug 2024 22:12:25 +0200 Subject: [PATCH] Implemented saving BankAccounts to dbCommented functionality --- composeApp/build.gradle.kts | 2 +- .../dataaccess/SqliteBankingRepository.kt | 123 ++++++++++++++++-- .../entities/AccountTransactionEntity.kt | 1 + .../dataaccess/entities/BankAccountEntity.kt | 42 ++++++ .../dataaccess/entities/UserAccountEntity.kt | 6 +- .../net/codinux/banking/ui/UserAccount.sq | 122 +++++++++++++---- .../dataaccess/SqliteBankingRepositoryTest.kt | 30 ++++- 7 files changed, 282 insertions(+), 44 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/BankAccountEntity.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 1f49d7a..a708434 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -121,7 +121,7 @@ sqldelight { databases { create("BankmeisterDb") { packageName.set("net.codinux.banking.dataaccess") - generateAsync.set(true) + generateAsync = true } } } 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 c8d7bc2..5dcd64d 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt @@ -2,12 +2,13 @@ package net.codinux.banking.dataaccess import app.cash.sqldelight.db.SqlDriver import kotlinx.datetime.LocalDate -import net.codinux.banking.client.model.AccountTransaction -import net.codinux.banking.client.model.Amount -import net.codinux.banking.client.model.BankingGroup -import net.codinux.banking.client.model.CustomerAccount +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 kotlin.enums.EnumEntries +import kotlin.js.JsName +import kotlin.jvm.JvmName class SqliteBankingRepository( sqlDriver: SqlDriver @@ -21,18 +22,67 @@ class SqliteBankingRepository( override fun getAllUserAccounts(): List { + val bankAccounts = getAllBankAccounts().groupBy { it.userAccountId } return userAccountQueries.selectAllUserAccounts { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodId, selectedTanMediumName, bankingGroup, iconUrl, wrongCredentialsEntered, userSetDisplayName, displayIndex -> - UserAccountEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, emptyList(), selectedTanMethodId, emptyList(), selectedTanMediumName, emptyList(), + UserAccountEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, bankAccounts[id] ?: emptyList(), selectedTanMethodId, emptyList(), selectedTanMediumName, emptyList(), bankingGroup?.let { BankingGroup.valueOf(it) }, iconUrl, wrongCredentialsEntered, userSetDisplayName, displayIndex.toInt()) }.executeAsList() } override suspend fun persistUserAccount(userAccount: CustomerAccount): Long { - return userAccountQueries.insertUserAccount(userAccount.bankCode, userAccount.loginName, userAccount.password, userAccount.bankName, userAccount.bic, - userAccount.customerName, userAccount.userId, userAccount.selectedTanMethodId, userAccount.selectedTanMediumName, - userAccount.bankingGroup?.name, userAccount.iconUrl, userAccount.wrongCredentialsEntered, userAccount.userSetDisplayName, userAccount.displayIndex.toLong() - ).executeAsOne() + return userAccountQueries.transactionWithResult { + userAccountQueries.insertUserAccount(userAccount.bankCode, userAccount.loginName, userAccount.password, userAccount.bankName, userAccount.bic, + userAccount.customerName, userAccount.userId, userAccount.selectedTanMethodId, userAccount.selectedTanMediumName, + userAccount.bankingGroup?.name, userAccount.iconUrl, userAccount.wrongCredentialsEntered, userAccount.userSetDisplayName, userAccount.displayIndex.toLong() + ) + + val userAccountId = getLastInsertedId() // getLastInsertedId() / last_insert_rowid() has to be called in a transaction with the insert operation, otherwise it will not work + + persistBankAccounts(userAccountId, userAccount.accounts) + + userAccountId + } + } + + + + fun getAllBankAccounts(): List = userAccountQueries.selectAllBankAccounts { id, userAccountId, identifier, accountHolderName, type, iban, subAccountNumber, productName, currency, accountLimit, isAccountTypeSupportedByApplication, features, balance, retrievedTransactionsFrom, retrievedTransactionsTo, countDaysForWhichTransactionsAreKept, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate -> + BankAccountEntity( + id, userAccountId, + identifier, accountHolderName, BankAccountType.valueOf(type), + iban, subAccountNumber, productName, currency, accountLimit, + + isAccountTypeSupportedByApplication, mapEnumSet(features, BankAccountFeatures.entries), + mapToAmount(balance), mapToDate(retrievedTransactionsFrom), mapToDate(retrievedTransactionsTo), + mapToInt(countDaysForWhichTransactionsAreKept), + + userSetDisplayName, mapToInt(displayIndex), + hideAccount, includeInAutomaticAccountsUpdate + ) + }.executeAsList() + + suspend fun persistBankAccounts(userAccountId: Long, bankAccounts: Collection): Map = + bankAccounts.associate { persistBankAccount(userAccountId, it) } + + private suspend fun persistBankAccount(userAccountId: Long, account: BankAccount): Pair { + userAccountQueries.insertBankAccount( + userAccountId, + account.identifier, account.accountHolderName, mapEnum(account.type), + account.iban, account.subAccountNumber, account.productName, account.currency, account.accountLimit, + + account.isAccountTypeSupportedByApplication, mapEnumCollectionToString(account.features), + + mapAmount(account.balance), + mapDate(account.retrievedTransactionsFrom), mapDate(account.retrievedTransactionsTo), + + mapInt(account.countDaysForWhichTransactionsAreKept), + + account.userSetDisplayName, mapInt(account.displayIndex), + account.hideAccount, account.includeInAutomaticAccountsUpdate + ) + + return Pair(getLastInsertedId(), account) } @@ -77,7 +127,7 @@ class SqliteBankingRepository( private suspend fun saveAccountTransaction(transaction: AccountTransaction) { accountTransactionQueries.insertTransaction( - transaction.amount.amount, transaction.currency, transaction.reference, + mapAmount(transaction.amount), transaction.currency, transaction.reference, mapDate(transaction.bookingDate), mapDate(transaction.valueDate), transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText, @@ -86,7 +136,7 @@ class SqliteBankingRepository( transaction.information, transaction.statementNumber?.toLong(), transaction.sequenceNumber?.toLong(), - transaction.openingBalance?.amount, transaction.closingBalance?.amount, + mapAmount(transaction.openingBalance), mapAmount(transaction.closingBalance), transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode, @@ -104,10 +154,59 @@ class SqliteBankingRepository( } - private fun mapToAmount(serializedAmount: String?): Amount? = serializedAmount?.let { Amount(it) } + private fun getLastInsertedId(): Long = + userAccountQueries.getLastInsertedId().executeAsOne() + + + @JvmName("mapAmount") + @JsName("mapAmount") + private fun mapAmount(amount: Amount?): String? = + amount?.let { mapAmount(it) } + + private fun mapAmount(amount: Amount): String = amount.amount + + private fun mapToAmount(serializedAmount: String?): Amount? = + serializedAmount?.let { mapToAmount(it) } + + private fun mapToAmount(serializedAmount: String): Amount = Amount(serializedAmount) + + @JvmName("mapDateNullable") + @JsName("mapDateNullable") + private fun mapDate(date: LocalDate?): String? = + date?.let { mapDate(it) } private fun mapDate(date: LocalDate): String = date.toString() + @JvmName("mapToDateNullable") + @JsName("mapToDateNullable") + private fun mapToDate(serializedDate: String?): LocalDate? = + serializedDate?.let { mapToDate(it) } + private fun mapToDate(serializedDate: String): LocalDate = LocalDate.parse(serializedDate) + private fun > mapEnum(enum: Enum): String = enum.name + + private fun > mapToEnum(enumName: String, values: EnumEntries): E = + values.first { it.name == enumName } + + private fun > mapEnumCollectionToString(enums: Collection): String = + enums.joinToString(",") { it.name } + + private fun > mapEnumSet(enumsString: String, values: EnumEntries): Set = + enumsString.split(',').map { mapToEnum(it, values) }.toSet() + + @JvmName("mapIntNullable") + @JsName("mapIntNullable") + private fun mapInt(int: Int?): Long? = + int?.let { mapInt(it) } + + private fun mapInt(int: Int): Long = int.toLong() + + @JvmName("mapToIntNullable") + @JsName("mapToIntNullable") + private fun mapToInt(int: Long?): Int? = + int?.let { mapToInt(it) } + + private fun mapToInt(int: Long): Int = int.toInt() + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/AccountTransactionEntity.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/AccountTransactionEntity.kt index 1795ee2..54798cb 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/AccountTransactionEntity.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/AccountTransactionEntity.kt @@ -21,6 +21,7 @@ class AccountTransactionEntity( bookingText: String? = null, userSetDisplayName: String? = null, + val category: String? = null, // TODO: add to AccountTransaction notes: String? = null, information: String? = null, diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/BankAccountEntity.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/BankAccountEntity.kt new file mode 100644 index 0000000..cde8c97 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/BankAccountEntity.kt @@ -0,0 +1,42 @@ +package net.codinux.banking.dataaccess.entities + +import kotlinx.datetime.LocalDate +import net.codinux.banking.client.model.* + +class BankAccountEntity( + val id: Long, + val userAccountId: Long, + + identifier: String, + accountHolderName: String, + type: BankAccountType = BankAccountType.Other, + iban: String? = null, + subAccountNumber: String? = null, + productName: String? = null, + currency: String = "EUR", + accountLimit: String? = null, + + isAccountTypeSupportedByApplication: Boolean = true, + features: Set = emptySet(), + + balance: Amount = Amount.Zero, // TODO: add a BigDecimal library + retrievedTransactionsFrom: LocalDate? = null, + retrievedTransactionsTo: LocalDate? = null, + + countDaysForWhichTransactionsAreKept: Int? = null, + +// bookedTransactions: MutableList = mutableListOf(), +// unbookedTransactions: MutableList = mutableListOf(), + + userSetDisplayName: String? = null, + displayIndex: Int = 0, + + hideAccount: Boolean = false, + includeInAutomaticAccountsUpdate: Boolean = true +) : BankAccount( + identifier, accountHolderName, type, iban, subAccountNumber, productName, currency, accountLimit, + isAccountTypeSupportedByApplication, features, balance, + retrievedTransactionsFrom, retrievedTransactionsTo, false, countDaysForWhichTransactionsAreKept, + mutableListOf(), mutableListOf(), + userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/UserAccountEntity.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/UserAccountEntity.kt index bb588ef..0b49030 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/UserAccountEntity.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/UserAccountEntity.kt @@ -1,6 +1,5 @@ package net.codinux.banking.dataaccess.entities -import net.codinux.banking.client.model.BankAccount import net.codinux.banking.client.model.BankingGroup import net.codinux.banking.client.model.CustomerAccount import net.codinux.banking.client.model.tan.TanMedium @@ -19,7 +18,7 @@ class UserAccountEntity( customerName: String, userId: String = loginName, - accounts: List = emptyList(), + accounts: List = emptyList(), // TODO: make accounts open selectedTanMethodId: String? = null, tanMethods: List = listOf(), @@ -50,4 +49,7 @@ class UserAccountEntity( user.bankingGroup, user.iconUrl, user.wrongCredentialsEntered, user.userSetDisplayName, user.displayIndex ) + + val accountEntities: List = accounts + } \ No newline at end of file diff --git a/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/UserAccount.sq b/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/UserAccount.sq index f44192c..93826d8 100644 --- a/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/UserAccount.sq +++ b/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/UserAccount.sq @@ -27,40 +27,114 @@ CREATE TABLE IF NOT EXISTS UserAccount ( ); -insertUserAccount { - INSERT INTO UserAccount( - bankCode, loginName, password, +insertUserAccount: +INSERT INTO UserAccount( + bankCode, loginName, password, - bankName, bic, + bankName, bic, - customerName, userId, + customerName, userId, - selectedTanMethodId, + selectedTanMethodId, - selectedTanMediumName, + selectedTanMediumName, - bankingGroup, iconUrl, + bankingGroup, iconUrl, - wrongCredentialsEntered, - userSetDisplayName, displayIndex - ) - VALUES( - ?, ?, ?, - ?, ?, - ?, ?, - ?, + wrongCredentialsEntered, + userSetDisplayName, displayIndex +) +VALUES( + ?, ?, ?, + ?, ?, + ?, ?, + ?, - ?, + ?, - ?, ?, - ?, - ?, ? - ); - - SELECT last_insert_rowid(); -} + ?, ?, + ?, + ?, ? +); selectAllUserAccounts: SELECT UserAccount.* FROM UserAccount; + + + +CREATE TABLE IF NOT EXISTS BankAccount ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + userAccountId INTEGER NOT NULL, + + identifier TEXT NOT NULL, + accountHolderName TEXT NOT NULL, + type TEXT NOT NULL, + iban TEXT, + subAccountNumber TEXT, + productName TEXT, + currency TEXT NOT NULL, + accountLimit TEXT, + + isAccountTypeSupportedByApplication INTEGER AS Boolean NOT NULL, + features TEXT NOT NULL, + + balance TEXT NOT NULL, + retrievedTransactionsFrom TEXT, + retrievedTransactionsTo TEXT, + + countDaysForWhichTransactionsAreKept INTEGER, + + userSetDisplayName TEXT, + displayIndex INTEGER NOT NULL, + + hideAccount INTEGER AS Boolean NOT NULL, + includeInAutomaticAccountsUpdate INTEGER AS Boolean NOT NULL +); + + +insertBankAccount: +INSERT INTO BankAccount( + userAccountId, + + identifier, accountHolderName, type, + iban, subAccountNumber, productName, currency, accountLimit, + + isAccountTypeSupportedByApplication, features, + + balance, retrievedTransactionsFrom, retrievedTransactionsTo, + + countDaysForWhichTransactionsAreKept, + + userSetDisplayName, displayIndex, + + hideAccount, includeInAutomaticAccountsUpdate +) +VALUES( + ?, + + ?, ?, ?, + ?, ?, ?, ?, ?, + + ?, ?, + + ?, ?, ?, + + ?, + + ?, ?, + + ?, ? +); + + +selectAllBankAccounts: +SELECT BankAccount.* +FROM BankAccount; + + +-- TODO: find a better place for this cross-cutting concern: +getLastInsertedId: +SELECT last_insert_rowid(); 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 9ebdb05..3691166 100644 --- a/composeApp/src/desktopTest/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepositoryTest.kt +++ b/composeApp/src/desktopTest/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepositoryTest.kt @@ -4,10 +4,7 @@ import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import kotlinx.coroutines.test.runTest import kotlinx.datetime.LocalDate -import net.codinux.banking.client.model.AccountTransaction -import net.codinux.banking.client.model.Amount -import net.codinux.banking.client.model.BankingGroup -import net.codinux.banking.client.model.CustomerAccount +import net.codinux.banking.client.model.* import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -23,7 +20,10 @@ class SqliteBankingRepositoryTest { @Test fun saveUserAccount() = runTest { - val userAccount = CustomerAccount("12345678", "SupiDupiUser", "geheim", "Abzock-Bank", "ABCDDEBBXXX", "Herr Maier", bankingGroup = BankingGroup.DKB).apply { + val bankAccounts = listOf( + BankAccount("12345", "Monika Tester", BankAccountType.CheckingAccount, balance = Amount("12.34"), retrievedTransactionsTo = LocalDate(2024, 5, 7), features = setOf(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.InstantPayment), countDaysForWhichTransactionsAreKept = 320) + ) + val userAccount = CustomerAccount("12345678", "SupiDupiUser", "geheim", "Abzock-Bank", "ABCDDEBBXXX", "Monika Tester", accounts = bankAccounts, bankingGroup = BankingGroup.DKB).apply { wrongCredentialsEntered = true displayIndex = 99 } @@ -51,6 +51,26 @@ class SqliteBankingRepositoryTest { assertEquals(userAccount.wrongCredentialsEntered, persisted.wrongCredentialsEntered) assertEquals(userAccount.displayIndex, persisted.displayIndex) + + assertEquals(1, persisted.accountEntities.size) + + val persistedBankAccount = persisted.accountEntities.first() + assertNotNull(persistedBankAccount.id) + assertEquals(persisted.id, persistedBankAccount.userAccountId) + + assertEquals(bankAccounts.first().identifier, persistedBankAccount.identifier) + assertEquals(bankAccounts.first().accountHolderName, persistedBankAccount.accountHolderName) + assertEquals(bankAccounts.first().type, persistedBankAccount.type) + + assertEquals(bankAccounts.first().balance, persistedBankAccount.balance) + assertEquals(bankAccounts.first().retrievedTransactionsFrom, persistedBankAccount.retrievedTransactionsFrom) + assertEquals(bankAccounts.first().retrievedTransactionsTo, persistedBankAccount.retrievedTransactionsTo) + + assertEquals(bankAccounts.first().features, persistedBankAccount.features) + + assertEquals(bankAccounts.first().countDaysForWhichTransactionsAreKept, persistedBankAccount.countDaysForWhichTransactionsAreKept) + assertEquals(bankAccounts.first().hideAccount, persistedBankAccount.hideAccount) + assertEquals(bankAccounts.first().includeInAutomaticAccountsUpdate, persistedBankAccount.includeInAutomaticAccountsUpdate) } @Test