Implemented updating account's transactions
This commit is contained in:
parent
3eb3c488da
commit
d3d90d4737
|
@ -1,7 +1,9 @@
|
|||
package net.codinux.banking.dataaccess
|
||||
|
||||
import net.codinux.banking.client.model.AccountTransaction
|
||||
import net.codinux.banking.client.model.UserAccount
|
||||
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.ui.model.AccountTransactionViewModel
|
||||
|
||||
|
@ -11,9 +13,13 @@ interface BankingRepository {
|
|||
|
||||
suspend fun persistUserAccount(userAccount: UserAccount): UserAccountEntity
|
||||
|
||||
suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity>
|
||||
|
||||
|
||||
fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel>
|
||||
|
||||
fun getAllAccountTransactions(): List<AccountTransactionEntity>
|
||||
|
||||
fun getAllTransactionsOfUserAccount(userAccount: UserAccountEntity): List<AccountTransactionEntity>
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@ package net.codinux.banking.dataaccess
|
|||
import net.codinux.banking.client.model.AccountTransaction
|
||||
import net.codinux.banking.client.model.UserAccount
|
||||
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.ui.model.AccountTransactionViewModel
|
||||
|
||||
|
@ -26,12 +27,19 @@ class InMemoryBankingRepository(
|
|||
return entity
|
||||
}
|
||||
|
||||
override suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity> {
|
||||
throw NotImplementedError("Lazy developer, method is not implemented")
|
||||
}
|
||||
|
||||
|
||||
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
|
||||
transactions.map { AccountTransactionViewModel(it) }
|
||||
|
||||
override fun getAllAccountTransactions(): List<AccountTransactionEntity> = transactions.toList()
|
||||
|
||||
override fun getAllTransactionsOfUserAccount(userAccount: UserAccountEntity): List<AccountTransactionEntity> =
|
||||
getAllAccountTransactions().filter { it.userAccountId == userAccount.id }
|
||||
|
||||
|
||||
private fun map(account: UserAccount) = UserAccountEntity(
|
||||
nextId++,
|
||||
|
|
|
@ -139,8 +139,52 @@ open class SqliteBankingRepository(
|
|||
}.executeAsList()
|
||||
}
|
||||
|
||||
override fun getAllTransactionsOfUserAccount(userAccount: UserAccountEntity): List<AccountTransactionEntity> {
|
||||
return accountTransactionQueries.selectAllTransactionsOfUserAccount(userAccount.id) { id, userAccountId, bankAccountId, amount, currency, reference, bookingDate, valueDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, userSetDisplayName, category, 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(
|
||||
id,
|
||||
userAccountId, bankAccountId,
|
||||
|
||||
Amount(amount), currency, reference,
|
||||
mapToDate(bookingDate), mapToDate(valueDate),
|
||||
otherPartyName, otherPartyBankCode, otherPartyAccountId,
|
||||
bookingText,
|
||||
|
||||
userSetDisplayName, category, notes,
|
||||
|
||||
information,
|
||||
statementNumber?.toInt(), sequenceNumber?.toInt(),
|
||||
|
||||
mapToAmount(openingBalance), mapToAmount(closingBalance),
|
||||
|
||||
endToEndReference, customerReference, mandateReference,
|
||||
creditorIdentifier, originatorsIdentificationCode,
|
||||
compensationAmount, originalAmount,
|
||||
sepaReference,
|
||||
deviantOriginator, deviantRecipient,
|
||||
referenceWithNoSpecialType, primaNotaNumber,
|
||||
textKeySupplement,
|
||||
|
||||
currencyType, bookingKey,
|
||||
referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution,
|
||||
supplementaryDetails,
|
||||
|
||||
transactionReferenceNumber, relatedReferenceNumber
|
||||
)
|
||||
}.executeAsList()
|
||||
}
|
||||
|
||||
|
||||
override suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity> {
|
||||
return accountTransactionQueries.transactionWithResult {
|
||||
transactions.map { transaction ->
|
||||
persistTransaction(bankAccount.userAccountId, bankAccount.id, transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Has to be executed in a transaction in order that getting persisted BankAccount's id works~
|
||||
* Has to be executed in a transaction in order that getting persisted AccountTransaction's id works~
|
||||
*/
|
||||
protected open suspend fun persistTransaction(userAccountId: Long, bankAccountId: Long, transaction: AccountTransaction): AccountTransactionEntity {
|
||||
accountTransactionQueries.insertTransaction(
|
||||
|
|
|
@ -32,7 +32,13 @@ private val typography = Typography(
|
|||
fun App() {
|
||||
LoggerFactory.defaultLoggerName = "net.codinux.banking.ui.Bankmeister"
|
||||
|
||||
val colors = MaterialTheme.colors.copy(primary = Colors.Primary, primaryVariant = Colors.PrimaryDark, secondary = Colors.Accent, onSecondary = Color.White)
|
||||
val colors = MaterialTheme.colors.copy(primary = Colors.Primary, primaryVariant = Colors.PrimaryDark, onPrimary = Color.White,
|
||||
secondary = Colors.Accent, secondaryVariant = Colors.Accent, onSecondary = Color.White)
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// the same values as in BottomBar, but LocalContentColor.current and LocalContentAlpha.current have a different value there
|
||||
val snackbarTextColor = MaterialTheme.colors.onPrimary.copy(alpha = 0.74f) // 0.74f = ContentAlpha.HighContrastContentAlpha.medium
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
@ -51,6 +57,23 @@ fun App() {
|
|||
TransactionsList(DI.uiState)
|
||||
}
|
||||
|
||||
SnackbarHost(
|
||||
hostState = snackbarHostState
|
||||
) { data ->
|
||||
Snackbar(
|
||||
modifier = Modifier.padding(bottom = 18.dp).padding(12.dp),
|
||||
action = { if (data.actionLabel == null) null else {
|
||||
TextButton(
|
||||
onClick = { data.performAction() },
|
||||
content = { Text(data.actionLabel!!, color = Colors.CodinuxSecondaryColor) }
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { Text(data.message, color = snackbarTextColor) },
|
||||
backgroundColor = Colors.Primary
|
||||
)
|
||||
}
|
||||
|
||||
BottomBar()
|
||||
}
|
||||
|
||||
|
@ -65,7 +88,7 @@ fun App() {
|
|||
}
|
||||
}
|
||||
|
||||
StateHandler(DI.uiState)
|
||||
StateHandler(DI.uiState, snackbarHostState)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -138,6 +138,12 @@ fun BottomBar() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxHeight().widthIn(IconWidth.times(2), IconWidth.times(2)), verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton({ coroutineScope.launch { DI.bankingService.updateAccountTransactions() } }, Modifier.width(IconWidth)) { // TODO: use sync, cached or autorenew as icon?
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "Neue Kontoumsätze abholen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package net.codinux.banking.ui.composables
|
||||
|
||||
import androidx.compose.material.SnackbarDuration
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.coroutines.launch
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.banking.ui.dialogs.AddAccountDialog
|
||||
import net.codinux.banking.ui.dialogs.ApplicationErrorDialog
|
||||
import net.codinux.banking.ui.dialogs.BankingClientErrorDialog
|
||||
|
@ -9,7 +13,7 @@ import net.codinux.banking.ui.screens.ExportScreen
|
|||
import net.codinux.banking.ui.state.UiState
|
||||
|
||||
@Composable
|
||||
fun StateHandler(uiState: UiState) {
|
||||
fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
||||
val showAddAccountDialog by uiState.showAddAccountDialog.collectAsState()
|
||||
val showExportScreen by uiState.showExportScreen.collectAsState()
|
||||
|
||||
|
@ -17,6 +21,8 @@ fun StateHandler(uiState: UiState) {
|
|||
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
|
||||
val applicationError by uiState.applicationErrorOccurred.collectAsState()
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
||||
if (showAddAccountDialog) {
|
||||
AddAccountDialog { uiState.showAddAccountDialog.value = false }
|
||||
|
@ -45,4 +51,25 @@ fun StateHandler(uiState: UiState) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch {
|
||||
DI.uiState.transactionsRetrievedEvents.collect { event ->
|
||||
val messagePrefix = if (event.newTransactions.isEmpty()) {
|
||||
"Keine neuen Umsätze"
|
||||
} else if (event.newTransactions.size == 1) {
|
||||
"1 Umsatz"
|
||||
} else {
|
||||
"${event.newTransactions.size} Umsätze"
|
||||
}
|
||||
|
||||
snackbarHostState.showSnackbar(
|
||||
message = "$messagePrefix für ${event.user.displayName} ${event.account.displayName}",
|
||||
actionLabel = "Coolio",
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package net.codinux.banking.ui.model.events
|
||||
|
||||
import net.codinux.banking.client.model.BankAccount
|
||||
import net.codinux.banking.client.model.UserAccount
|
||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||
|
||||
data class AccountTransactionsRetrieved(
|
||||
val user: UserAccount,
|
||||
val account: BankAccount,
|
||||
val newTransactions: List<AccountTransactionViewModel>
|
||||
)
|
|
@ -1,24 +1,27 @@
|
|||
package net.codinux.banking.ui.service
|
||||
|
||||
import bankmeister.composeapp.generated.resources.Res
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.codinux.banking.client.SimpleBankingClientCallback
|
||||
import net.codinux.banking.client.fints4k.FinTs4kBankingClient
|
||||
import net.codinux.banking.client.model.AccountTransaction
|
||||
import net.codinux.banking.client.model.Amount
|
||||
import net.codinux.banking.client.model.*
|
||||
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||
import net.codinux.banking.client.model.options.RetrieveTransactions
|
||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||
import net.codinux.banking.client.model.response.ErrorType
|
||||
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
||||
import net.codinux.banking.client.model.response.Response
|
||||
import net.codinux.banking.client.model.response.ResponseType
|
||||
import net.codinux.banking.client.model.response.*
|
||||
import net.codinux.banking.dataaccess.BankingRepository
|
||||
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.fints.config.FinTsClientConfiguration
|
||||
import net.codinux.banking.fints.config.FinTsClientOptions
|
||||
import net.codinux.banking.ui.IOorDefault
|
||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||
import net.codinux.banking.ui.model.BankInfo
|
||||
import net.codinux.banking.ui.model.TanChallengeReceived
|
||||
import net.codinux.banking.ui.model.error.*
|
||||
import net.codinux.banking.ui.model.events.AccountTransactionsRetrieved
|
||||
import net.codinux.banking.ui.state.UiState
|
||||
import net.codinux.csv.reader.CsvReader
|
||||
import net.codinux.log.logger
|
||||
|
@ -53,6 +56,8 @@ class BankingService(
|
|||
|
||||
fun getAllAccountTransactions() = bankingRepository.getAllAccountTransactions()
|
||||
|
||||
fun getAllTransactionsOfUserAccount(userAccount: UserAccountEntity) = bankingRepository.getAllTransactionsOfUserAccount(userAccount)
|
||||
|
||||
fun getAllAccountTransactionsAsViewModel() = bankingRepository.getAllAccountTransactionsAsViewModel()
|
||||
|
||||
|
||||
|
@ -90,14 +95,109 @@ class BankingService(
|
|||
userAccounts.add(newUserEntity)
|
||||
uiState.userAccounts.value = userAccounts
|
||||
|
||||
val transactions = uiState.transactions.value.toMutableList()
|
||||
transactions.addAll(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities }.map { AccountTransactionViewModel(it) })
|
||||
uiState.transactions.value = transactions.sortedByDescending { it.valueDate }
|
||||
updateTransactionsInUi(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities })
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not save user account ${response.user}" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun updateAccountTransactions() {
|
||||
val selectedAccount = uiState.transactionsFilter.value.selectedAccount
|
||||
if (selectedAccount != null) {
|
||||
updateAccountTransactions(selectedAccount.userAccount, selectedAccount.bankAccount)
|
||||
} else {
|
||||
uiState.userAccounts.value.forEach { user ->
|
||||
updateAccountTransactions(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateAccountTransactions(user: UserAccountEntity, bankAccount: BankAccountEntity? = null) {
|
||||
withContext(Dispatchers.IOorDefault) {
|
||||
try {
|
||||
val response = client.updateAccountTransactionsAsync(user, bankAccount?.let { listOf(it) })
|
||||
if (response.type == ResponseType.Success && response.data != null) {
|
||||
handleSuccessfulUpdateAccountTransactionsResponse(user, response.data!!)
|
||||
} else {
|
||||
handleUnsuccessfulBankingClientResponse(BankingClientAction.UpdateAccountTransactions, response)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not update account transactions for $user" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleSuccessfulUpdateAccountTransactionsResponse(user: UserAccountEntity, responses: List<GetTransactionsResponse>) {
|
||||
try {
|
||||
// TODO: when user gets updated by BankingClient, also update user in database
|
||||
// val newUser = response.user
|
||||
// val newUserEntity = bankingRepository.persistUserAccount(newUser)
|
||||
//
|
||||
// log.info { "Saved user account $newUserEntity with ${newUserEntity.accounts.flatMap { it.bookedTransactionsEntities }.size} transactions" }
|
||||
|
||||
val userAccountTransactions = getAllTransactionsOfUserAccount(user)
|
||||
|
||||
responses.forEach { response ->
|
||||
val account = (response.account as? BankAccountEntity) ?: user.accounts.first { it.identifier == response.account.identifier && it.subAccountNumber == response.account.subAccountNumber }
|
||||
|
||||
// TODO: this should be done in BankingClient
|
||||
if (account.lastTransactionRetrievalTime == null || account.lastTransactionRetrievalTime!! < response.transactionRetrievalTime) {
|
||||
account.lastTransactionRetrievalTime = response.transactionRetrievalTime
|
||||
}
|
||||
if (account.retrievedTransactionsFrom == null || (response.retrievedTransactionsFrom != null && account.retrievedTransactionsFrom!! < response.retrievedTransactionsFrom!!)) {
|
||||
account.retrievedTransactionsFrom = response.retrievedTransactionsFrom
|
||||
}
|
||||
|
||||
// TODO: update BankAccount and may updated Transactions in database
|
||||
|
||||
val newTransactions = findNewTransactions(response.bookedTransactions, account, userAccountTransactions)
|
||||
|
||||
val newTransactionsEntities = if (newTransactions.isNotEmpty()) {
|
||||
bankingRepository.persistTransactions(account, newTransactions)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val transactionsViewModel = updateTransactionsInUi(newTransactionsEntities)
|
||||
uiState.fireNewTransactionsRetrieved(AccountTransactionsRetrieved(user, account, transactionsViewModel))
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not save updated account transactions for user $user" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun findNewTransactions(retrievedTransactions: List<AccountTransaction>, account: BankAccountEntity, existingTransactions: List<AccountTransactionEntity>): List<AccountTransaction> {
|
||||
val existingAccountTransactions = existingTransactions.filter { it.bankAccountId == account.id }
|
||||
val accountId = account.id.toString()
|
||||
|
||||
val existingTransactionsByIdentifier = existingAccountTransactions.associateBy { getTransactionsIdentifier(it, accountId) }
|
||||
val existingTransactionsIdentifiers = existingTransactionsByIdentifier.keys
|
||||
|
||||
// val (newTransactions, duplicateTransactions) = retrievedTransactions.partition {
|
||||
// existingTransactionsIdentifiers.contains(getTransactionsIdentifier(it, accountId)) == false
|
||||
// }
|
||||
// if (duplicateTransactions.isEmpty() || newTransactions.isEmpty()) { }
|
||||
|
||||
return retrievedTransactions.filter { existingTransactionsIdentifiers.contains(getTransactionsIdentifier(it, accountId)) == false }
|
||||
}
|
||||
|
||||
private fun getTransactionsIdentifier(transaction: AccountTransaction, accountId: String? = null): String =
|
||||
with(transaction) {
|
||||
"${accountId ?: ""} $amount $currency $bookingDate $valueDate $unparsedReference $sepaReference $otherPartyName $otherPartyBankCode $otherPartyAccountId"
|
||||
}
|
||||
|
||||
private fun updateTransactionsInUi(addedTransactions: List<AccountTransactionEntity>): List<AccountTransactionViewModel> {
|
||||
val transactionsViewModel = addedTransactions.map { AccountTransactionViewModel(it) }
|
||||
|
||||
val transactions = uiState.transactions.value.toMutableList()
|
||||
transactions.addAll(transactionsViewModel)
|
||||
uiState.transactions.value = transactions.sortedByDescending { it.valueDate }
|
||||
|
||||
return transactionsViewModel
|
||||
}
|
||||
|
||||
|
||||
private fun handleUnsuccessfulBankingClientResponse(action: BankingClientAction, response: Response<*>) {
|
||||
log.error { "$action was not successful: $response" }
|
||||
|
||||
|
@ -108,6 +208,7 @@ class BankingService(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun readTransactionsFromCsv(): List<AccountTransaction> {
|
||||
val csv = Res.readBytes("files/transactions.csv").decodeToString()
|
||||
val csvReader = CsvReader(hasHeaderRow = true, reuseRowInstance = true, skipEmptyRows = true).read(csv)
|
||||
|
|
|
@ -3,15 +3,18 @@ package net.codinux.banking.ui.state
|
|||
import androidx.compose.material.DrawerState
|
||||
import androidx.compose.material.DrawerValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import net.codinux.banking.dataaccess.entities.UserAccountEntity
|
||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||
import net.codinux.banking.ui.model.AccountTransactionsFilter
|
||||
import net.codinux.banking.ui.model.BankAccountFilter
|
||||
import net.codinux.banking.ui.model.TanChallengeReceived
|
||||
import net.codinux.banking.ui.model.error.ApplicationError
|
||||
import net.codinux.banking.ui.model.error.BankingClientError
|
||||
import net.codinux.banking.ui.model.error.ErroneousAction
|
||||
import net.codinux.banking.ui.model.events.AccountTransactionsRetrieved
|
||||
|
||||
class UiState : ViewModel() {
|
||||
|
||||
|
@ -19,6 +22,16 @@ class UiState : ViewModel() {
|
|||
|
||||
val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())
|
||||
|
||||
val transactionsRetrievedEvents = MutableSharedFlow<AccountTransactionsRetrieved>()
|
||||
|
||||
suspend fun fireNewTransactionsRetrieved(event: AccountTransactionsRetrieved) {
|
||||
coroutineScope {
|
||||
launch {
|
||||
transactionsRetrievedEvents.emit(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val drawerState = MutableStateFlow(DrawerState(DrawerValue.Closed))
|
||||
|
||||
|
|
|
@ -119,4 +119,9 @@ FROM AccountTransaction;
|
|||
|
||||
selectAllTransactionsAsViewModel:
|
||||
SELECT id, userAccountId, bankAccountId, amount, currency, unparsedReference, valueDate, otherPartyName, bookingText, sepaReference, userSetDisplayName, category
|
||||
FROM AccountTransaction;
|
||||
FROM AccountTransaction;
|
||||
|
||||
|
||||
selectAllTransactionsOfUserAccount:
|
||||
SELECT AccountTransaction.*
|
||||
FROM AccountTransaction WHERE userAccountId = ?;
|
Loading…
Reference in New Issue