From b44161a45f675359b0e57201e978be4d72313032 Mon Sep 17 00:00:00 2001 From: dankito Date: Sun, 9 Aug 2020 15:13:48 +0200 Subject: [PATCH] Implemented fetching all transactions --- .../ui/android/dialogs/AddAccountDialog.kt | 2 +- .../ui/javafx/dialogs/AddAccountDialog.kt | 2 +- .../dankito/banking/ui/model/BankAccount.kt | 3 + .../banking/ui/presenter/BankingPresenter.kt | 25 +++-- .../BankingiOSApp.xcdatamodel/contents | 3 +- .../Base.lproj/Localizable.strings | 7 ++ .../de.lproj/Localizable.strings | 7 ++ .../BankingiOSApp/persistence/Mapper.swift | 4 + .../ui/views/AccountTransactionsDialog.swift | 100 +++++++++++++++--- 9 files changed, 128 insertions(+), 25 deletions(-) diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/AddAccountDialog.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/AddAccountDialog.kt index efc9e3fa..3786f087 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/AddAccountDialog.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/AddAccountDialog.kt @@ -169,7 +169,7 @@ open class AddAccountDialog : DialogFragment() { } protected open fun retrieveAccountTransactionsAndDismiss(response: AddAccountResponse, messageDialog: DialogInterface) { - presenter.fetchAccountTransactionsAsync(response.customer) { } + presenter.fetchAllAccountTransactionsAsync(response.customer) { } messageDialog.dismiss() } diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/AddAccountDialog.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/AddAccountDialog.kt index 34cd6955..e9a929f4 100755 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/AddAccountDialog.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/AddAccountDialog.kt @@ -272,7 +272,7 @@ open class AddAccountDialog(protected val presenter: BankingPresenter) : Window( val userSelection = dialogService.showDialog(Alert.AlertType.CONFIRMATION, message, null, currentStage, ButtonType.YES, ButtonType.NO) when (userSelection) { - ButtonType.YES -> presenter.fetchAccountTransactionsAsync(response.customer) { } + ButtonType.YES -> presenter.fetchAllAccountTransactionsAsync(response.customer) { } else -> { } // nothing to do then, simply close dialog } diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/BankAccount.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/BankAccount.kt index 54bb34c5..90d90057 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/BankAccount.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/BankAccount.kt @@ -40,6 +40,9 @@ open class BankAccount @JvmOverloads constructor( open var technicalId: String = UUID.random() + open var haveAllTransactionsBeenFetched: Boolean = false + + open var userSetDisplayName: String? = null open val displayName: String diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt index 2c3b4648..2db2f69b 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt @@ -167,10 +167,11 @@ open class BankingPresenter( if (response.supportsRetrievingTransactionsOfLast90DaysWithoutTan) { response.bookedTransactionsOfLast90Days.keys.forEach { bankAccount -> - retrievedAccountTransactions(startDate, GetTransactionsResponse(bankAccount, true, null, + retrievedAccountTransactions(GetTransactionsResponse(bankAccount, true, null, response.bookedTransactionsOfLast90Days[bankAccount] ?: listOf(), - response.unbookedTransactionsOfLast90Days[bankAccount] ?: listOf(), - response.balances[bankAccount]) + response.unbookedTransactionsOfLast90Days[bankAccount] ?: listOf(), + response.balances[bankAccount]), + startDate, false ) } } @@ -257,18 +258,18 @@ open class BankingPresenter( } - open fun fetchAccountTransactionsAsync(customer: Customer, - callback: (GetTransactionsResponse) -> Unit) { + open fun fetchAllAccountTransactionsAsync(customer: Customer, + callback: (GetTransactionsResponse) -> Unit) { customer.accounts.forEach { bankAccount -> if (bankAccount.supportsRetrievingAccountTransactions) { - fetchAccountTransactionsAsync(bankAccount, callback) // TODO: use a synchronous version of fetchAccountTransactions() so that all bank accounts get handled serially + fetchAllAccountTransactionsAsync(bankAccount, callback) // TODO: use a synchronous version of fetchAccountTransactions() so that all bank accounts get handled serially } } } - open fun fetchAccountTransactionsAsync(bankAccount: BankAccount, - callback: (GetTransactionsResponse) -> Unit) { + open fun fetchAllAccountTransactionsAsync(bankAccount: BankAccount, + callback: (GetTransactionsResponse) -> Unit) { fetchAccountTransactionsAsync(bankAccount, null, false, callback) } @@ -282,7 +283,7 @@ open class BankingPresenter( client.getTransactionsAsync(bankAccount, GetTransactionsParameter(true, fromDate, null, abortIfTanIsRequired, { receivedAccountsTransactionChunk(bankAccount, it) } )) { response -> if (response.tanRequiredButWeWereToldToAbortIfSo == false) { // don't call retrievedAccountTransactions() if aborted due to TAN required but we told client to abort if so - retrievedAccountTransactions(startDate, response) + retrievedAccountTransactions(response, startDate, fromDate == null) } callback(response) @@ -326,10 +327,14 @@ open class BankingPresenter( fetchAccountTransactionsAsync(bankAccount, fromDate, abortIfTanIsRequired, callback) } - protected open fun retrievedAccountTransactions(startDate: Date, response: GetTransactionsResponse) { + protected open fun retrievedAccountTransactions(response: GetTransactionsResponse, startDate: Date, didFetchAllTransactions: Boolean) { if (response.isSuccessful) { response.bankAccount.lastRetrievedTransactionsTimestamp = startDate + if (didFetchAllTransactions) { + response.bankAccount.haveAllTransactionsBeenFetched = true + } + updateAccountTransactionsAndBalances(response) } diff --git a/ui/BankingiOSApp/BankingiOSApp/BankingiOSApp.xcdatamodeld/BankingiOSApp.xcdatamodel/contents b/ui/BankingiOSApp/BankingiOSApp/BankingiOSApp.xcdatamodeld/BankingiOSApp.xcdatamodel/contents index 88fe6d96..3abd187c 100644 --- a/ui/BankingiOSApp/BankingiOSApp/BankingiOSApp.xcdatamodeld/BankingiOSApp.xcdatamodel/contents +++ b/ui/BankingiOSApp/BankingiOSApp/BankingiOSApp.xcdatamodeld/BankingiOSApp.xcdatamodel/contents @@ -42,6 +42,7 @@ + @@ -78,7 +79,7 @@ - + diff --git a/ui/BankingiOSApp/BankingiOSApp/Base.lproj/Localizable.strings b/ui/BankingiOSApp/BankingiOSApp/Base.lproj/Localizable.strings index ebc90b20..5f6f7f94 100644 --- a/ui/BankingiOSApp/BankingiOSApp/Base.lproj/Localizable.strings +++ b/ui/BankingiOSApp/BankingiOSApp/Base.lproj/Localizable.strings @@ -45,8 +45,15 @@ /* AccountTransactionsDialog */ +"Fetch all account transactions" = "Alle Umsätze holen"; "Transfer money to %@" = "Transfer money to %@"; +"Could not fetch latest transactions" = "Could not fetch latest transactions"; +"Could not fetch latest transactions for %@. Error message from your bank: %@." = "Could not fetch latest transactions for %@.\nError message from your bank:\n%@."; + +"Could not fetch all transactions" = "Could not fetch all transactions"; +"Could not fetch all transactions for %@. Error message from your bank: %@." = "Could not fetch all transactions for %@.\nError message from your bank:\n%@."; + /* New action sheet */ diff --git a/ui/BankingiOSApp/BankingiOSApp/de.lproj/Localizable.strings b/ui/BankingiOSApp/BankingiOSApp/de.lproj/Localizable.strings index 5e9891f7..94a76dd4 100644 --- a/ui/BankingiOSApp/BankingiOSApp/de.lproj/Localizable.strings +++ b/ui/BankingiOSApp/BankingiOSApp/de.lproj/Localizable.strings @@ -45,8 +45,15 @@ /* AccountTransactionsDialog */ +"Fetch all account transactions" = "Alle Umsätze holen"; "Transfer money to %@" = "Neue Überweisung an %@"; +"Could not fetch latest transactions" = "Umsätze konnte nicht aktualisiert werden"; +"Could not fetch latest transactions for %@. Error message from your bank: %@." = "Die Umsätze für %@ konnten nicht aktualisiert werden.\nFehlermeldung Ihrer Bank:\n%@."; + +"Could not fetch all transactions" = "Es konnte nicht all Umsätze geholt werden"; +"Could not fetch all transactions for %@. Error message from your bank: %@." = "Für %@ konnten nicht alle Umsätze geholt werden.\nFehlermeldung Ihrer Bank:\n%@."; + /* New action sheet */ diff --git a/ui/BankingiOSApp/BankingiOSApp/persistence/Mapper.swift b/ui/BankingiOSApp/BankingiOSApp/persistence/Mapper.swift index 58e31db7..b27fe50e 100644 --- a/ui/BankingiOSApp/BankingiOSApp/persistence/Mapper.swift +++ b/ui/BankingiOSApp/BankingiOSApp/persistence/Mapper.swift @@ -63,6 +63,8 @@ class Mapper { func map(_ customer: Customer, _ account: PersistedBankAccount) -> BankAccount { let mapped = BankAccount(customer: customer, identifier: map(account.identifier), accountHolderName: map(account.accountHolderName), iban: account.iban, subAccountNumber: account.subAccountNumber, customerId: map(account.customerId), balance: map(account.balance), currency: map(account.currency), type: map(account.type), productName: account.productName, accountLimit: account.accountLimit, lastRetrievedTransactionsTimestamp: map(account.lastRetrievedTransactionsTimestamp), supportsRetrievingAccountTransactions: account.supportsRetrievingAccountTransactions, supportsRetrievingBalance: account.supportsRetrievingBalance, supportsTransferringMoney: account.supportsTransferringMoney, supportsInstantPaymentMoneyTransfer: account.supportsInstantPaymentMoneyTransfer, bookedTransactions: [], unbookedTransactions: []) + mapped.haveAllTransactionsBeenFetched = account.haveAllTransactionsBeenFetched + mapped.userSetDisplayName = account.userSetDisplayName mapped.bookedTransactions = map(mapped, account.transactions as? Set) @@ -96,6 +98,8 @@ class Mapper { mapped.supportsTransferringMoney = account.supportsTransferringMoney mapped.supportsInstantPaymentMoneyTransfer = account.supportsInstantPaymentMoneyTransfer + mapped.haveAllTransactionsBeenFetched = account.haveAllTransactionsBeenFetched + mapped.userSetDisplayName = account.userSetDisplayName mapped.transactions = NSSet(array: map(mapped, account.bookedTransactions, context)) diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/views/AccountTransactionsDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/views/AccountTransactionsDialog.swift index 936b1787..7cc70172 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/views/AccountTransactionsDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/views/AccountTransactionsDialog.swift @@ -13,6 +13,13 @@ struct AccountTransactionsDialog: View { private let areMoreThanOneBanksTransactionsDisplayed: Bool + @State private var haveAllTransactionsBeenFetched: Bool + + @State private var showFetchAllTransactionsOverlay: Bool + + @State private var accountsForWhichNotAllTransactionsHaveBeenFetched: [BankAccount] + + @State private var filteredTransactions: [AccountTransaction] @State private var balanceOfFilteredTransactions: CommonBigDecimal @@ -29,28 +36,33 @@ struct AccountTransactionsDialog: View { } + @State private var errorMessage: Message? = nil + + @Inject private var presenter: BankingPresenterSwift init(allBanks: [Customer]) { - self.init(title: "All accounts", transactions: allBanks.flatMap { $0.accounts }.flatMap { $0.bookedTransactions }, balance: allBanks.sumBalances()) + let allAccounts = allBanks.flatMap { $0.accounts } + + self.init("All accounts", allAccounts.flatMap { $0.bookedTransactions }, allBanks.sumBalances(), allAccounts.filter { $0.haveAllTransactionsBeenFetched == false }) presenter.selectedAllBankAccounts() } init(bank: Customer) { - self.init(title: bank.displayName, transactions: bank.accounts.flatMap { $0.bookedTransactions }, balance: bank.balance) + self.init(bank.displayName, bank.accounts.flatMap { $0.bookedTransactions }, bank.balance, bank.accounts.filter { $0.haveAllTransactionsBeenFetched == false }) presenter.selectedAccount(customer: bank) } init(account: BankAccount) { - self.init(title: account.displayName, transactions: account.bookedTransactions, balance: account.balance) + self.init(account.displayName, account.bookedTransactions, account.balance, account.haveAllTransactionsBeenFetched ? [] : [account]) presenter.selectedBankAccount(bankAccount: account) } - fileprivate init(title: String, transactions: [AccountTransaction], balance: CommonBigDecimal) { + fileprivate init(_ title: String, _ transactions: [AccountTransaction], _ balance: CommonBigDecimal, _ accountsForWhichNotAllTransactionsHaveBeenFetched: [BankAccount] = []) { self.title = title self.allTransactions = transactions @@ -60,6 +72,10 @@ struct AccountTransactionsDialog: View { self._balanceOfFilteredTransactions = State(initialValue: balance) self.areMoreThanOneBanksTransactionsDisplayed = Set(allTransactions.compactMap { $0.bankAccount }.compactMap { $0.customer }).count > 1 + + _accountsForWhichNotAllTransactionsHaveBeenFetched = State(initialValue: accountsForWhichNotAllTransactionsHaveBeenFetched) + _haveAllTransactionsBeenFetched = State(initialValue: accountsForWhichNotAllTransactionsHaveBeenFetched.isEmpty) + _showFetchAllTransactionsOverlay = State(initialValue: accountsForWhichNotAllTransactionsHaveBeenFetched.isNotEmpty) } @@ -77,26 +93,86 @@ struct AccountTransactionsDialog: View { } .padding(.horizontal) - List(filteredTransactions.sorted(by: { $0.valueDate.date > $1.valueDate.date } ), id: \.technicalId) { transaction in - AccountTransactionListItem(transaction, self.areMoreThanOneBanksTransactionsDisplayed) + Spacer() + + List { + ForEach(filteredTransactions.sorted(by: { $0.valueDate.date > $1.valueDate.date } ), id: \.technicalId) { transaction in + AccountTransactionListItem(transaction, self.areMoreThanOneBanksTransactionsDisplayed) + } + + if haveAllTransactionsBeenFetched == false { + Spacer() + + HStack(alignment: .center) { + Spacer() + + Button("Fetch all account transactions") { + self.fetchAllTransactions(self.accountsForWhichNotAllTransactionsHaveBeenFetched) + } + + Spacer() + } + .frame(height: 35) + } + } + + Spacer() + + if showFetchAllTransactionsOverlay { + HStack(alignment: .center) { + Button("x") { + self.showFetchAllTransactionsOverlay = false + } + + Spacer() + + Button("Fetch all account transactions") { + self.fetchAllTransactions(self.accountsForWhichNotAllTransactionsHaveBeenFetched) + } + + Spacer() + } + .frame(height: 35) + .padding(.top, 8) + .padding(.horizontal, 6) + .background(Color(UIColor.systemGroupedBackground)) } } + .alert(item: $errorMessage) { message in + Alert(title: message.title, message: message.message, dismissButton: message.primaryButton) + } .showNavigationBarTitle(LocalizedStringKey(title)) - .navigationBarItems(trailing: UpdateButton { _ in self.retrieveTransactions() }) + .navigationBarItems(trailing: UpdateButton { _ in self.updateTransactions() }) } - private func retrieveTransactions() { + private func updateTransactions() { presenter.updateSelectedBankAccountTransactionsAsync { response in if response.isSuccessful { self.filterTransactions(self.searchText) } else if response.userCancelledAction == false { - // TODO: show updating transactions failed message + self.errorMessage = Message(title: Text("Could not fetch latest transactions"), message: Text("Could not fetch latest transactions for \(response.bankAccount.displayName). Error message from your bank: \(response.errorToShowToUser ?? "").")) } } } + private func fetchAllTransactions(_ accounts: [BankAccount]) { + accounts.forEach { account in + presenter.fetchAllAccountTransactionsAsync(bankAccount: account, callback: self.handleGetAllTransactionsResult) + } + } + + private func handleGetAllTransactionsResult(_ response: GetTransactionsResponse) { + self.accountsForWhichNotAllTransactionsHaveBeenFetched = self.accountsForWhichNotAllTransactionsHaveBeenFetched.filter { $0.haveAllTransactionsBeenFetched == false } + self.haveAllTransactionsBeenFetched = self.accountsForWhichNotAllTransactionsHaveBeenFetched.isEmpty + self.showFetchAllTransactionsOverlay = self.accountsForWhichNotAllTransactionsHaveBeenFetched.isNotEmpty + + if response.isSuccessful == false { + self.errorMessage = Message(title: Text("Could not fetch all transactions"), message: Text("Could not fetch all transactions for \(response.bankAccount.displayName). Error message from your bank: \(response.errorToShowToUser ?? "").")) + } + } + private func filterTransactions(_ query: String) { self.filteredTransactions = presenter.searchSelectedAccountTransactions(query: query) @@ -107,9 +183,9 @@ struct AccountTransactionsDialog: View { struct AccountTransactionsDialog_Previews: PreviewProvider { static var previews: some View { - AccountTransactionsDialog(title: previewBanks[0].displayName, transactions: [ + AccountTransactionsDialog(previewBanks[0].displayName, [ AccountTransaction(bankAccount: previewBanks[0].accounts[0], amount: CommonBigDecimal(double: 1234.56), currency: "€", unparsedUsage: "Usage", bookingDate: CommonDate(year: 2020, month: 5, day: 7), otherPartyName: "Marieke Musterfrau", otherPartyBankCode: nil, otherPartyAccountId: nil, bookingText: "SEPA Ueberweisung", valueDate: CommonDate(year: 2020, month: 5, day: 7)) - ], - balance: CommonBigDecimal(double: 84.12)) + ], + CommonBigDecimal(double: 84.12)) } }