diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 0c12130..1f49d7a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -83,6 +83,8 @@ kotlin { commonTest.dependencies { implementation(libs.kotlin.test) + + implementation(libs.coroutines.test) } androidMain.dependencies { 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 8f0b550..2dfcd98 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/BankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/BankingRepository.kt @@ -1,10 +1,17 @@ package net.codinux.banking.dataaccess import net.codinux.banking.client.model.AccountTransaction +import net.codinux.banking.client.model.CustomerAccount import net.codinux.banking.dataaccess.entities.AccountTransactionEntity +import net.codinux.banking.dataaccess.entities.UserAccountEntity interface BankingRepository { + fun getAllUserAccounts(): List + + suspend fun persistUserAccount(userAccount: CustomerAccount): Long + + fun getAllAccountTransactions(): List suspend fun persistAccountTransactions(transactions: Collection) 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 2ca694c..e882d71 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/InMemoryBankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/InMemoryBankingRepository.kt @@ -1,17 +1,31 @@ package net.codinux.banking.dataaccess import net.codinux.banking.client.model.AccountTransaction +import net.codinux.banking.client.model.CustomerAccount import net.codinux.banking.dataaccess.entities.AccountTransactionEntity +import net.codinux.banking.dataaccess.entities.UserAccountEntity class InMemoryBankingRepository( - transactions: Collection + userAccounts: Collection = emptyList(), + transactions: Collection = emptyList() ) : BankingRepository { private var nextId = 0L // TODO: make thread-safe + private val userAccounts = userAccounts.map { map(it) }.toMutableList() + private val transactions = transactions.map { map(it) }.toMutableList() + override fun getAllUserAccounts(): List = userAccounts.toList() + + override suspend fun persistUserAccount(userAccount: CustomerAccount): Long { + val entity = map(userAccount) + this.userAccounts.add(entity) + return entity.id + } + + override fun getAllAccountTransactions(): List = transactions.toList() override suspend fun persistAccountTransactions(transactions: Collection) { @@ -19,6 +33,13 @@ class InMemoryBankingRepository( } + private fun map(account: CustomerAccount) = UserAccountEntity( + nextId++, + account.bankCode, account.loginName, account.password, account.bankName, account.bic, account.customerName, account.userId, + emptyList(), account.selectedTanMethodId, emptyList(), account.selectedTanMediumName, emptyList(), + account.bankingGroup, account.iconUrl, account.wrongCredentialsEntered, account.userSetDisplayName, account.displayIndex + ) + private fun map(transaction: AccountTransaction) = AccountTransactionEntity( nextId++, transaction.amount, transaction.currency, transaction.reference, 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 45466d2..0b84497 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt @@ -4,7 +4,10 @@ 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.dataaccess.entities.AccountTransactionEntity +import net.codinux.banking.dataaccess.entities.UserAccountEntity class SqliteBankingRepository( sqlDriver: SqlDriver @@ -12,9 +15,27 @@ class SqliteBankingRepository( private val database = BankmeisterDb(sqlDriver) + private val userAccountQueries = database.userAccountQueries + private val accountTransactionQueries = database.accountTransactionQueries + override fun getAllUserAccounts(): List { + + 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(), + 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() + } + + override fun getAllAccountTransactions(): List { return accountTransactionQueries.selectAllTransactions { id, amount, currency, reference, bookingDate, valueDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, userSetDisplayName, 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( 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 new file mode 100644 index 0000000..bb588ef --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/UserAccountEntity.kt @@ -0,0 +1,53 @@ +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 +import net.codinux.banking.client.model.tan.TanMethod + +class UserAccountEntity( + val id: Long, + + bankCode: String, + loginName: String, + password: String?, + + bankName: String, + bic: String, + + customerName: String, + userId: String = loginName, + + accounts: List = emptyList(), + + selectedTanMethodId: String? = null, + tanMethods: List = listOf(), + + selectedTanMediumName: String? = null, + tanMedia: List = listOf(), + + bankingGroup: BankingGroup? = null, + iconUrl: String? = null, + + wrongCredentialsEntered: Boolean = false, + + userSetDisplayName: String? = null, + displayIndex: Int = 0 +) : CustomerAccount(bankCode, loginName, password, bankName, bic, customerName, userId, accounts, selectedTanMethodId, tanMethods, selectedTanMediumName, tanMedia, bankingGroup, iconUrl) { + + init { + this.wrongCredentialsEntered = wrongCredentialsEntered + this.userSetDisplayName = userSetDisplayName + this.displayIndex = displayIndex + } + + + constructor(id: Long, user: CustomerAccount) : this( + id, + user.bankCode, user.loginName, user.password, user.bankName, user.bic, user.customerName, user.userId, + emptyList(), user.selectedTanMethodId, emptyList(), user.selectedTanMediumName, emptyList(), + user.bankingGroup, user.iconUrl, user.wrongCredentialsEntered, user.userSetDisplayName, user.displayIndex + ) + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/DI.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/DI.kt index 7320ec8..8495fd1 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/DI.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/DI.kt @@ -21,10 +21,10 @@ object DI { val bankFinder = BankFinder() + var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList()) - val bankingService by lazy { - BankingService(uiState, bankingRepository, bankFinder) } + val bankingService by lazy { BankingService(uiState, bankingRepository, bankFinder) } fun setRepository(sqlDriver: SqlDriver) = setRepository(SqliteBankingRepository(sqlDriver)) 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 28a4425..570d1ae 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 @@ -11,6 +11,7 @@ import net.codinux.banking.client.model.options.RetrieveTransactions import net.codinux.banking.client.model.request.GetAccountDataRequest import net.codinux.banking.client.model.response.* 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.BankInfo @@ -39,9 +40,11 @@ class BankingService( suspend fun init() { try { + uiState.userAccounts.value = bankingRepository.getAllUserAccounts() + uiState.transactions.value = bankingRepository.getAllAccountTransactions() } catch (e: Throwable) { - log.error(e) { "Could not read all account transactions from repository" } + log.error(e) { "Could not read all user accounts and account transactions from repository" } } } @@ -69,11 +72,8 @@ class BankingService( } private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) { - // TODO: save customer - val transactions = uiState.transactions.value.toMutableList() transactions.addAll(response.bookedTransactions) - uiState.transactions.value = transactions.sortedByDescending { it.valueDate } try { @@ -83,6 +83,18 @@ class BankingService( } catch (e: Throwable) { log.error(e) { "Could not save account transactions ${response.bookedTransactions}" } } + + try { + val newUserAccountId = bankingRepository.persistUserAccount(response.customer) + + log.info { "Saved user account ${response.customer}" } + + val userAccounts = uiState.userAccounts.value.toMutableList() + userAccounts.add(UserAccountEntity(newUserAccountId, response.customer)) + uiState.userAccounts.value = userAccounts + } catch (e: Throwable) { + log.error(e) { "Could not save user account ${response.customer}" } + } } private fun handleUnsuccessfulBankingClientResponse(action: BankingClientAction, response: Response<*>) { 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 4105cb0..6e5dc8c 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 @@ -3,6 +3,7 @@ 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.TanChallengeReceived import net.codinux.banking.ui.model.error.ApplicationError import net.codinux.banking.ui.model.error.BankingClientError @@ -10,6 +11,8 @@ import net.codinux.banking.ui.model.error.ErroneousAction class UiState : ViewModel() { + val userAccounts = MutableStateFlow>(emptyList()) + val transactions = MutableStateFlow>(emptyList()) val applicationErrorOccurred = MutableStateFlow(null) diff --git a/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/UserAccount.sq b/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/UserAccount.sq new file mode 100644 index 0000000..f44192c --- /dev/null +++ b/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/UserAccount.sq @@ -0,0 +1,66 @@ +import kotlin.Boolean; + +CREATE TABLE IF NOT EXISTS UserAccount ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + bankCode TEXT NOT NULL, + loginName TEXT NOT NULL, + password TEXT, + + bankName TEXT NOT NULL, + bic TEXT NOT NULL, + + customerName TEXT NOT NULL, + userId TEXT NOT NULL, + + selectedTanMethodId TEXT, + + selectedTanMediumName TEXT, + + bankingGroup TEXT, + iconUrl TEXT, + + wrongCredentialsEntered INTEGER AS Boolean NOT NULL, + + userSetDisplayName TEXT, + displayIndex INTEGER NOT NULL +); + + +insertUserAccount { + INSERT INTO UserAccount( + bankCode, loginName, password, + + bankName, bic, + + customerName, userId, + + selectedTanMethodId, + + selectedTanMediumName, + + bankingGroup, iconUrl, + + wrongCredentialsEntered, + userSetDisplayName, displayIndex + ) + VALUES( + ?, ?, ?, + ?, ?, + ?, ?, + ?, + + ?, + + ?, ?, + ?, + ?, ? + ); + + SELECT last_insert_rowid(); +} + + +selectAllUserAccounts: +SELECT UserAccount.* +FROM UserAccount; diff --git a/composeApp/src/desktopMain/kotlin/net/codinux/banking/ui/main.kt b/composeApp/src/desktopMain/kotlin/net/codinux/banking/ui/main.kt index 1995ea7..119fc01 100644 --- a/composeApp/src/desktopMain/kotlin/net/codinux/banking/ui/main.kt +++ b/composeApp/src/desktopMain/kotlin/net/codinux/banking/ui/main.kt @@ -39,7 +39,10 @@ fun main() = application { @Preview @Composable fun AppPreview() { - DI.setRepository(InMemoryBankingRepository(listOf(AccountTransaction(Amount("12.34"), "EUR", "Lohn", LocalDate(2024, 7, 5), LocalDate(2024, 6, 15), "Dein Boss")))) + DI.setRepository(InMemoryBankingRepository( + emptyList(), + listOf(AccountTransaction(Amount("12.34"), "EUR", "Lohn", LocalDate(2024, 7, 5), LocalDate(2024, 6, 15), "Dein Boss")) + )) App() } \ 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 6b951d7..9ebdb05 100644 --- a/composeApp/src/desktopTest/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepositoryTest.kt +++ b/composeApp/src/desktopTest/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepositoryTest.kt @@ -2,10 +2,12 @@ package net.codinux.banking.dataaccess import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver -import kotlinx.coroutines.runBlocking +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 kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -20,7 +22,39 @@ class SqliteBankingRepositoryTest { @Test - fun saveTransaction() = runBlocking { + fun saveUserAccount() = runTest { + val userAccount = CustomerAccount("12345678", "SupiDupiUser", "geheim", "Abzock-Bank", "ABCDDEBBXXX", "Herr Maier", bankingGroup = BankingGroup.DKB).apply { + wrongCredentialsEntered = true + displayIndex = 99 + } + + underTest.persistUserAccount(userAccount) + + val result = underTest.getAllUserAccounts() + + assertEquals(1, result.size) + + val persisted = result.first() + assertNotNull(persisted.id) + + assertEquals(userAccount.bankCode, persisted.bankCode) + assertEquals(userAccount.loginName, persisted.loginName) + assertEquals(userAccount.password, persisted.password) + + assertEquals(userAccount.bankName, persisted.bankName) + assertEquals(userAccount.bic, persisted.bic) + + assertEquals(userAccount.customerName, persisted.customerName) + assertEquals(userAccount.userId, persisted.userId) + + assertEquals(userAccount.bankingGroup, persisted.bankingGroup) + + assertEquals(userAccount.wrongCredentialsEntered, persisted.wrongCredentialsEntered) + assertEquals(userAccount.displayIndex, persisted.displayIndex) + } + + @Test + fun saveTransaction() = runTest { val transaction = AccountTransaction(Amount("12.45"), "EUR", "Lohn", LocalDate(2024, 5, 7), LocalDate(2024, 6, 15), "Dein Boss") underTest.persistAccountTransactions(listOf(transaction)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac7d01c..13421e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ banking-client-model = { group = "net.codinux.banking.client", name = "banking-c fints4k-banking-client = { group = "net.codinux.banking.client", name = "fints4k-banking-client", version.ref = "banking-client" } kcsv = { group = "net.codinux.csv", name = "kcsv", version.ref = "kcsv" } +coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-serializable = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serializable" } klf = { group = "net.codinux.log", name = "klf", version.ref = "klf" }