From a42de32260cf4167f4851abb0523c7efd9ba4752 Mon Sep 17 00:00:00 2001 From: dankito Date: Wed, 11 Sep 2024 06:39:59 +0200 Subject: [PATCH] Implemented requesting and parsing securities account balance --- .../codinux/banking/fints/FinTsJobExecutor.kt | 28 +++++++++++++- .../banking/fints/mapper/FinTsModelMapper.kt | 2 + .../banking/fints/messages/MessageBuilder.kt | 22 +++++++++++ .../messages/segmente/id/CustomerSegmentId.kt | 7 +++- .../implementierte/depot/Depotaufstellung.kt | 37 +++++++++++++++++++ .../banking/fints/model/MessageType.kt | 2 + .../fints/model/RetrievedAccountData.kt | 4 +- .../fints/response/InstituteSegmentId.kt | 7 +++- .../banking/fints/response/ResponseParser.kt | 16 +++++++- .../SecuritiesAccountBalanceSegment.kt | 9 +++++ .../fints/transactions/swift/model/Holding.kt | 2 + .../swift/model/StatementOfHoldings.kt | 4 +- .../banking/client/model/BankAccount.kt | 3 ++ .../transactions/swift/Mt535ParserTest.kt | 5 +-- 14 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/segmente/implementierte/depot/Depotaufstellung.kt create mode 100644 fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/segments/SecuritiesAccountBalanceSegment.kt diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/FinTsJobExecutor.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/FinTsJobExecutor.kt index cc401688..7c134570 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/FinTsJobExecutor.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/FinTsJobExecutor.kt @@ -202,6 +202,29 @@ open class FinTsJobExecutor( var balance: Money? = balanceResponse?.getFirstSegmentById(InstituteSegmentId.Balance)?.let { Money(it.balance, it.currency) } + + // TODO: for larger portfolios there can be a Aufsetzpunkt, but for balances we currently do not support sending multiple messages + val statementOfHoldings = balanceResponse?.getFirstSegmentById(InstituteSegmentId.SecuritiesAccountBalance)?.let { + val statementOfHoldings = it.statementOfHoldings + val statementOfHolding = statementOfHoldings.firstOrNull { it.totalBalance != null } + if (statementOfHolding != null) { + balance = Money(statementOfHolding.totalBalance!!, statementOfHolding.currency ?: Currency.DefaultCurrencyCode) + } + statementOfHoldings + } ?: emptyList() + + if (parameter.account.supportsRetrievingAccountTransactions == false) { + if (balanceResponse == null) { + return GetAccountTransactionsResponse(context, BankResponse(false, "Balance could not be retrieved"), RetrievedAccountData.unsuccessful(parameter.account)) + } else { + val successful = balance != null || balanceResponse.tanRequiredButWeWereToldToAbortIfSo + val retrievedData = RetrievedAccountData(parameter.account, successful, balance, emptyList(), emptyList(), statementOfHoldings, Instant.nowExt(), null, null, balanceResponse?.internalError) + + return GetAccountTransactionsResponse(context, balanceResponse, retrievedData) + } + } + + val bookedTransactions = mutableSetOf() val unbookedTransactions = mutableSetOf() @@ -235,11 +258,12 @@ open class FinTsJobExecutor( closeDialog(context) val successful = response.tanRequiredButWeWereToldToAbortIfSo - || (response.successful && (parameter.alsoRetrieveBalance == false || balance != null)) + || (response.successful && (parameter.alsoRetrieveBalance == false || balance != null)) + || (parameter.account.supportsRetrievingAccountTransactions == false && balance != null) val fromDate = parameter.fromDate ?: parameter.account.serverTransactionsRetentionDays?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) } ?: bookedTransactions.minByOrNull { it.valueDate }?.valueDate - val retrievedData = RetrievedAccountData(parameter.account, successful, balance, bookedTransactions, unbookedTransactions, startTime, fromDate, parameter.toDate ?: LocalDate.todayAtEuropeBerlin(), response.internalError) + val retrievedData = RetrievedAccountData(parameter.account, successful, balance, bookedTransactions, unbookedTransactions, statementOfHoldings, startTime, fromDate, parameter.toDate ?: LocalDate.todayAtEuropeBerlin(), response.internalError) return GetAccountTransactionsResponse(context, response, retrievedData, if (parameter.maxCountEntries != null) parameter.isSettingMaxCountEntriesAllowedByBank else null) diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/mapper/FinTsModelMapper.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/mapper/FinTsModelMapper.kt index 0c44f91b..feede22c 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/mapper/FinTsModelMapper.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/mapper/FinTsModelMapper.kt @@ -103,6 +103,8 @@ open class FinTsModelMapper { addAll(map(accountTransactionsResponse)) } } + + bankAccount.statementOfHoldings = accountTransactionsResponse.statementOfHoldings } } diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/MessageBuilder.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/MessageBuilder.kt index d9daca09..1b287470 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/MessageBuilder.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/MessageBuilder.kt @@ -13,6 +13,7 @@ import net.codinux.banking.fints.messages.segmente.Synchronisierung import net.codinux.banking.fints.messages.segmente.id.CustomerSegmentId import net.codinux.banking.fints.messages.segmente.id.ISegmentId import net.codinux.banking.fints.messages.segmente.implementierte.* +import net.codinux.banking.fints.messages.segmente.implementierte.depot.Depotaufstellung import net.codinux.banking.fints.messages.segmente.implementierte.sepa.SepaBankTransferBase import net.codinux.banking.fints.messages.segmente.implementierte.tan.TanGeneratorListeAnzeigen import net.codinux.banking.fints.messages.segmente.implementierte.tan.TanGeneratorTanMediumAnOderUmmelden @@ -242,17 +243,38 @@ open class MessageBuilder(protected val utils: FinTsUtils = FinTsUtils()) { return createSignedMessageBuilderResult(context, MessageType.GetBalance, segments) } + val securitiesAccountResult = supportsGetSecuritiesAccountBalance(account) + + if (securitiesAccountResult.isJobVersionSupported) { + return createGetSecuritiesAccountBalanceMessage(context, result, account) + } + return result } + protected open fun createGetSecuritiesAccountBalanceMessage(context: JobContext, result: MessageBuilderResult, + account: AccountData): MessageBuilderResult { + + val segments = mutableListOf(Depotaufstellung(SignedMessagePayloadFirstSegmentNumber, account)) + + addTanSegmentIfRequired(context, CustomerSegmentId.SecuritiesAccountBalance, segments, SignedMessagePayloadFirstSegmentNumber + 1) + + return createSignedMessageBuilderResult(context, MessageType.GetSecuritiesAccountBalance, segments) + } + open fun supportsGetBalance(account: AccountData): Boolean { return supportsGetBalanceMessage(account).isJobVersionSupported + || supportsGetSecuritiesAccountBalance(account).isJobVersionSupported } protected open fun supportsGetBalanceMessage(account: AccountData): MessageBuilderResult { return getSupportedVersionsOfJobForAccount(CustomerSegmentId.Balance, account, listOf(5, 6, 7, 8)) } + protected open fun supportsGetSecuritiesAccountBalance(account: AccountData): MessageBuilderResult { + return getSupportedVersionsOfJobForAccount(CustomerSegmentId.SecuritiesAccountBalance, account, listOf(6)) + } + open fun createGetTanMediaListMessage(context: JobContext, tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle, diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/segmente/id/CustomerSegmentId.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/segmente/id/CustomerSegmentId.kt index b112a228..6091a25d 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/segmente/id/CustomerSegmentId.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/segmente/id/CustomerSegmentId.kt @@ -27,6 +27,11 @@ enum class CustomerSegmentId(override val id: String) : ISegmentId { SepaRealTimeTransfer("HKIPZ"), - SepaAccountInfoParameters("HKSPA") // not implemented, retrieved automatically with UPD + SepaAccountInfoParameters("HKSPA"), // not implemented, retrieved automatically with UPD + + + /* Wertpapierdepot */ + + SecuritiesAccountBalance("HKWPD") } \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/segmente/implementierte/depot/Depotaufstellung.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/segmente/implementierte/depot/Depotaufstellung.kt new file mode 100644 index 00000000..3e7e3622 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/segmente/implementierte/depot/Depotaufstellung.kt @@ -0,0 +1,37 @@ +package net.codinux.banking.fints.messages.segmente.implementierte.depot + +import net.codinux.banking.fints.messages.Existenzstatus +import net.codinux.banking.fints.messages.datenelemente.implementierte.Aufsetzpunkt +import net.codinux.banking.fints.messages.datenelemente.implementierte.account.MaximaleAnzahlEintraege +import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.Segmentkopf +import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.account.Kontoverbindung +import net.codinux.banking.fints.messages.segmente.Segment +import net.codinux.banking.fints.messages.segmente.id.CustomerSegmentId +import net.codinux.banking.fints.model.AccountData + +/** + * Nr. Name Version Typ Format Länge Status Anzahl Restriktionen +1 Segmentkopf 1 DEG M 1 +2 Depot 3 DEG ktv # M 1 +3 Währung der Depotaufstellung 1 DE cur # C 1 O: „Währung der Depotaufstellung wählbar“ (BPD) = „J“; N: sonst +4 Kursqualität 2 DE code 1 C 1 1,2 O: „Kursqualität wählbar“ (BPD) = „J“; N: sonst +5 Maximale Anzahl Einträge 1 DE num ..4 C 1 >0 O: „Eingabe Anzahl Einträge erlaubt“ (BPD) = „J“; N: sonst +6 Aufsetzpunkt 1 DE an ..35 C 1 M: vom Institut wurde ein Aufsetzpunkt rückgemeldet N: sonst + */ +class Depotaufstellung( + segmentNumber: Int, + account: AccountData, +// parameter: GetAccountTransactionsParameter +): Segment(listOf( + Segmentkopf(CustomerSegmentId.SecuritiesAccountBalance, 6, segmentNumber), + Kontoverbindung(account), + // TODO: + // 3. Währung der Depotaufstellung + // 4. Kursqualität + // 5. Maximale Anzahl Einträge + // 6. Aufsetzpunkt + +// MaximaleAnzahlEintraege(parameter), // TODO: this is wrong, it only works for HKKAZ + MaximaleAnzahlEintraege(null, Existenzstatus.Optional), + Aufsetzpunkt(null, Existenzstatus.Optional) // will be set dynamically, see MessageBuilder.rebuildMessageWithContinuationId(); M: vom Institut wurde ein Aufsetzpunkt rückgemeldet. N: sonst +)) \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/MessageType.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/MessageType.kt index 12cc5812..d4b9af1f 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/MessageType.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/MessageType.kt @@ -25,6 +25,8 @@ enum class MessageType { GetCreditCardTransactions, + GetSecuritiesAccountBalance, + TransferMoney } \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/RetrievedAccountData.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/RetrievedAccountData.kt index a7a0ba83..b324f069 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/RetrievedAccountData.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/RetrievedAccountData.kt @@ -2,6 +2,7 @@ package net.codinux.banking.fints.model import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate +import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings open class RetrievedAccountData( @@ -10,6 +11,7 @@ open class RetrievedAccountData( open val balance: Money?, open var bookedTransactions: Collection, open var unbookedTransactions: Collection, + open var statementOfHoldings: List, open val retrievalTime: Instant, open val retrievedTransactionsFrom: LocalDate?, open val retrievedTransactionsTo: LocalDate?, @@ -19,7 +21,7 @@ open class RetrievedAccountData( companion object { fun unsuccessful(account: AccountData): RetrievedAccountData { - return RetrievedAccountData(account, false, null, listOf(), listOf(), Instant.DISTANT_PAST, null, null) + return RetrievedAccountData(account, false, null, listOf(), listOf(), listOf(), Instant.DISTANT_PAST, null, null) } } diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/InstituteSegmentId.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/InstituteSegmentId.kt index 1d549632..6a95f9eb 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/InstituteSegmentId.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/InstituteSegmentId.kt @@ -43,6 +43,11 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId { CreditCardTransactions("DIKKU"), - CreditCardTransactionsParameters(CreditCardTransactions.id + "S") + CreditCardTransactionsParameters(CreditCardTransactions.id + "S"), + + + /* Wertpapierdepot */ + + SecuritiesAccountBalance("HIWPD") } \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/ResponseParser.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/ResponseParser.kt index 3116e77a..91188d86 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/ResponseParser.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/ResponseParser.kt @@ -24,11 +24,13 @@ import net.codinux.banking.fints.model.Money import net.codinux.banking.fints.response.segments.* import net.codinux.banking.fints.util.MessageUtils import net.codinux.banking.fints.extensions.getAllExceptionMessagesJoined +import net.codinux.banking.fints.transactions.swift.Mt535Parser open class ResponseParser( protected open val messageUtils: MessageUtils = MessageUtils(), - open var logAppender: IMessageLogAppender? = null + open var logAppender: IMessageLogAppender? = null, + open var mt535Parser: Mt535Parser = Mt535Parser(logAppender) ) { companion object { @@ -116,6 +118,7 @@ open class ResponseParser( InstituteSegmentId.ChangeTanMediaParameters.id -> parseChangeTanMediaParameters(segment, segmentId, dataElementGroups) InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups) + InstituteSegmentId.SecuritiesAccountBalance.id -> parseSecuritiesAccountBalanceSegment(segment, dataElementGroups) InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups) InstituteSegmentId.AccountTransactionsMt940Parameters.id -> parseMt940AccountTransactionsParameters(segment, segmentId, dataElementGroups) @@ -690,6 +693,17 @@ open class ResponseParser( ) } + protected open fun parseSecuritiesAccountBalanceSegment(segment: String, dataElementGroups: List): SecuritiesAccountBalanceSegment { + // 1 Segmentkopf 1 DEG M 1 + // 2 Depotaufstellung 1 DE bin .. M 1 + + val balancesMt535String = extractBinaryData(dataElementGroups[1]) + // TODO: for larger portfolios there can be a Aufsetzpunkt, but for balances we currently do not support sending multiple messages + val statementOfHoldings = mt535Parser.parseMt535String(balancesMt535String) + + return SecuritiesAccountBalanceSegment(statementOfHoldings, segment) + } + protected open fun parseBalanceToNullIfZeroOrNotSet(dataElementGroup: String): Balance? { if (dataElementGroup.isEmpty()) { return null diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/segments/SecuritiesAccountBalanceSegment.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/segments/SecuritiesAccountBalanceSegment.kt new file mode 100644 index 00000000..6f2c1688 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/segments/SecuritiesAccountBalanceSegment.kt @@ -0,0 +1,9 @@ +package net.codinux.banking.fints.response.segments + +import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings + +class SecuritiesAccountBalanceSegment( + val statementOfHoldings: List, + segmentString: String +) + : ReceivedSegment(segmentString) \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/transactions/swift/model/Holding.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/transactions/swift/model/Holding.kt index 8a69fd6a..87e7e3d8 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/transactions/swift/model/Holding.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/transactions/swift/model/Holding.kt @@ -2,8 +2,10 @@ package net.codinux.banking.fints.transactions.swift.model import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate +import kotlinx.serialization.Serializable import net.codinux.banking.fints.model.Amount +@Serializable data class Holding( val name: String, val isin: String?, diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/transactions/swift/model/StatementOfHoldings.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/transactions/swift/model/StatementOfHoldings.kt index ff59db50..3b41016c 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/transactions/swift/model/StatementOfHoldings.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/transactions/swift/model/StatementOfHoldings.kt @@ -1,6 +1,7 @@ package net.codinux.banking.fints.transactions.swift.model import kotlinx.datetime.LocalDate +import kotlinx.serialization.Serializable import net.codinux.banking.fints.model.Amount /** @@ -8,6 +9,7 @@ import net.codinux.banking.fints.model.Amount * „Statement of Holdings“; basiert auf SWIFT „Standards Release Guide“ * (letzte berücksichtigte Änderung SRG 1998) */ +@Serializable data class StatementOfHoldings( val bankCode: String, val accountIdentifier: String, @@ -15,7 +17,7 @@ data class StatementOfHoldings( val holdings: List, val totalBalance: Amount? = null, - val totalBalanceCurrency: String? = null, + val currency: String? = null, /** * The page number is actually mandatory, but to be prepared for surprises like for [statementDate] i added error diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccount.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccount.kt index 9ff01f8f..5c83d885 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccount.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccount.kt @@ -5,6 +5,7 @@ import kotlinx.datetime.LocalDate import kotlinx.serialization.Serializable import net.codinux.banking.fints.model.Currency import net.codinux.banking.fints.model.Money +import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings @Serializable @@ -40,6 +41,8 @@ open class BankAccount( open var bookedTransactions: List = listOf() + open var statementOfHoldings: List = emptyList() + override fun toString(): String { return "$productName ($identifier)" diff --git a/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/transactions/swift/Mt535ParserTest.kt b/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/transactions/swift/Mt535ParserTest.kt index a2b35035..a1bf42d7 100644 --- a/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/transactions/swift/Mt535ParserTest.kt +++ b/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/transactions/swift/Mt535ParserTest.kt @@ -8,7 +8,6 @@ import net.codinux.banking.fints.transactions.swift.model.ContinuationIndicator import net.codinux.banking.fints.transactions.swift.model.Holding import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings import kotlin.test.Test -import kotlin.test.assertNotNull class Mt535ParserTest { @@ -37,7 +36,7 @@ class Mt535ParserTest { assertEquals("1234567", statement.accountIdentifier) assertEquals("17026,37", statement.totalBalance?.string) - assertEquals("EUR", statement.totalBalanceCurrency) + assertEquals("EUR", statement.currency) assertEquals(1, statement.pageNumber) assertEquals(ContinuationIndicator.SinglePage, statement.continuationIndicator) @@ -61,7 +60,7 @@ class Mt535ParserTest { assertEquals(accountId, statement.accountIdentifier) assertEquals(totalBalance, statement.totalBalance?.string) - assertEquals(totalBalanceCurrency, statement.totalBalanceCurrency) + assertEquals(totalBalanceCurrency, statement.currency) assertEquals(pageNumber, statement.pageNumber) assertEquals(continuationIndicator, statement.continuationIndicator)