Implemented requesting and parsing securities account balance

This commit is contained in:
dankito 2024-09-11 06:39:59 +02:00
parent 95e60b2706
commit a42de32260
14 changed files with 138 additions and 10 deletions

View File

@ -202,6 +202,29 @@ open class FinTsJobExecutor(
var balance: Money? = balanceResponse?.getFirstSegmentById<BalanceSegment>(InstituteSegmentId.Balance)?.let { var balance: Money? = balanceResponse?.getFirstSegmentById<BalanceSegment>(InstituteSegmentId.Balance)?.let {
Money(it.balance, it.currency) 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<SecuritiesAccountBalanceSegment>(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<AccountTransaction>() val bookedTransactions = mutableSetOf<AccountTransaction>()
val unbookedTransactions = mutableSetOf<Any>() val unbookedTransactions = mutableSetOf<Any>()
@ -235,11 +258,12 @@ open class FinTsJobExecutor(
closeDialog(context) closeDialog(context)
val successful = response.tanRequiredButWeWereToldToAbortIfSo 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 val fromDate = parameter.fromDate
?: parameter.account.serverTransactionsRetentionDays?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) } ?: parameter.account.serverTransactionsRetentionDays?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) }
?: bookedTransactions.minByOrNull { it.valueDate }?.valueDate ?: 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, return GetAccountTransactionsResponse(context, response, retrievedData,
if (parameter.maxCountEntries != null) parameter.isSettingMaxCountEntriesAllowedByBank else null) if (parameter.maxCountEntries != null) parameter.isSettingMaxCountEntriesAllowedByBank else null)

View File

@ -103,6 +103,8 @@ open class FinTsModelMapper {
addAll(map(accountTransactionsResponse)) addAll(map(accountTransactionsResponse))
} }
} }
bankAccount.statementOfHoldings = accountTransactionsResponse.statementOfHoldings
} }
} }

View File

@ -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.CustomerSegmentId
import net.codinux.banking.fints.messages.segmente.id.ISegmentId 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.*
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.sepa.SepaBankTransferBase
import net.codinux.banking.fints.messages.segmente.implementierte.tan.TanGeneratorListeAnzeigen import net.codinux.banking.fints.messages.segmente.implementierte.tan.TanGeneratorListeAnzeigen
import net.codinux.banking.fints.messages.segmente.implementierte.tan.TanGeneratorTanMediumAnOderUmmelden 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) return createSignedMessageBuilderResult(context, MessageType.GetBalance, segments)
} }
val securitiesAccountResult = supportsGetSecuritiesAccountBalance(account)
if (securitiesAccountResult.isJobVersionSupported) {
return createGetSecuritiesAccountBalanceMessage(context, result, account)
}
return result return result
} }
protected open fun createGetSecuritiesAccountBalanceMessage(context: JobContext, result: MessageBuilderResult,
account: AccountData): MessageBuilderResult {
val segments = mutableListOf<Segment>(Depotaufstellung(SignedMessagePayloadFirstSegmentNumber, account))
addTanSegmentIfRequired(context, CustomerSegmentId.SecuritiesAccountBalance, segments, SignedMessagePayloadFirstSegmentNumber + 1)
return createSignedMessageBuilderResult(context, MessageType.GetSecuritiesAccountBalance, segments)
}
open fun supportsGetBalance(account: AccountData): Boolean { open fun supportsGetBalance(account: AccountData): Boolean {
return supportsGetBalanceMessage(account).isJobVersionSupported return supportsGetBalanceMessage(account).isJobVersionSupported
|| supportsGetSecuritiesAccountBalance(account).isJobVersionSupported
} }
protected open fun supportsGetBalanceMessage(account: AccountData): MessageBuilderResult { protected open fun supportsGetBalanceMessage(account: AccountData): MessageBuilderResult {
return getSupportedVersionsOfJobForAccount(CustomerSegmentId.Balance, account, listOf(5, 6, 7, 8)) 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, open fun createGetTanMediaListMessage(context: JobContext,
tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle, tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle,

View File

@ -27,6 +27,11 @@ enum class CustomerSegmentId(override val id: String) : ISegmentId {
SepaRealTimeTransfer("HKIPZ"), SepaRealTimeTransfer("HKIPZ"),
SepaAccountInfoParameters("HKSPA") // not implemented, retrieved automatically with UPD SepaAccountInfoParameters("HKSPA"), // not implemented, retrieved automatically with UPD
/* Wertpapierdepot */
SecuritiesAccountBalance("HKWPD")
} }

View File

@ -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
))

View File

@ -25,6 +25,8 @@ enum class MessageType {
GetCreditCardTransactions, GetCreditCardTransactions,
GetSecuritiesAccountBalance,
TransferMoney TransferMoney
} }

View File

@ -2,6 +2,7 @@ package net.codinux.banking.fints.model
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings
open class RetrievedAccountData( open class RetrievedAccountData(
@ -10,6 +11,7 @@ open class RetrievedAccountData(
open val balance: Money?, open val balance: Money?,
open var bookedTransactions: Collection<AccountTransaction>, open var bookedTransactions: Collection<AccountTransaction>,
open var unbookedTransactions: Collection<Any>, open var unbookedTransactions: Collection<Any>,
open var statementOfHoldings: List<StatementOfHoldings>,
open val retrievalTime: Instant, open val retrievalTime: Instant,
open val retrievedTransactionsFrom: LocalDate?, open val retrievedTransactionsFrom: LocalDate?,
open val retrievedTransactionsTo: LocalDate?, open val retrievedTransactionsTo: LocalDate?,
@ -19,7 +21,7 @@ open class RetrievedAccountData(
companion object { companion object {
fun unsuccessful(account: AccountData): RetrievedAccountData { 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)
} }
} }

View File

@ -43,6 +43,11 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
CreditCardTransactions("DIKKU"), CreditCardTransactions("DIKKU"),
CreditCardTransactionsParameters(CreditCardTransactions.id + "S") CreditCardTransactionsParameters(CreditCardTransactions.id + "S"),
/* Wertpapierdepot */
SecuritiesAccountBalance("HIWPD")
} }

View File

@ -24,11 +24,13 @@ import net.codinux.banking.fints.model.Money
import net.codinux.banking.fints.response.segments.* import net.codinux.banking.fints.response.segments.*
import net.codinux.banking.fints.util.MessageUtils import net.codinux.banking.fints.util.MessageUtils
import net.codinux.banking.fints.extensions.getAllExceptionMessagesJoined import net.codinux.banking.fints.extensions.getAllExceptionMessagesJoined
import net.codinux.banking.fints.transactions.swift.Mt535Parser
open class ResponseParser( open class ResponseParser(
protected open val messageUtils: MessageUtils = MessageUtils(), protected open val messageUtils: MessageUtils = MessageUtils(),
open var logAppender: IMessageLogAppender? = null open var logAppender: IMessageLogAppender? = null,
open var mt535Parser: Mt535Parser = Mt535Parser(logAppender)
) { ) {
companion object { companion object {
@ -116,6 +118,7 @@ open class ResponseParser(
InstituteSegmentId.ChangeTanMediaParameters.id -> parseChangeTanMediaParameters(segment, segmentId, dataElementGroups) InstituteSegmentId.ChangeTanMediaParameters.id -> parseChangeTanMediaParameters(segment, segmentId, dataElementGroups)
InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups) InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups)
InstituteSegmentId.SecuritiesAccountBalance.id -> parseSecuritiesAccountBalanceSegment(segment, dataElementGroups)
InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups) InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups)
InstituteSegmentId.AccountTransactionsMt940Parameters.id -> parseMt940AccountTransactionsParameters(segment, segmentId, dataElementGroups) InstituteSegmentId.AccountTransactionsMt940Parameters.id -> parseMt940AccountTransactionsParameters(segment, segmentId, dataElementGroups)
@ -690,6 +693,17 @@ open class ResponseParser(
) )
} }
protected open fun parseSecuritiesAccountBalanceSegment(segment: String, dataElementGroups: List<String>): 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? { protected open fun parseBalanceToNullIfZeroOrNotSet(dataElementGroup: String): Balance? {
if (dataElementGroup.isEmpty()) { if (dataElementGroup.isEmpty()) {
return null return null

View File

@ -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<StatementOfHoldings>,
segmentString: String
)
: ReceivedSegment(segmentString)

View File

@ -2,8 +2,10 @@ package net.codinux.banking.fints.transactions.swift.model
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
import net.codinux.banking.fints.model.Amount import net.codinux.banking.fints.model.Amount
@Serializable
data class Holding( data class Holding(
val name: String, val name: String,
val isin: String?, val isin: String?,

View File

@ -1,6 +1,7 @@
package net.codinux.banking.fints.transactions.swift.model package net.codinux.banking.fints.transactions.swift.model
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
import net.codinux.banking.fints.model.Amount 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 * Statement of Holdings; basiert auf SWIFT Standards Release Guide
* (letzte berücksichtigte Änderung SRG 1998) * (letzte berücksichtigte Änderung SRG 1998)
*/ */
@Serializable
data class StatementOfHoldings( data class StatementOfHoldings(
val bankCode: String, val bankCode: String,
val accountIdentifier: String, val accountIdentifier: String,
@ -15,7 +17,7 @@ data class StatementOfHoldings(
val holdings: List<Holding>, val holdings: List<Holding>,
val totalBalance: Amount? = null, 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 * The page number is actually mandatory, but to be prepared for surprises like for [statementDate] i added error

View File

@ -5,6 +5,7 @@ import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.codinux.banking.fints.model.Currency import net.codinux.banking.fints.model.Currency
import net.codinux.banking.fints.model.Money import net.codinux.banking.fints.model.Money
import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings
@Serializable @Serializable
@ -40,6 +41,8 @@ open class BankAccount(
open var bookedTransactions: List<AccountTransaction> = listOf() open var bookedTransactions: List<AccountTransaction> = listOf()
open var statementOfHoldings: List<StatementOfHoldings> = emptyList()
override fun toString(): String { override fun toString(): String {
return "$productName ($identifier)" return "$productName ($identifier)"

View File

@ -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.Holding
import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertNotNull
class Mt535ParserTest { class Mt535ParserTest {
@ -37,7 +36,7 @@ class Mt535ParserTest {
assertEquals("1234567", statement.accountIdentifier) assertEquals("1234567", statement.accountIdentifier)
assertEquals("17026,37", statement.totalBalance?.string) assertEquals("17026,37", statement.totalBalance?.string)
assertEquals("EUR", statement.totalBalanceCurrency) assertEquals("EUR", statement.currency)
assertEquals(1, statement.pageNumber) assertEquals(1, statement.pageNumber)
assertEquals(ContinuationIndicator.SinglePage, statement.continuationIndicator) assertEquals(ContinuationIndicator.SinglePage, statement.continuationIndicator)
@ -61,7 +60,7 @@ class Mt535ParserTest {
assertEquals(accountId, statement.accountIdentifier) assertEquals(accountId, statement.accountIdentifier)
assertEquals(totalBalance, statement.totalBalance?.string) assertEquals(totalBalance, statement.totalBalance?.string)
assertEquals(totalBalanceCurrency, statement.totalBalanceCurrency) assertEquals(totalBalanceCurrency, statement.currency)
assertEquals(pageNumber, statement.pageNumber) assertEquals(pageNumber, statement.pageNumber)
assertEquals(continuationIndicator, statement.continuationIndicator) assertEquals(continuationIndicator, statement.continuationIndicator)