Implemented saving CustomerAccounts to db

This commit is contained in:
dankito 2024-08-27 13:50:18 +02:00
parent 6e6eb91e74
commit 8a7226661f
12 changed files with 233 additions and 10 deletions

View File

@ -83,6 +83,8 @@ kotlin {
commonTest.dependencies { commonTest.dependencies {
implementation(libs.kotlin.test) implementation(libs.kotlin.test)
implementation(libs.coroutines.test)
} }
androidMain.dependencies { androidMain.dependencies {

View File

@ -1,10 +1,17 @@
package net.codinux.banking.dataaccess package net.codinux.banking.dataaccess
import net.codinux.banking.client.model.AccountTransaction 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.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.UserAccountEntity
interface BankingRepository { interface BankingRepository {
fun getAllUserAccounts(): List<UserAccountEntity>
suspend fun persistUserAccount(userAccount: CustomerAccount): Long
fun getAllAccountTransactions(): List<AccountTransactionEntity> fun getAllAccountTransactions(): List<AccountTransactionEntity>
suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>) suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>)

View File

@ -1,17 +1,31 @@
package net.codinux.banking.dataaccess package net.codinux.banking.dataaccess
import net.codinux.banking.client.model.AccountTransaction 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.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.UserAccountEntity
class InMemoryBankingRepository( class InMemoryBankingRepository(
transactions: Collection<AccountTransaction> userAccounts: Collection<CustomerAccount> = emptyList(),
transactions: Collection<AccountTransaction> = emptyList()
) : BankingRepository { ) : BankingRepository {
private var nextId = 0L // TODO: make thread-safe private var nextId = 0L // TODO: make thread-safe
private val userAccounts = userAccounts.map { map(it) }.toMutableList()
private val transactions = transactions.map { map(it) }.toMutableList() private val transactions = transactions.map { map(it) }.toMutableList()
override fun getAllUserAccounts(): List<UserAccountEntity> = userAccounts.toList()
override suspend fun persistUserAccount(userAccount: CustomerAccount): Long {
val entity = map(userAccount)
this.userAccounts.add(entity)
return entity.id
}
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>) {
@ -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( private fun map(transaction: AccountTransaction) = AccountTransactionEntity(
nextId++, nextId++,
transaction.amount, transaction.currency, transaction.reference, transaction.amount, transaction.currency, transaction.reference,

View File

@ -4,7 +4,10 @@ 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.AccountTransaction
import net.codinux.banking.client.model.Amount 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.UserAccountEntity
class SqliteBankingRepository( class SqliteBankingRepository(
sqlDriver: SqlDriver sqlDriver: SqlDriver
@ -12,9 +15,27 @@ class SqliteBankingRepository(
private val database = BankmeisterDb(sqlDriver) private val database = BankmeisterDb(sqlDriver)
private val userAccountQueries = database.userAccountQueries
private val accountTransactionQueries = database.accountTransactionQueries private val accountTransactionQueries = database.accountTransactionQueries
override fun getAllUserAccounts(): List<UserAccountEntity> {
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<AccountTransactionEntity> { override fun getAllAccountTransactions(): List<AccountTransactionEntity> {
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 -> 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( AccountTransactionEntity(

View File

@ -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<BankAccount> = emptyList(),
selectedTanMethodId: String? = null,
tanMethods: List<TanMethod> = listOf(),
selectedTanMediumName: String? = null,
tanMedia: List<TanMedium> = 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
)
}

View File

@ -21,10 +21,10 @@ object DI {
val bankFinder = BankFinder() val bankFinder = BankFinder()
var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList()) var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList())
val bankingService by lazy { val bankingService by lazy { BankingService(uiState, bankingRepository, bankFinder) }
BankingService(uiState, bankingRepository, bankFinder) }
fun setRepository(sqlDriver: SqlDriver) = setRepository(SqliteBankingRepository(sqlDriver)) fun setRepository(sqlDriver: SqlDriver) = setRepository(SqliteBankingRepository(sqlDriver))

View File

@ -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.request.GetAccountDataRequest
import net.codinux.banking.client.model.response.* import net.codinux.banking.client.model.response.*
import net.codinux.banking.dataaccess.BankingRepository 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.FinTsClientConfiguration
import net.codinux.banking.fints.config.FinTsClientOptions import net.codinux.banking.fints.config.FinTsClientOptions
import net.codinux.banking.ui.model.BankInfo import net.codinux.banking.ui.model.BankInfo
@ -39,9 +40,11 @@ class BankingService(
suspend fun init() { suspend fun init() {
try { try {
uiState.userAccounts.value = bankingRepository.getAllUserAccounts()
uiState.transactions.value = bankingRepository.getAllAccountTransactions() uiState.transactions.value = bankingRepository.getAllAccountTransactions()
} catch (e: Throwable) { } 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) { private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) {
// TODO: save customer
val transactions = uiState.transactions.value.toMutableList() val transactions = uiState.transactions.value.toMutableList()
transactions.addAll(response.bookedTransactions) transactions.addAll(response.bookedTransactions)
uiState.transactions.value = transactions.sortedByDescending { it.valueDate } uiState.transactions.value = transactions.sortedByDescending { it.valueDate }
try { try {
@ -83,6 +83,18 @@ class BankingService(
} 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 {
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<*>) { private fun handleUnsuccessfulBankingClientResponse(action: BankingClientAction, response: Response<*>) {

View File

@ -3,6 +3,7 @@ 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.client.model.AccountTransaction
import net.codinux.banking.dataaccess.entities.UserAccountEntity
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
@ -10,6 +11,8 @@ import net.codinux.banking.ui.model.error.ErroneousAction
class UiState : ViewModel() { class UiState : ViewModel() {
val userAccounts = MutableStateFlow<List<UserAccountEntity>>(emptyList())
val transactions = MutableStateFlow<List<AccountTransaction>>(emptyList()) val transactions = MutableStateFlow<List<AccountTransaction>>(emptyList())
val applicationErrorOccurred = MutableStateFlow<ApplicationError?>(null) val applicationErrorOccurred = MutableStateFlow<ApplicationError?>(null)

View File

@ -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;

View File

@ -39,7 +39,10 @@ fun main() = application {
@Preview @Preview
@Composable @Composable
fun AppPreview() { 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() App()
} }

View File

@ -2,10 +2,12 @@ package net.codinux.banking.dataaccess
import app.cash.sqldelight.async.coroutines.synchronous 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.runBlocking 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.AccountTransaction
import net.codinux.banking.client.model.Amount 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
@ -20,7 +22,39 @@ class SqliteBankingRepositoryTest {
@Test @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") val transaction = AccountTransaction(Amount("12.45"), "EUR", "Lohn", LocalDate(2024, 5, 7), LocalDate(2024, 6, 15), "Dein Boss")
underTest.persistAccountTransactions(listOf(transaction)) underTest.persistAccountTransactions(listOf(transaction))

View File

@ -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" } 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" } 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" } kotlinx-serializable = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serializable" }
klf = { group = "net.codinux.log", name = "klf", version.ref = "klf" } klf = { group = "net.codinux.log", name = "klf", version.ref = "klf" }