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 {
implementation(libs.kotlin.test)
implementation(libs.coroutines.test)
}
androidMain.dependencies {

View File

@ -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<UserAccountEntity>
suspend fun persistUserAccount(userAccount: CustomerAccount): Long
fun getAllAccountTransactions(): List<AccountTransactionEntity>
suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>)

View File

@ -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<AccountTransaction>
userAccounts: Collection<CustomerAccount> = emptyList(),
transactions: Collection<AccountTransaction> = 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<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 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(
nextId++,
transaction.amount, transaction.currency, transaction.reference,

View File

@ -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<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> {
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(

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()
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))

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.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<*>) {

View File

@ -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<List<UserAccountEntity>>(emptyList())
val transactions = MutableStateFlow<List<AccountTransaction>>(emptyList())
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
@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()
}

View File

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

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" }
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" }