From e749a88c83e819b2edab1026aa20d7d555a1222a Mon Sep 17 00:00:00 2001 From: dankito Date: Thu, 21 May 2020 00:29:53 +0200 Subject: [PATCH] Implemented parsing each received MT940 part immediately and passing it on to UI --- .../net/dankito/banking/fints/FinTsClient.kt | 55 ++++++++++--------- .../fints/messages/MessageBuilderResult.kt | 7 +++ .../banking/fints/model/DialogContext.kt | 3 +- .../fints/model/GetTransactionsParameter.kt | 3 +- .../IAccountTransactionsParser.kt | 2 + .../Mt940AccountTransactionsParser.kt | 6 ++ .../fints/transactions/mt940/IMt940Parser.kt | 2 + .../fints/transactions/mt940/Mt940Parser.kt | 17 ++++++ .../parameters/GetTransactionsParameter.kt | 4 +- .../banking/ui/presenter/BankingPresenter.kt | 10 +++- .../dankito/banking/fints4kBankingClient.kt | 3 +- 11 files changed, 80 insertions(+), 32 deletions(-) diff --git a/fints4k/src/main/kotlin/net/dankito/banking/fints/FinTsClient.kt b/fints4k/src/main/kotlin/net/dankito/banking/fints/FinTsClient.kt index f51b8273..fc784706 100644 --- a/fints4k/src/main/kotlin/net/dankito/banking/fints/FinTsClient.kt +++ b/fints4k/src/main/kotlin/net/dankito/banking/fints/FinTsClient.kt @@ -307,39 +307,35 @@ open class FinTsClient @JvmOverloads constructor( val message = messageBuilder.createGetTransactionsMessage(parameter, account, dialogContext) + val bookedTransactions = mutableListOf() + var remainingMt940String = "" + + dialogContext.chunkedResponseHandler = { response -> + response.getFirstSegmentById(InstituteSegmentId.AccountTransactionsMt940)?.let { transactionsSegment -> + val (chunkTransaction, remainder) = mt940Parser.parseTransactionsChunk(remainingMt940String + transactionsSegment.bookedTransactionsString, account) + + bookedTransactions.addAll(chunkTransaction) + remainingMt940String = remainder + + parameter.retrievedChunkListener?.invoke(bookedTransactions) + } + } + val response = getAndHandleResponseForMessage(message, dialogContext) closeDialog(dialogContext) - response.getFirstSegmentById(InstituteSegmentId.AccountTransactionsMt940)?.let { transactions -> - // just retrieved all transactions -> check if retrieving that ones of last 90 days is possible without entering TAN - if (account.supportsRetrievingTransactionsOfLast90DaysWithoutTan == null && - response.successful && transactions.bookedTransactionsString.isNotEmpty() && parameter.fromDate == null) { - tryGetTransactionsOfLast90DaysWithoutTan(bank, customer, account, true) - } - - val bookedAndUnbookedTransactions = getTransactionsFromResponse(response, transactions, account) - - return GetTransactionsResponse(response, - bookedAndUnbookedTransactions.first.sortedByDescending { it.bookingDate }, - bookedAndUnbookedTransactions.second, - balance) + // just retrieved all transactions -> check if retrieving that ones of last 90 days is possible without entering TAN + if (account.supportsRetrievingTransactionsOfLast90DaysWithoutTan == null && + response.successful && bookedTransactions.isNotEmpty() && parameter.fromDate == null) { + tryGetTransactionsOfLast90DaysWithoutTan(bank, customer, account, true) } - return GetTransactionsResponse(response) - } - - protected open fun getTransactionsFromResponse(response: Response, transactions: ReceivedAccountTransactions, account: AccountData): Pair, List> { - val bookedTransactionsString = StringBuilder() - val unbookedTransactionsString = StringBuilder() - - getTransactionsFromResponse(response, transactions, bookedTransactionsString, unbookedTransactionsString) - - val bookedTransactions = mt940Parser.parseTransactions(bookedTransactionsString.toString(), account) - val unbookedTransactions = listOf() // TODO: implement parsing MT942 - - return Pair(bookedTransactions, unbookedTransactions) + return GetTransactionsResponse(response, + bookedTransactions.sortedByDescending { it.bookingDate }, + listOf(), // TODO: implement parsing MT942 + balance) } protected open fun getTransactionsFromResponse(response: Response, transactions: ReceivedAccountTransactions, @@ -561,11 +557,16 @@ open class FinTsClient @JvmOverloads constructor( // if there's a Aufsetzpunkt (continuationId) set, then response is not complete yet, there's more information to fetch by sending this Aufsetzpunkt handledResponse.aufsetzpunkt?.let { continuationId -> if (handledResponse.followUpResponse == null) { // for re-sent messages followUpResponse is already set and dialog already closed -> would be overwritten with an error response that dialog is closed + if (message.isSendEnteredTanMessage() == false) { // for sending TAN no follow up message can be created -> filter out, otherwise chunkedResponseHandler would get called twice for same response + dialogContext.chunkedResponseHandler?.invoke(handledResponse) + } + handledResponse.followUpResponse = getFollowUpMessageForContinuationId(handledResponse, continuationId, message, dialogContext) handledResponse.hasFollowUpMessageButCouldNotReceiveIt = handledResponse.followUpResponse == null } } + ?: run { dialogContext.chunkedResponseHandler?.invoke(handledResponse) } return handledResponse } @@ -794,7 +795,7 @@ open class FinTsClient @JvmOverloads constructor( protected open fun resendMessageInNewDialog(lastCreatedMessage: MessageBuilderResult?, previousDialogContext: DialogContext): Response { lastCreatedMessage?.let { // do not use previousDialogContext.currentMessage as this may is previous dialog's dialog close message - val newDialogContext = DialogContext(previousDialogContext.bank, previousDialogContext.customer, previousDialogContext.product) + val newDialogContext = DialogContext(previousDialogContext.bank, previousDialogContext.customer, previousDialogContext.product, chunkedResponseHandler = previousDialogContext.chunkedResponseHandler) val initDialogResponse = initDialog(newDialogContext) if (initDialogResponse.successful == false) { diff --git a/fints4k/src/main/kotlin/net/dankito/banking/fints/messages/MessageBuilderResult.kt b/fints4k/src/main/kotlin/net/dankito/banking/fints/messages/MessageBuilderResult.kt index eeb7d342..a6497ce6 100644 --- a/fints4k/src/main/kotlin/net/dankito/banking/fints/messages/MessageBuilderResult.kt +++ b/fints4k/src/main/kotlin/net/dankito/banking/fints/messages/MessageBuilderResult.kt @@ -1,6 +1,7 @@ package net.dankito.banking.fints.messages import net.dankito.banking.fints.messages.segmente.Segment +import net.dankito.banking.fints.messages.segmente.implementierte.ZweiSchrittTanEinreichung open class MessageBuilderResult( @@ -25,4 +26,10 @@ open class MessageBuilderResult( open val getHighestAllowedVersion: Int? get() = allowedVersions.max() + open fun isSendEnteredTanMessage(): Boolean { + // contains only a ZweiSchrittTanEinreichung segment + return messageBodySegments.size == 1 + && messageBodySegments.first() is ZweiSchrittTanEinreichung + } + } \ No newline at end of file diff --git a/fints4k/src/main/kotlin/net/dankito/banking/fints/model/DialogContext.kt b/fints4k/src/main/kotlin/net/dankito/banking/fints/model/DialogContext.kt index 7a97ab7a..b42d9e0e 100644 --- a/fints4k/src/main/kotlin/net/dankito/banking/fints/model/DialogContext.kt +++ b/fints4k/src/main/kotlin/net/dankito/banking/fints/model/DialogContext.kt @@ -12,7 +12,8 @@ open class DialogContext( var dialogId: String = InitialDialogId, var response: Response? = null, var didBankCloseDialog: Boolean = false, - var previousMessageInDialog: MessageBuilderResult? = null + var previousMessageInDialog: MessageBuilderResult? = null, + var chunkedResponseHandler: ((Response) -> Unit)? = null ) : MessageBaseData(bank, customer, product) { companion object { diff --git a/fints4k/src/main/kotlin/net/dankito/banking/fints/model/GetTransactionsParameter.kt b/fints4k/src/main/kotlin/net/dankito/banking/fints/model/GetTransactionsParameter.kt index 69b7d1a4..ea9f2315 100644 --- a/fints4k/src/main/kotlin/net/dankito/banking/fints/model/GetTransactionsParameter.kt +++ b/fints4k/src/main/kotlin/net/dankito/banking/fints/model/GetTransactionsParameter.kt @@ -7,5 +7,6 @@ open class GetTransactionsParameter @JvmOverloads constructor( val alsoRetrieveBalance: Boolean = true, val fromDate: Date? = null, val toDate: Date? = null, - val maxCountEntries: Int? = null + val maxCountEntries: Int? = null, + val retrievedChunkListener: ((List) -> Unit)? = null ) \ No newline at end of file diff --git a/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/IAccountTransactionsParser.kt b/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/IAccountTransactionsParser.kt index 50a5419c..83e6fabc 100644 --- a/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/IAccountTransactionsParser.kt +++ b/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/IAccountTransactionsParser.kt @@ -8,4 +8,6 @@ interface IAccountTransactionsParser { fun parseTransactions(transactionsString: String, account: AccountData): List + fun parseTransactionsChunk(transactionsChunk: String, account: AccountData): Pair, String> + } \ No newline at end of file diff --git a/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/Mt940AccountTransactionsParser.kt b/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/Mt940AccountTransactionsParser.kt index 75e7e5bd..b1830d2e 100644 --- a/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/Mt940AccountTransactionsParser.kt +++ b/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/Mt940AccountTransactionsParser.kt @@ -27,6 +27,12 @@ open class Mt940AccountTransactionsParser @JvmOverloads constructor( return accountStatements.flatMap { mapToAccountTransactions(it, account) } } + override fun parseTransactionsChunk(transactionsChunk: String, account: AccountData): Pair, String> { + val (accountStatements, remainder) = mt940Parser.parseTransactionsChunk(transactionsChunk) + + return Pair(accountStatements.flatMap { mapToAccountTransactions(it, account) }, remainder) + } + protected open fun mapToAccountTransactions(statement: AccountStatement, account: AccountData): List { try { return statement.transactions.map { mapToAccountTransaction(statement, it, account) } diff --git a/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/mt940/IMt940Parser.kt b/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/mt940/IMt940Parser.kt index 31f6c8d0..c5aaad4b 100644 --- a/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/mt940/IMt940Parser.kt +++ b/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/mt940/IMt940Parser.kt @@ -7,4 +7,6 @@ interface IMt940Parser { fun parseMt940String(mt940String: String): List + fun parseTransactionsChunk(mt940Chunk: String): Pair, String> + } \ No newline at end of file diff --git a/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/mt940/Mt940Parser.kt b/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/mt940/Mt940Parser.kt index 70bb1df7..98dafa19 100644 --- a/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/mt940/Mt940Parser.kt +++ b/fints4k/src/main/kotlin/net/dankito/banking/fints/transactions/mt940/Mt940Parser.kt @@ -73,6 +73,23 @@ open class Mt940Parser : IMt940Parser { return listOf() } + override fun parseTransactionsChunk(mt940Chunk: String): Pair, String> { + try { + val singleAccountStatementsStrings = splitIntoSingleAccountStatements(mt940Chunk) + + val transactions = singleAccountStatementsStrings.mapNotNull { parseAccountStatement(it) } + + val remainder = if (singleAccountStatementsStrings.size == transactions.size + 1) singleAccountStatementsStrings.last() + else "" + + return Pair(transactions, remainder) + } catch (e: Exception) { + log.error("Could not parse account statements from MT940 string:\n$mt940Chunk", e) + } + + return Pair(listOf(), "") + } + protected open fun splitIntoSingleAccountStatements(mt940String: String): List { return mt940String.split(AccountStatementsSeparatorPattern) diff --git a/ui/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/parameters/GetTransactionsParameter.kt b/ui/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/parameters/GetTransactionsParameter.kt index 8219dc0d..c86683e9 100644 --- a/ui/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/parameters/GetTransactionsParameter.kt +++ b/ui/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/parameters/GetTransactionsParameter.kt @@ -1,12 +1,14 @@ package net.dankito.banking.ui.model.parameters +import net.dankito.banking.ui.model.AccountTransaction import java.util.* open class GetTransactionsParameter( val alsoRetrieveBalance: Boolean = true, val fromDate: Date? = null, - val toDate: Date? = null + val toDate: Date? = null, + val retrievedChunkListener: ((List) -> Unit)? = null ) { constructor() : this(true, null, null) // for Java diff --git a/ui/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/BankingPresenter.kt b/ui/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/BankingPresenter.kt index 942f3fe4..ef9024ce 100644 --- a/ui/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/BankingPresenter.kt +++ b/ui/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/BankingPresenter.kt @@ -268,7 +268,7 @@ open class BankingPresenter( getClientForAccount(bankAccount.account)?.let { client -> val startDate = Date() - client.getTransactionsAsync(bankAccount, net.dankito.banking.ui.model.parameters.GetTransactionsParameter(true, fromDate)) { response -> + client.getTransactionsAsync(bankAccount, net.dankito.banking.ui.model.parameters.GetTransactionsParameter(true, fromDate, null, { receivedAccountsTransactionChunk(bankAccount, it) } )) { response -> retrievedAccountTransactions(bankAccount, startDate, response) @@ -299,6 +299,14 @@ open class BankingPresenter( callRetrievedAccountTransactionsResponseListener(bankAccount, response) } + protected open fun receivedAccountsTransactionChunk(bankAccount: BankAccount, accountTransactionsChunk: List) { + if (accountTransactionsChunk.isNotEmpty()) { + bankAccount.addBookedTransactions(accountTransactionsChunk) + + callRetrievedAccountTransactionsResponseListener(bankAccount, GetTransactionsResponse(true, null, mapOf(bankAccount to accountTransactionsChunk))) + } + } + protected open fun updateAccountTransactionsAndBalances(bankAccount: BankAccount, response: GetTransactionsResponse) { response.bookedTransactions.forEach { entry -> diff --git a/ui/fints4kBankingClient/src/main/kotlin/net/dankito/banking/fints4kBankingClient.kt b/ui/fints4kBankingClient/src/main/kotlin/net/dankito/banking/fints4kBankingClient.kt index 6b7f359a..d8f64d1f 100644 --- a/ui/fints4kBankingClient/src/main/kotlin/net/dankito/banking/fints4kBankingClient.kt +++ b/ui/fints4kBankingClient/src/main/kotlin/net/dankito/banking/fints4kBankingClient.kt @@ -106,7 +106,8 @@ open class fints4kBankingClient( callback(GetTransactionsResponse(false, "Cannot find account for ${bankAccount.identifier}")) // TODO: translate } else { - client.getTransactionsAsync(net.dankito.banking.fints.model.GetTransactionsParameter(parameter.alsoRetrieveBalance, parameter.fromDate, parameter.toDate), account) { response -> + client.getTransactionsAsync(net.dankito.banking.fints.model.GetTransactionsParameter(parameter.alsoRetrieveBalance, parameter.fromDate, parameter.toDate, null, + { parameter.retrievedChunkListener?.invoke(mapper.mapTransactions(bankAccount, it)) } ), account) { response -> val mappedResponse = mapper.mapResponse(bankAccount, response)