Implemented mapping Holdings

This commit is contained in:
dankito 2024-09-11 22:47:13 +02:00
parent c5b7967ce1
commit 5187e34797
6 changed files with 139 additions and 7 deletions

View File

@ -14,5 +14,8 @@ value class Amount(val amount: String = "0") {
}
constructor(amount: Double) : this(amount.toString())
override fun toString() = amount
}

View File

@ -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<AccountTransaction> = mutableListOf(),
open val prebookedTransactions: MutableList<PrebookedAccountTransaction> = mutableListOf(),
open val holdings: List<Holding> = emptyList(),
var userSetDisplayName: String? = null,
var displayIndex: Int = 0,

View File

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

View File

@ -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<AccountTransaction>,
val prebookedTransactions: List<PrebookedAccountTransaction>,
val holdings: List<Holding> = emptyList(),
val transactionsRetrievalTime: Instant,
val retrievedTransactionsFrom: LocalDate? = null,
val retrievedTransactionsTo: LocalDate? = null

View File

@ -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"
}

View File

@ -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<net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings>, 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<net.codinux.banking.client.model.securitiesaccount.Holding> {
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 {