From 202c9217e3e07496d5ade2062127071d107b973f Mon Sep 17 00:00:00 2001 From: dankito Date: Thu, 12 Sep 2024 13:33:20 +0200 Subject: [PATCH] Implemented persisting and deleting Holdings (but not updating yet) --- .../banking/dataaccess/BankingRepository.kt | 9 +++ .../dataaccess/InMemoryBankingRepository.kt | 13 ++++ .../dataaccess/SqliteBankingRepository.kt | 58 +++++++++++++++- .../dataaccess/entities/BankAccountEntity.kt | 4 +- .../dataaccess/entities/HoldingEntity.kt | 46 +++++++++++++ .../banking/ui/service/BankingService.kt | 16 +++++ .../codinux/banking/ui/AccountTransaction.sq | 69 ++++++++++++++++++- 7 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/HoldingEntity.kt 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 be9ebca..c746630 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/BankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/BankingRepository.kt @@ -2,8 +2,10 @@ package net.codinux.banking.dataaccess import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.User +import net.codinux.banking.client.model.securitiesaccount.Holding import net.codinux.banking.dataaccess.entities.AccountTransactionEntity import net.codinux.banking.dataaccess.entities.BankAccountEntity +import net.codinux.banking.dataaccess.entities.HoldingEntity import net.codinux.banking.dataaccess.entities.UserEntity import net.codinux.banking.ui.model.AccountTransactionViewModel @@ -16,6 +18,13 @@ interface BankingRepository { suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List): List + suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List): List + + suspend fun updateHoldings(holdings: List) + + suspend fun deleteHoldings(holdings: List) + + fun getAllAccountTransactionsAsViewModel(): List fun getAllAccountTransactions(): List 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 94cccb5..c1f375a 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/InMemoryBankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/InMemoryBankingRepository.kt @@ -2,8 +2,10 @@ package net.codinux.banking.dataaccess import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.User +import net.codinux.banking.client.model.securitiesaccount.Holding import net.codinux.banking.dataaccess.entities.AccountTransactionEntity import net.codinux.banking.dataaccess.entities.BankAccountEntity +import net.codinux.banking.dataaccess.entities.HoldingEntity import net.codinux.banking.dataaccess.entities.UserEntity import net.codinux.banking.ui.model.AccountTransactionViewModel @@ -32,6 +34,17 @@ class InMemoryBankingRepository( } + override suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List): List = emptyList() // no-op + + override suspend fun updateHoldings(holdings: List) { + // no-op + } + + override suspend fun deleteHoldings(holdings: List) { + // no-op + } + + override fun getAllAccountTransactionsAsViewModel(): List = transactions.map { AccountTransactionViewModel(it) } 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 cab70a6..a2fd7ad 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/SqliteBankingRepository.kt @@ -4,6 +4,7 @@ import app.cash.sqldelight.db.SqlDriver import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import net.codinux.banking.client.model.* +import net.codinux.banking.client.model.securitiesaccount.Holding import net.codinux.banking.client.model.tan.* import net.codinux.banking.dataaccess.entities.* import net.codinux.banking.ui.model.AccountTransactionViewModel @@ -29,13 +30,20 @@ open class SqliteBankingRepository( val bankAccounts = getAllBankAccounts().groupBy { it.userId } val tanMethods = getAllTanMethods().groupBy { it.userId } val tanMedia = getAllTanMedia().groupBy { it.userId } + val holdings = getAllHoldings().groupBy { it.bankAccountId } return userQueries.selectAllUsers { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, userSetDisplayName, clientData, displayIndex, iconUrl, wrongCredentialsEntered -> - UserEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, bankAccounts[id] ?: emptyList(), selectedTanMethodIdentifier, tanMethods[id] ?: emptyList(), selectedTanMediumIdentifier, tanMedia[id] ?: emptyList(), + UserEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, getAccountsOfUser(id, bankAccounts, holdings), selectedTanMethodIdentifier, tanMethods[id] ?: emptyList(), selectedTanMediumIdentifier, tanMedia[id] ?: emptyList(), bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered) }.executeAsList() } + protected open fun getAccountsOfUser(userId: Long, bankAccounts: Map>, holdings: Map>): List { + return bankAccounts[userId].orEmpty().onEach { + it.holdings = holdings[it.id].orEmpty() + } + } + override suspend fun persistUser(user: User): UserEntity { return userQueries.transactionWithResult { userQueries.insertUser(user.bankCode, user.loginName, user.password, user.bankName, user.bic, @@ -106,7 +114,9 @@ open class SqliteBankingRepository( persistTransaction(userId, accountId, transaction) } - return BankAccountEntity(accountId, userId, account, accountTransactionEntities, account.holdings) + val holdings = account.holdings.map { holding -> persistHolding(userId, accountId, holding) } + + return BankAccountEntity(accountId, userId, account, accountTransactionEntities, holdings) } @@ -196,6 +206,50 @@ open class SqliteBankingRepository( } + protected open fun getAllHoldings(): List = + accountTransactionQueries.selectAllHoldings { id, userId, bankAccountId, name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate -> + HoldingEntity(id, userId, bankAccountId, name, isin, wkn, mapToInt(quantity), currency, mapToAmount(totalBalance), mapToAmount(marketValue), performancePercentage?.toFloat(), mapToAmount(totalCostPrice), mapToAmount(averageCostPrice), mapToInstant(pricingTime), mapToDate(buyingDate)) + }.executeAsList() + + override suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List): List = + accountTransactionQueries.transactionWithResult { + holdings.map { persistHolding(bankAccount.userId, bankAccount.id, it) } + } + + /** + * Has to be executed in a transaction in order that getting persisted Holding's id works~ + */ + protected open suspend fun persistHolding(userId: Long, bankAccountId: Long, holding: Holding): HoldingEntity { + accountTransactionQueries.insertHolding( + userId, bankAccountId, + + holding.name, holding.isin, holding.wkn, + + mapInt(holding.quantity), holding.currency, + + mapAmount(holding.totalBalance), mapAmount(holding.marketValue), + holding.performancePercentage?.toDouble(), + mapAmount(holding.totalCostPrice), mapAmount(holding.averageCostPrice), + + mapInstant(holding.pricingTime), mapDate(holding.buyingDate) + ) + + return HoldingEntity(getLastInsertedId(), userId, bankAccountId, holding) + } + + override suspend fun updateHoldings(holdings: List) { + // TODO + } + + override suspend fun deleteHoldings(holdings: List) { + accountTransactionQueries.transaction { + holdings.forEach { holding -> + accountTransactionQueries.deleteHolding(holding.id) + } + } + } + + override fun getAllAccountTransactionsAsViewModel(): List = accountTransactionQueries.selectAllTransactionsAsViewModel { id, userId, bankAccountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, category -> AccountTransactionViewModel(id, userId, bankAccountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, category) 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 index f855efc..3560dce 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/BankAccountEntity.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/BankAccountEntity.kt @@ -30,7 +30,7 @@ class BankAccountEntity( bookedTransactions: MutableList = mutableListOf(), prebookedTransactions: MutableList = mutableListOf(), - holdings: List = emptyList(), + override var holdings: List = emptyList(), userSetDisplayName: String? = null, displayIndex: Int = 0, @@ -54,7 +54,7 @@ class BankAccountEntity( userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ) { - constructor(id: Long, userId: Long, account: BankAccount, transactions: List = emptyList(), holdings: List = emptyList()) : this( + constructor(id: Long, userId: Long, account: BankAccount, transactions: List = emptyList(), holdings: List = emptyList()) : this( id, userId, account.identifier, account.subAccountNumber, account.iban, account.productName, diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/HoldingEntity.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/HoldingEntity.kt new file mode 100644 index 0000000..bcfba83 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/dataaccess/entities/HoldingEntity.kt @@ -0,0 +1,46 @@ +package net.codinux.banking.dataaccess.entities + +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import net.codinux.banking.client.model.Amount +import net.codinux.banking.client.model.securitiesaccount.Holding + +class HoldingEntity( + val id: Long, + val userId: Long, + val bankAccountId: Long, + + name: String, + + isin: String? = null, + wkn: String? = null, + + quantity: Int? = null, + currency: String? = null, + + totalBalance: Amount? = null, + marketValue: Amount? = null, + + performancePercentage: Float? = null, + + totalCostPrice: Amount? = null, + averageCostPrice: Amount? = null, + + pricingTime: Instant? = null, + buyingDate: LocalDate? = null +) : Holding(name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate) { + + constructor(id: Long, userId: Long, bankAccountId: Long, holding: Holding) : this( + id, userId, bankAccountId, + + holding.name, holding.isin, holding.wkn, + + holding.quantity, holding.currency, + + holding.totalBalance, holding.marketValue, + holding.performancePercentage, + holding.totalCostPrice, holding.averageCostPrice, + + holding.pricingTime, holding.buyingDate + ) +} \ No newline at end of file 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 eb29c39..304ee0b 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 @@ -172,6 +172,22 @@ class BankingService( emptyList() } + val existingHoldingsByIsin = account.holdings.associateBy { it.identifier } + val retrievedHoldingsByIsin = response.holdings.associateBy { it.identifier } + + val (newHoldings, updatedRetrievedHoldings) = response.holdings.partition { existingHoldingsByIsin.keys.contains(it.identifier) == false } + val (updatedExistingHoldings, deletedHoldings) = account.holdings.partition { retrievedHoldingsByIsin.keys.contains(it.identifier) } + + val persistedNewHoldings = bankingRepository.persistHoldings(account, newHoldings) +// bankingRepository.updateHoldings(updatedExistingHoldings) // TODO: map data of updatedRetrievedHoldings to updatedExistingHoldings + bankingRepository.deleteHoldings(deletedHoldings) + + account.holdings = account.holdings.toMutableList().apply { + addAll(persistedNewHoldings) + removeAll(deletedHoldings) + } + + updateHoldingsInUi(response.holdings) val transactionsViewModel = updateTransactionsInUi(newTransactionsEntities) diff --git a/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/AccountTransaction.sq b/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/AccountTransaction.sq index 66a02f9..d6fe0f8 100644 --- a/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/AccountTransaction.sq +++ b/composeApp/src/commonMain/sqldelight/net/codinux/banking/ui/AccountTransaction.sq @@ -133,4 +133,71 @@ FROM AccountTransaction WHERE userId = ?; getTransactionWithId: SELECT AccountTransaction.* -FROM AccountTransaction WHERE id = ?; \ No newline at end of file +FROM AccountTransaction WHERE id = ?; + + + + +CREATE TABLE IF NOT EXISTS Holding ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + userId INTEGER NOT NULL, + bankAccountId INTEGER NOT NULL, + + name TEXT NOT NULL, + isin TEXT, + wkn TEXT, + + quantity INTEGER , + currency TEXT, + + totalBalance TEXT, + marketValue TEXT, + + performancePercentage REAL, + + totalCostPrice TEXT, + averageCostPrice TEXT, + + pricingTime TEXT, + buyingDate TEXT +); + + +insertHolding: +INSERT INTO Holding( + userId, bankAccountId, + + name, isin, wkn, + + quantity, currency, + + totalBalance, marketValue, + performancePercentage, + + totalCostPrice, averageCostPrice, + + pricingTime, buyingDate +) +VALUES( + ?, ?, + + ?, ?, ?, + + ?, ?, + + ?, ?, + ?, + + ?,?, + + ?, ? +); + + +selectAllHoldings: +SELECT Holding.* +FROM Holding; + +deleteHolding: +DELETE FROM Holding WHERE id = ?; \ No newline at end of file