From 5187e3479756ee131adbc90c257546dbc6af4d8d Mon Sep 17 00:00:00 2001 From: dankito Date: Wed, 11 Sep 2024 22:47:13 +0200 Subject: [PATCH] Implemented mapping Holdings --- .../codinux/banking/client/model/Amount.kt | 3 + .../banking/client/model/BankAccount.kt | 2 + .../model/extensions/AmountExtensions.kt | 6 ++ .../model/response/GetTransactionsResponse.kt | 2 + .../client/model/securitiesaccount/Holding.kt | 49 +++++++++++ .../banking/client/fints4k/FinTs4kMapper.kt | 84 +++++++++++++++++-- 6 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/AmountExtensions.kt create mode 100644 BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/securitiesaccount/Holding.kt diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/Amount.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/Amount.kt index bd98ea00..bd217748 100644 --- a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/Amount.kt +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/Amount.kt @@ -14,5 +14,8 @@ value class Amount(val amount: String = "0") { } + constructor(amount: Double) : this(amount.toString()) + + override fun toString() = amount } \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/BankAccount.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/BankAccount.kt index 4af3af0c..1e533150 100644 --- a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/BankAccount.kt +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/BankAccount.kt @@ -3,6 +3,7 @@ package net.codinux.banking.client.model import kotlinx.datetime.* import net.codinux.banking.client.model.config.JsonIgnore import net.codinux.banking.client.model.config.NoArgConstructor +import net.codinux.banking.client.model.securitiesaccount.Holding @Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED") @NoArgConstructor @@ -28,6 +29,7 @@ open class BankAccount( open val bookedTransactions: MutableList = mutableListOf(), open val prebookedTransactions: MutableList = mutableListOf(), + open val holdings: List = emptyList(), var userSetDisplayName: String? = null, var displayIndex: Int = 0, diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/AmountExtensions.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/AmountExtensions.kt new file mode 100644 index 00000000..025826f7 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/AmountExtensions.kt @@ -0,0 +1,6 @@ +package net.codinux.banking.client.model.extensions + +import net.codinux.banking.client.model.Amount + +// TODO: really map to BigDecimal +fun Amount.toBigDecimal(): Double = this.amount.toDouble() \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetTransactionsResponse.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetTransactionsResponse.kt index d274e261..d1632055 100644 --- a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetTransactionsResponse.kt +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetTransactionsResponse.kt @@ -7,6 +7,7 @@ import net.codinux.banking.client.model.Amount import net.codinux.banking.client.model.BankAccount import net.codinux.banking.client.model.PrebookedAccountTransaction import net.codinux.banking.client.model.config.NoArgConstructor +import net.codinux.banking.client.model.securitiesaccount.Holding @NoArgConstructor open class GetTransactionsResponse( @@ -14,6 +15,7 @@ open class GetTransactionsResponse( val balance: Amount? = null, val bookedTransactions: List, val prebookedTransactions: List, + val holdings: List = emptyList(), val transactionsRetrievalTime: Instant, val retrievedTransactionsFrom: LocalDate? = null, val retrievedTransactionsTo: LocalDate? = null diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/securitiesaccount/Holding.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/securitiesaccount/Holding.kt new file mode 100644 index 00000000..1dcb84a4 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/securitiesaccount/Holding.kt @@ -0,0 +1,49 @@ +package net.codinux.banking.client.model.securitiesaccount + +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import net.codinux.banking.client.model.Amount +import net.codinux.banking.client.model.config.NoArgConstructor + +@NoArgConstructor +open class Holding( + val name: String, + + val isin: String? = null, + val wkn: String? = null, + + val quantity: Int? = null, + val currency: String? = null, + + /** + * Gesamter Kurswert aller Einheiten des Wertpapiers + */ + val totalBalance: Amount? = null, + /** + * Aktueller Kurswert einer einzelnen Einheit des Wertpapiers + */ + val marketValue: Amount? = null, + + /** + * Änderung in Prozent Aktueller Kurswert gegenüber Einstandspreis. + */ + val performancePercentage: Float? = null, + + /** + * Gesamter Einstandspreis (Kaufpreis) + */ + val totalCostPrice: Amount? = null, + /** + * (Durchschnittlicher) Einstandspreis/-kurs einer Einheit des Wertpapiers + */ + val averageCostPrice: Amount? = null, + + /** + * Zeitpunkt zu dem der Kurswert bestimmt wurde + */ + val pricingTime: Instant? = null, + + val buyingDate: LocalDate? = null, +) { + override fun toString() = "$name $totalBalance $currency" +} \ No newline at end of file diff --git a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt index d32ae5da..381a21ed 100644 --- a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt @@ -1,11 +1,13 @@ package net.codinux.banking.client.fints4k import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import net.codinux.banking.client.model.* import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.Amount +import net.codinux.banking.client.model.extensions.toBigDecimal import net.codinux.banking.client.model.tan.* import net.codinux.banking.client.model.options.GetAccountDataOptions import net.codinux.banking.client.model.request.GetAccountDataRequest @@ -24,6 +26,7 @@ import net.dankito.banking.client.model.response.ErrorCode import net.codinux.banking.fints.mapper.FinTsModelMapper import net.codinux.banking.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion import net.codinux.banking.fints.model.* +import net.codinux.banking.fints.transactions.swift.model.Holding import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.MobilePhoneTanMedium import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium @@ -111,7 +114,8 @@ open class FinTs4kMapper { account.retrievedTransactionsFrom = finTsBankAccount.retrievedTransactionsFrom } - Response.success(GetTransactionsResponse(account, mapAmount(finTsBankAccount.balance), mapBookedTransactions(finTsBankAccount), emptyList(), + Response.success(GetTransactionsResponse(account, mapMoney(finTsBankAccount.balance), mapBookedTransactions(finTsBankAccount), emptyList(), + mapHoldings(finTsBankAccount.statementOfHoldings, finTsBankAccount.currency, finTsBankAccount.lastTransactionsRetrievalTime), finTsBankAccount.lastTransactionsRetrievalTime ?: Clock.System.now(), param.retrieveTransactionsFrom, param.retrieveTransactionsTo)) } else { mapError(getAccountDataResponse) @@ -156,10 +160,11 @@ open class FinTs4kMapper { account.identifier, account.subAccountNumber, account.iban, account.productName, account.accountHolderName, mapAccountType(account.type), account.currency, account.accountLimit, account.isAccountTypeSupportedByApplication, mapFeatures(account), - mapAmount(account.balance), + mapMoney(account.balance), account.serverTransactionsRetentionDays, account.lastTransactionsRetrievalTime, account.retrievedTransactionsFrom, - bookedTransactions = mapBookedTransactions(account).toMutableList() + bookedTransactions = mapBookedTransactions(account).toMutableList(), + holdings = mapHoldings(account.statementOfHoldings, account.currency, account.lastTransactionsRetrievalTime) ) protected open fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType = @@ -185,12 +190,12 @@ open class FinTs4kMapper { account.bookedTransactions.map { mapTransaction(it) } protected open fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction( - mapAmount(transaction.amount), transaction.amount.currency.code, transaction.reference, + mapMoney(transaction.amount), transaction.amount.currency.code, transaction.reference, transaction.bookingDate, transaction.valueDate, transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId, transaction.postingText, - mapNullableAmount(transaction.openingBalance), mapNullableAmount(transaction.closingBalance), + mapNullableMoney(transaction.openingBalance), mapNullableMoney(transaction.closingBalance), transaction.statementNumber, transaction.sheetNumber, @@ -209,9 +214,74 @@ open class FinTs4kMapper { transaction.isReversal ) - protected open fun mapNullableAmount(amount: Money?) = amount?.let { mapAmount(it) } + protected open fun mapHoldings(statements: List, accountCurrency: String, lastAccountUpdateTime: Instant? = null) = + statements.flatMap { mapHoldings(it, accountCurrency, lastAccountUpdateTime) } - protected open fun mapAmount(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.')) + protected open fun mapHoldings(statement: net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings, accountCurrency: String, lastAccountUpdateTime: Instant? = null): List { + + val totalBalance = mapNullableAmount(statement.totalBalance) + val currency = statement.currency ?: accountCurrency + val statementDate: Instant? = /* statement.statementDate ?: statement.preparationDate ?: */ lastAccountUpdateTime // TODO + + return statement.holdings.map { mapHolding(it, currency, statementDate, if (statement.holdings.size == 1) totalBalance else null) } + } + + protected open fun mapHolding(holding: Holding, accountCurrency: String, statementDate: Instant?, totalBalance: Amount? = null) = net.codinux.banking.client.model.securitiesaccount.Holding( + holding.name, holding.isin, holding.wkn, + + holding.quantity, holding.currency ?: accountCurrency, + + getTotalBalance(holding), mapNullableAmount(holding.marketValue), + calculatePerformance(holding), + getTotalCostPrice(holding), mapNullableAmount(holding.averageCostPrice), + + holding.pricingTime ?: statementDate, holding.buyingDate + ) + + private fun getTotalBalance(holding: Holding): Amount? { + return if (holding.totalBalance != null) { + mapNullableAmount(holding.totalBalance) + } else if (holding.quantity != null && holding.marketValue != null) { + Amount(holding.quantity!! * mapAmount(holding.marketValue!!).toBigDecimal()) + } else { + null + } + } + + private fun getTotalCostPrice(holding: Holding): Amount? { + return if (holding.totalCostPrice != null) { + mapNullableAmount(holding.totalCostPrice) + } else if (holding.quantity != null && holding.averageCostPrice != null) { + Amount(holding.quantity!! * mapAmount(holding.averageCostPrice!!).toBigDecimal()) + } else { + null + } + } + + private fun calculatePerformance(holding: Holding): Float? { + val totalBalance = getTotalBalance(holding) + val totalCostPrice = getTotalCostPrice(holding) + + if (totalBalance != null && totalCostPrice != null) { + return ((totalBalance.toBigDecimal() - totalCostPrice.toBigDecimal()) / totalCostPrice.toBigDecimal() * 100).toFloat() + } + + val marketValue = mapNullableAmount(holding.marketValue) + val costPrice = mapNullableAmount(holding.averageCostPrice) + if (marketValue != null && costPrice != null) { + return ((marketValue.toBigDecimal() - costPrice.toBigDecimal()) / costPrice.toBigDecimal() * 100).toFloat() + } + + return null + } + + protected open fun mapNullableMoney(amount: Money?) = amount?.let { mapMoney(it) } + + protected open fun mapMoney(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.')) + + protected open fun mapNullableAmount(amount: net.codinux.banking.fints.model.Amount?) = amount?.let { mapAmount(it) } + + protected open fun mapAmount(amount: net.codinux.banking.fints.model.Amount) = Amount.fromString(amount.string.replace(',', '.')) open fun mapTanChallenge(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallenge {