Implemented saving BankAccounts to dbCommented functionality

This commit is contained in:
dankito 2024-08-27 22:12:25 +02:00
parent d75e8705ea
commit 55aad5242a
7 changed files with 282 additions and 44 deletions

View File

@ -121,7 +121,7 @@ sqldelight {
databases { databases {
create("BankmeisterDb") { create("BankmeisterDb") {
packageName.set("net.codinux.banking.dataaccess") packageName.set("net.codinux.banking.dataaccess")
generateAsync.set(true) generateAsync = true
} }
} }
} }

View File

@ -2,12 +2,13 @@ package net.codinux.banking.dataaccess
import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlDriver
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.*
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.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity
import kotlin.enums.EnumEntries
import kotlin.js.JsName
import kotlin.jvm.JvmName
class SqliteBankingRepository( class SqliteBankingRepository(
sqlDriver: SqlDriver sqlDriver: SqlDriver
@ -21,18 +22,67 @@ class SqliteBankingRepository(
override fun getAllUserAccounts(): List<UserAccountEntity> { override fun getAllUserAccounts(): List<UserAccountEntity> {
val bankAccounts = getAllBankAccounts().groupBy { it.userAccountId }
return userAccountQueries.selectAllUserAccounts { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodId, selectedTanMediumName, bankingGroup, iconUrl, wrongCredentialsEntered, userSetDisplayName, displayIndex -> 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()) bankingGroup?.let { BankingGroup.valueOf(it) }, iconUrl, wrongCredentialsEntered, userSetDisplayName, displayIndex.toInt())
}.executeAsList() }.executeAsList()
} }
override suspend fun persistUserAccount(userAccount: CustomerAccount): Long { override suspend fun persistUserAccount(userAccount: CustomerAccount): Long {
return userAccountQueries.insertUserAccount(userAccount.bankCode, userAccount.loginName, userAccount.password, userAccount.bankName, userAccount.bic, return userAccountQueries.transactionWithResult {
userAccount.customerName, userAccount.userId, userAccount.selectedTanMethodId, userAccount.selectedTanMediumName, userAccountQueries.insertUserAccount(userAccount.bankCode, userAccount.loginName, userAccount.password, userAccount.bankName, userAccount.bic,
userAccount.bankingGroup?.name, userAccount.iconUrl, userAccount.wrongCredentialsEntered, userAccount.userSetDisplayName, userAccount.displayIndex.toLong() userAccount.customerName, userAccount.userId, userAccount.selectedTanMethodId, userAccount.selectedTanMediumName,
).executeAsOne() 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<BankAccountEntity> = 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<BankAccount>): Map<Long, BankAccount> =
bankAccounts.associate { persistBankAccount(userAccountId, it) }
private suspend fun persistBankAccount(userAccountId: Long, account: BankAccount): Pair<Long, BankAccount> {
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) { private suspend fun saveAccountTransaction(transaction: AccountTransaction) {
accountTransactionQueries.insertTransaction( accountTransactionQueries.insertTransaction(
transaction.amount.amount, transaction.currency, transaction.reference, mapAmount(transaction.amount), transaction.currency, transaction.reference,
mapDate(transaction.bookingDate), mapDate(transaction.valueDate), mapDate(transaction.bookingDate), mapDate(transaction.valueDate),
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
transaction.bookingText, transaction.bookingText,
@ -86,7 +136,7 @@ class SqliteBankingRepository(
transaction.information, transaction.information,
transaction.statementNumber?.toLong(), transaction.sequenceNumber?.toLong(), 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.endToEndReference, transaction.customerReference, transaction.mandateReference,
transaction.creditorIdentifier, transaction.originatorsIdentificationCode, 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() 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 mapToDate(serializedDate: String): LocalDate = LocalDate.parse(serializedDate)
private fun <E : Enum<E>> mapEnum(enum: Enum<E>): String = enum.name
private fun <E : Enum<E>> mapToEnum(enumName: String, values: EnumEntries<E>): E =
values.first { it.name == enumName }
private fun <E : Enum<E>> mapEnumCollectionToString(enums: Collection<E>): String =
enums.joinToString(",") { it.name }
private fun <E : Enum<E>> mapEnumSet(enumsString: String, values: EnumEntries<E>): Set<E> =
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()
} }

View File

@ -21,6 +21,7 @@ class AccountTransactionEntity(
bookingText: String? = null, bookingText: String? = null,
userSetDisplayName: String? = null, userSetDisplayName: String? = null,
val category: String? = null, // TODO: add to AccountTransaction
notes: String? = null, notes: String? = null,
information: String? = null, information: String? = null,

View File

@ -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<BankAccountFeatures> = emptySet(),
balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
retrievedTransactionsFrom: LocalDate? = null,
retrievedTransactionsTo: LocalDate? = null,
countDaysForWhichTransactionsAreKept: Int? = null,
// bookedTransactions: MutableList<AccountTransaction> = mutableListOf(),
// unbookedTransactions: MutableList<UnbookedAccountTransaction> = 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
)

View File

@ -1,6 +1,5 @@
package net.codinux.banking.dataaccess.entities 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.BankingGroup
import net.codinux.banking.client.model.CustomerAccount import net.codinux.banking.client.model.CustomerAccount
import net.codinux.banking.client.model.tan.TanMedium import net.codinux.banking.client.model.tan.TanMedium
@ -19,7 +18,7 @@ class UserAccountEntity(
customerName: String, customerName: String,
userId: String = loginName, userId: String = loginName,
accounts: List<BankAccount> = emptyList(), accounts: List<BankAccountEntity> = emptyList(), // TODO: make accounts open
selectedTanMethodId: String? = null, selectedTanMethodId: String? = null,
tanMethods: List<TanMethod> = listOf(), tanMethods: List<TanMethod> = listOf(),
@ -50,4 +49,7 @@ class UserAccountEntity(
user.bankingGroup, user.iconUrl, user.wrongCredentialsEntered, user.userSetDisplayName, user.displayIndex user.bankingGroup, user.iconUrl, user.wrongCredentialsEntered, user.userSetDisplayName, user.displayIndex
) )
val accountEntities: List<BankAccountEntity> = accounts
} }

View File

@ -27,40 +27,114 @@ CREATE TABLE IF NOT EXISTS UserAccount (
); );
insertUserAccount { insertUserAccount:
INSERT INTO UserAccount( INSERT INTO UserAccount(
bankCode, loginName, password, bankCode, loginName, password,
bankName, bic, bankName, bic,
customerName, userId, customerName, userId,
selectedTanMethodId, selectedTanMethodId,
selectedTanMediumName, selectedTanMediumName,
bankingGroup, iconUrl, bankingGroup, iconUrl,
wrongCredentialsEntered, wrongCredentialsEntered,
userSetDisplayName, displayIndex userSetDisplayName, displayIndex
) )
VALUES( VALUES(
?, ?, ?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?,
?, ?,
?, ?, ?, ?,
?, ?,
?, ? ?, ?
); );
SELECT last_insert_rowid();
}
selectAllUserAccounts: selectAllUserAccounts:
SELECT UserAccount.* SELECT UserAccount.*
FROM 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();

View File

@ -4,10 +4,7 @@ import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.*
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.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
@ -23,7 +20,10 @@ class SqliteBankingRepositoryTest {
@Test @Test
fun saveUserAccount() = runTest { 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 wrongCredentialsEntered = true
displayIndex = 99 displayIndex = 99
} }
@ -51,6 +51,26 @@ class SqliteBankingRepositoryTest {
assertEquals(userAccount.wrongCredentialsEntered, persisted.wrongCredentialsEntered) assertEquals(userAccount.wrongCredentialsEntered, persisted.wrongCredentialsEntered)
assertEquals(userAccount.displayIndex, persisted.displayIndex) 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 @Test