Implemented fetching all transactions

This commit is contained in:
dankito 2020-08-09 15:13:48 +02:00
parent bab012a9eb
commit b44161a45f
9 changed files with 128 additions and 25 deletions

View File

@ -169,7 +169,7 @@ open class AddAccountDialog : DialogFragment() {
} }
protected open fun retrieveAccountTransactionsAndDismiss(response: AddAccountResponse, messageDialog: DialogInterface) { protected open fun retrieveAccountTransactionsAndDismiss(response: AddAccountResponse, messageDialog: DialogInterface) {
presenter.fetchAccountTransactionsAsync(response.customer) { } presenter.fetchAllAccountTransactionsAsync(response.customer) { }
messageDialog.dismiss() messageDialog.dismiss()
} }

View File

@ -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) val userSelection = dialogService.showDialog(Alert.AlertType.CONFIRMATION, message, null, currentStage, ButtonType.YES, ButtonType.NO)
when (userSelection) { when (userSelection) {
ButtonType.YES -> presenter.fetchAccountTransactionsAsync(response.customer) { } ButtonType.YES -> presenter.fetchAllAccountTransactionsAsync(response.customer) { }
else -> { } // nothing to do then, simply close dialog else -> { } // nothing to do then, simply close dialog
} }

View File

@ -40,6 +40,9 @@ open class BankAccount @JvmOverloads constructor(
open var technicalId: String = UUID.random() open var technicalId: String = UUID.random()
open var haveAllTransactionsBeenFetched: Boolean = false
open var userSetDisplayName: String? = null open var userSetDisplayName: String? = null
open val displayName: String open val displayName: String

View File

@ -167,10 +167,11 @@ open class BankingPresenter(
if (response.supportsRetrievingTransactionsOfLast90DaysWithoutTan) { if (response.supportsRetrievingTransactionsOfLast90DaysWithoutTan) {
response.bookedTransactionsOfLast90Days.keys.forEach { bankAccount -> response.bookedTransactionsOfLast90Days.keys.forEach { bankAccount ->
retrievedAccountTransactions(startDate, GetTransactionsResponse(bankAccount, true, null, retrievedAccountTransactions(GetTransactionsResponse(bankAccount, true, null,
response.bookedTransactionsOfLast90Days[bankAccount] ?: listOf(), response.bookedTransactionsOfLast90Days[bankAccount] ?: listOf(),
response.unbookedTransactionsOfLast90Days[bankAccount] ?: listOf(), response.unbookedTransactionsOfLast90Days[bankAccount] ?: listOf(),
response.balances[bankAccount]) response.balances[bankAccount]),
startDate, false
) )
} }
} }
@ -257,17 +258,17 @@ open class BankingPresenter(
} }
open fun fetchAccountTransactionsAsync(customer: Customer, open fun fetchAllAccountTransactionsAsync(customer: Customer,
callback: (GetTransactionsResponse) -> Unit) { callback: (GetTransactionsResponse) -> Unit) {
customer.accounts.forEach { bankAccount -> customer.accounts.forEach { bankAccount ->
if (bankAccount.supportsRetrievingAccountTransactions) { 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, open fun fetchAllAccountTransactionsAsync(bankAccount: BankAccount,
callback: (GetTransactionsResponse) -> Unit) { callback: (GetTransactionsResponse) -> Unit) {
fetchAccountTransactionsAsync(bankAccount, null, false, callback) fetchAccountTransactionsAsync(bankAccount, null, false, callback)
@ -282,7 +283,7 @@ open class BankingPresenter(
client.getTransactionsAsync(bankAccount, GetTransactionsParameter(true, fromDate, null, abortIfTanIsRequired, { receivedAccountsTransactionChunk(bankAccount, it) } )) { response -> 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 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) callback(response)
@ -326,10 +327,14 @@ open class BankingPresenter(
fetchAccountTransactionsAsync(bankAccount, fromDate, abortIfTanIsRequired, callback) 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) { if (response.isSuccessful) {
response.bankAccount.lastRetrievedTransactionsTimestamp = startDate response.bankAccount.lastRetrievedTransactionsTimestamp = startDate
if (didFetchAllTransactions) {
response.bankAccount.haveAllTransactionsBeenFetched = true
}
updateAccountTransactionsAndBalances(response) updateAccountTransactionsAndBalances(response)
} }

View File

@ -42,6 +42,7 @@
<attribute name="balance" attributeType="Decimal" defaultValueString="0.0"/> <attribute name="balance" attributeType="Decimal" defaultValueString="0.0"/>
<attribute name="currency" attributeType="String"/> <attribute name="currency" attributeType="String"/>
<attribute name="customerId" attributeType="String"/> <attribute name="customerId" attributeType="String"/>
<attribute name="haveAllTransactionsBeenFetched" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="iban" optional="YES" attributeType="String"/> <attribute name="iban" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/> <attribute name="identifier" attributeType="String"/>
<attribute name="lastRetrievedTransactionsTimestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="lastRetrievedTransactionsTimestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@ -78,7 +79,7 @@
</entity> </entity>
<elements> <elements>
<element name="PersistedAccountTransaction" positionX="-36" positionY="45" width="128" height="553"/> <element name="PersistedAccountTransaction" positionX="-36" positionY="45" width="128" height="553"/>
<element name="PersistedBankAccount" positionX="-54" positionY="63" width="128" height="328"/> <element name="PersistedBankAccount" positionX="-54" positionY="63" width="128" height="343"/>
<element name="PersistedCustomer" positionX="-63" positionY="-18" width="128" height="253"/> <element name="PersistedCustomer" positionX="-63" positionY="-18" width="128" height="253"/>
<element name="PersistedTanProcedure" positionX="-54" positionY="135" width="128" height="88"/> <element name="PersistedTanProcedure" positionX="-54" positionY="135" width="128" height="88"/>
</elements> </elements>

View File

@ -45,8 +45,15 @@
/* AccountTransactionsDialog */ /* AccountTransactionsDialog */
"Fetch all account transactions" = "Alle Umsätze holen";
"Transfer money to %@" = "Transfer money to %@"; "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 */ /* New action sheet */

View File

@ -45,8 +45,15 @@
/* AccountTransactionsDialog */ /* AccountTransactionsDialog */
"Fetch all account transactions" = "Alle Umsätze holen";
"Transfer money to %@" = "Neue Überweisung an %@"; "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 */ /* New action sheet */

View File

@ -63,6 +63,8 @@ class Mapper {
func map(_ customer: Customer, _ account: PersistedBankAccount) -> BankAccount { 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: []) 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.userSetDisplayName = account.userSetDisplayName
mapped.bookedTransactions = map(mapped, account.transactions as? Set<PersistedAccountTransaction>) mapped.bookedTransactions = map(mapped, account.transactions as? Set<PersistedAccountTransaction>)
@ -96,6 +98,8 @@ class Mapper {
mapped.supportsTransferringMoney = account.supportsTransferringMoney mapped.supportsTransferringMoney = account.supportsTransferringMoney
mapped.supportsInstantPaymentMoneyTransfer = account.supportsInstantPaymentMoneyTransfer mapped.supportsInstantPaymentMoneyTransfer = account.supportsInstantPaymentMoneyTransfer
mapped.haveAllTransactionsBeenFetched = account.haveAllTransactionsBeenFetched
mapped.userSetDisplayName = account.userSetDisplayName mapped.userSetDisplayName = account.userSetDisplayName
mapped.transactions = NSSet(array: map(mapped, account.bookedTransactions, context)) mapped.transactions = NSSet(array: map(mapped, account.bookedTransactions, context))

View File

@ -13,6 +13,13 @@ struct AccountTransactionsDialog: View {
private let areMoreThanOneBanksTransactionsDisplayed: Bool 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 filteredTransactions: [AccountTransaction]
@State private var balanceOfFilteredTransactions: CommonBigDecimal @State private var balanceOfFilteredTransactions: CommonBigDecimal
@ -29,28 +36,33 @@ struct AccountTransactionsDialog: View {
} }
@State private var errorMessage: Message? = nil
@Inject private var presenter: BankingPresenterSwift @Inject private var presenter: BankingPresenterSwift
init(allBanks: [Customer]) { 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() presenter.selectedAllBankAccounts()
} }
init(bank: Customer) { 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) presenter.selectedAccount(customer: bank)
} }
init(account: BankAccount) { 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) 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.title = title
self.allTransactions = transactions self.allTransactions = transactions
@ -60,6 +72,10 @@ struct AccountTransactionsDialog: View {
self._balanceOfFilteredTransactions = State(initialValue: balance) self._balanceOfFilteredTransactions = State(initialValue: balance)
self.areMoreThanOneBanksTransactionsDisplayed = Set(allTransactions.compactMap { $0.bankAccount }.compactMap { $0.customer }).count > 1 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) .padding(.horizontal)
List(filteredTransactions.sorted(by: { $0.valueDate.date > $1.valueDate.date } ), id: \.technicalId) { transaction in Spacer()
List {
ForEach(filteredTransactions.sorted(by: { $0.valueDate.date > $1.valueDate.date } ), id: \.technicalId) { transaction in
AccountTransactionListItem(transaction, self.areMoreThanOneBanksTransactionsDisplayed) 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)) .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 presenter.updateSelectedBankAccountTransactionsAsync { response in
if response.isSuccessful { if response.isSuccessful {
self.filterTransactions(self.searchText) self.filterTransactions(self.searchText)
} }
else if response.userCancelledAction == false { 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) { private func filterTransactions(_ query: String) {
self.filteredTransactions = presenter.searchSelectedAccountTransactions(query: query) self.filteredTransactions = presenter.searchSelectedAccountTransactions(query: query)
@ -107,9 +183,9 @@ struct AccountTransactionsDialog: View {
struct AccountTransactionsDialog_Previews: PreviewProvider { struct AccountTransactionsDialog_Previews: PreviewProvider {
static var previews: some View { 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)) 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))
} }
} }