Compare commits

..

No commits in common. "main" and "v0.5.1" have entirely different histories.
main ... v0.5.1

62 changed files with 740 additions and 2212 deletions

View file

@ -1,14 +1,9 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.*
import net.codinux.banking.client.model.options.RetrieveTransactions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.GetTransactionsResponse
import net.codinux.banking.client.model.response.Response
import net.codinux.banking.client.model.response.TransferMoneyResponse
import net.codinux.banking.client.model.tan.TanMethodType
interface BankingClient {
@ -35,23 +30,4 @@ interface BankingClient {
*/
suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse>
/**
* Convenience wrapper around [getAccountDataAsync].
* Updates account's transactions beginning from [BankAccount.lastAccountUpdateTime].
* This may requires TAN if [BankAccount.lastAccountUpdateTime] is older than 90 days.
*
* Optionally specify which [accounts] should be updated. If not specified all accounts will be updated.
*/
suspend fun updateAccountTransactionsAsync(
bank: BankAccess, accounts: List<BankAccount>? = null,
preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications
): Response<List<GetTransactionsResponse>>
suspend fun transferMoneyAsync(bankCode: String, loginName: String, password: String, recipientName: String,
recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null) =
transferMoneyAsync(TransferMoneyRequestForUser(bankCode, loginName, password, null, recipientName, recipientAccountIdentifier, null, amount, paymentReference = paymentReference))
suspend fun transferMoneyAsync(request: TransferMoneyRequestForUser): Response<TransferMoneyResponse>
}

View file

@ -1,15 +1,9 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.options.RetrieveTransactions
import net.codinux.banking.client.model.request.TransferMoneyRequest
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.GetTransactionsResponse
import net.codinux.banking.client.model.response.Response
import net.codinux.banking.client.model.response.TransferMoneyResponse
import net.codinux.banking.client.model.tan.TanMethodType
interface BankingClientForUser {
@ -36,19 +30,4 @@ interface BankingClientForUser {
*/
suspend fun getAccountDataAsync(options: GetAccountDataOptions): Response<GetAccountDataResponse>
/**
* Convenience wrapper around [getAccountDataAsync].
* Updates account's transactions beginning from [BankAccount.lastAccountUpdateTime].
* This may requires TAN if [BankAccount.lastAccountUpdateTime] is older than 90 days.
*/
suspend fun updateAccountTransactionsAsync(
accounts: List<BankAccount>? = null,
preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications
): Response<List<GetTransactionsResponse>>
suspend fun transferMoneyAsync(recipientName: String, recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null): Response<TransferMoneyResponse>
suspend fun transferMoneyAsync(request: TransferMoneyRequest): Response<TransferMoneyResponse>
}

View file

@ -1,39 +1,15 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.response.GetTransactionsResponse
import net.codinux.banking.client.model.response.Response
import net.codinux.banking.client.model.tan.TanMethodType
abstract class BankingClientForUserBase(
protected val credentials: AccountCredentials,
protected val client: BankingClient
) : BankingClientForUser {
private lateinit var bank: BankAccess
override suspend fun getAccountDataAsync(options: GetAccountDataOptions) =
client.getAccountDataAsync(GetAccountDataRequest(credentials, options)).also {
it.data?.bank?.let { retrievedBank ->
this.bank = retrievedBank
}
}
override suspend fun updateAccountTransactionsAsync(accounts: List<BankAccount>?, preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>?): Response<List<GetTransactionsResponse>> =
client.updateAccountTransactionsAsync(bank, accounts)
override suspend fun transferMoneyAsync(recipientName: String, recipientAccountIdentifier: String, amount: Amount, paymentReference: String?) =
transferMoneyAsync(TransferMoneyRequest(null, recipientName, recipientAccountIdentifier, null, amount, paymentReference = paymentReference))
override suspend fun transferMoneyAsync(request: TransferMoneyRequest) =
client.transferMoneyAsync(TransferMoneyRequestForUser(bank.domesticBankCode, bank.loginName, bank.password!!, request))
client.getAccountDataAsync(GetAccountDataRequest(credentials, options))
}

View file

@ -1,24 +0,0 @@
package net.codinux.banking.client.service
import net.codinux.banking.client.model.AccountTransaction
open class BankingModelService {
/**
* It's not possible to retrieve only new transactions from bank server (almost no bank implements HKKAN job). So
* for updating account transactions we start at the date of latest account transactions retrieval time (e.g.
* transactions have at last been fetched at 01. September 12:00, then there may have been some other transactions
* been booked on September 1st after 12:00 o'clock).
*
* Therefore retrieved account transactions may contain transactions that we already have locally. This method filters
* from [retrievedTransactions] those already in [existingTransactions] and returns only that ones, that are not in
* [existingTransactions].
*/
open fun findNewTransactions(retrievedTransactions: List<AccountTransaction>, existingTransactions: List<AccountTransaction>): List<AccountTransaction> {
val existingTransactionsByIdentifier = existingTransactions.associateBy { it.identifier }
val existingTransactionsIdentifiers = existingTransactionsByIdentifier.keys
return retrievedTransactions.filter { transaction -> existingTransactionsIdentifiers.contains(transaction.identifier) == false }
}
}

View file

@ -1,59 +1,21 @@
package net.codinux.banking.client
import kotlinx.coroutines.runBlocking
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.tan.TanMethodType
/* BankingClient */
fun BankingClient.getAccountData(bankCode: String, loginName: String, password: String) = runBlocking {
getAccountDataAsync(bankCode, loginName, password)
this@getAccountData.getAccountDataAsync(bankCode, loginName, password)
}
fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
getAccountDataAsync(request)
this@getAccountData.getAccountDataAsync(request)
}
fun BankingClient.updateAccountTransactions(bank: BankAccess, accounts: List<BankAccount>? = null, preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications) = runBlocking {
updateAccountTransactionsAsync(bank, accounts, preferredTanMethodsIfSelectedTanMethodIsNotAvailable)
}
fun BankingClient.transferMoney(bankCode: String, loginName: String, password: String, recipientName: String,
recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null) = runBlocking {
transferMoneyAsync(bankCode, loginName, password, recipientName, recipientAccountIdentifier, amount, paymentReference)
}
fun BankingClient.transferMoney(request: TransferMoneyRequestForUser) = runBlocking {
transferMoneyAsync(request)
}
/* BankingClientForUser */
fun BankingClientForUser.getAccountData() = runBlocking {
getAccountDataAsync()
this@getAccountData.getAccountDataAsync()
}
fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlocking {
getAccountDataAsync(options)
}
fun BankingClientForUser.updateAccountTransactions(accounts: List<BankAccount>? = null, preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications) = runBlocking {
updateAccountTransactionsAsync(accounts, preferredTanMethodsIfSelectedTanMethodIsNotAvailable)
}
fun BankingClientForUser.transferMoney(recipientName: String, recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null) = runBlocking {
transferMoneyAsync(recipientName, recipientAccountIdentifier, amount, paymentReference)
}
fun BankingClientForUser.transferMoney(request: TransferMoneyRequest) = runBlocking {
transferMoneyAsync(request)
this@getAccountData.getAccountDataAsync(options)
}

View file

@ -79,8 +79,6 @@ kotlin {
val kotlinxDateTimeVersion: String by project
val jsJodaTimeZoneVersion: String by project
val ionspinBigNumVersion: String by project
sourceSets {
commonMain {
@ -104,26 +102,12 @@ kotlin {
jsMain {
dependencies {
api(npm("@js-joda/timezone", jsJodaTimeZoneVersion))
implementation(npm("big.js", "6.0.3"))
}
}
jsTest { }
nativeMain { }
nativeTest { }
linuxMain {
dependencies {
implementation("com.ionspin.kotlin:bignum:$ionspinBigNumVersion")
}
}
mingwMain {
dependencies {
implementation("com.ionspin.kotlin:bignum:$ionspinBigNumVersion")
}
}
}
}

View file

@ -1,43 +0,0 @@
package net.codinux.banking.client.model
import platform.Foundation.NSDecimalNumber
import net.codinux.banking.client.model.config.NoArgConstructor
import platform.Foundation.NSDecimalNumberHandler
import platform.Foundation.NSRoundingMode
@NoArgConstructor
actual class Amount actual constructor(amount: String) {
actual companion object {
actual val Zero = Amount("0.00")
private val handler = NSDecimalNumberHandler.decimalNumberHandlerWithRoundingMode(roundingMode = NSRoundingMode.NSRoundBankers, scale = DecimalPrecision.toShort(), false, false, false, false)
}
internal val amount = NSDecimalNumber(string = amount)
actual operator fun plus(other: Amount): Amount =
Amount(amount.decimalNumberByAdding(other.amount).stringValue)
actual operator fun minus(other: Amount): Amount =
Amount(amount.decimalNumberBySubtracting(other.amount).stringValue)
actual operator fun times(other: Amount): Amount =
Amount(amount.decimalNumberByMultiplyingBy(other.amount).stringValue)
actual operator fun div(other: Amount): Amount =
Amount(amount.decimalNumberByDividingBy(other.amount, handler).stringValue)
override fun equals(other: Any?): Boolean {
return other is Amount && this.amount == other.amount
}
override fun hashCode() =
amount.hashCode()
actual override fun toString(): String = amount.stringValue
}

View file

@ -1,15 +1,13 @@
package net.codinux.banking.client.model
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class AccountTransaction(
val amount: Amount = Amount.Zero, // TODO: a string is really bad in UI, find a better solution
val amount: Amount = Amount.Zero,
val currency: String,
val reference: String?, // Alternative: Remittance information, Transaction description, (payment) purpose, payment reference
val reference: String, // Alternative: purpose (or Remittance information)
/**
* Transaction date (Buchungstag) - der Tag, an dem ein Zahlungsvorgang in das System einer Bank eingegangen ist.
@ -24,130 +22,45 @@ open class AccountTransaction(
*/
val valueDate: LocalDate,
// deutsche Begriffe: "Transaktionspartei" oder "Beteiligte Partei"
// Englische: Transaction party (ist die beste Wahl für eine neutrale und übergreifende Beschreibung),
// Counterparty (ist nützlich in formellen oder finanziellen Kontexten), Participant (ist breiter gefasst, aber weniger präzise)
/**
* Name of the Payer (debtor) or Payee (creditor)
*/
val otherPartyName: String? = null, // Alternatives: Parties involved, Transaction parties.single names: Beneficiary, Payee respectively Payer, Debtor
/**
* Bank Identifier, in most cases BIC, of the Payer or Payee
*/
val otherPartyBankId: String? = null,
/**
* Account Identifier, in most cases IBAN, of the Payer or Payee
*/
val otherPartyBankCode: String? = null,
val otherPartyAccountId: String? = null,
/**
* Buchungstext, z. B. DAUERAUFTRAG, BARGELDAUSZAHLUNG, ONLINE-UEBERWEISUNG, FOLGELASTSCHRIFT, ...
*/
val postingText: String? = null,
val bookingText: String? = null,
val information: String? = null,
val statementNumber: Int? = null,
val sequenceNumber: Int? = null,
val openingBalance: Amount? = null,
val closingBalance: Amount? = null,
/**
* Auszugsnummer
*/
val statementNumber: Int? = null,
/**
* Blattnummer
*/
val sheetNumber: Int? = null,
/**
* Kundenreferenz.
*/
val customerReference: String? = null,
/**
* Bankreferenz
*/
val bankReference: String? = null,
/**
* Währungsart und Umsatzbetrag in Ursprungswährung
*/
val furtherInformation: String? = null,
/* Information mostly of direct debit (Lastschrift) */
val endToEndReference: String? = null,
val customerReference: String? = null,
val mandateReference: String? = null,
val creditorIdentifier: String? = null,
val originatorsIdentificationCode: String? = null,
/**
* Summe aus Auslagenersatz und Bearbeitungsprovision bei einer nationalen Rücklastschrift
* sowie optionalem Zinsausgleich.
*/
val compensationAmount: String? = null,
/**
* Betrag der ursprünglichen Lastschrift
*/
val originalAmount: String? = null,
/**
* Abweichender Überweisender oder Zahlungsempfänger
*/
val sepaReference: String? = null,
val deviantOriginator: String? = null,
/**
* Abweichender Zahlungsempfänger oder Zahlungspflichtiger
*/
val deviantRecipient: String? = null,
val referenceWithNoSpecialType: String? = null,
val primaNotaNumber: String? = null,
val textKeySupplement: String? = null,
/**
* Primanoten-Nr.
*/
val journalNumber: String? = null,
/**
* Bei R-Transaktionen siehe Tabelle der
* SEPA-Rückgabecodes, bei SEPALastschriften siehe optionale Belegung
* bei GVC 104 und GVC 105 (GVC = Geschäftsvorfallcode)
*/
val textKeyAddition: String? = null,
val currencyType: String? = null,
val bookingKey: String? = null,
val referenceForTheAccountOwner: String? = null,
val referenceOfTheAccountServicingInstitution: String? = null,
val supplementaryDetails: String? = null,
/**
* Referenznummer, die vom Sender als eindeutige Kennung für die Nachricht vergeben wurde
* (z.B. als Referenz auf stornierte Nachrichten).
*/
val orderReferenceNumber: String? = null,
/**
* Bezugsreferenz
*/
val referenceNumber: String? = null,
val transactionReferenceNumber: String? = null,
val relatedReferenceNumber: String? = null,
/**
* Storno, ob die Buchung storniert wurde(?).
* Aus:
* RC = Storno Haben
* RD = Storno Soll
*/
val isReversal: Boolean = false,
var userSetReference: String? = null,
var userSetOtherPartyName: String? = null,
var userSetDisplayName: String? = null,
var category: String? = null,
var notes: String? = null,
) {
open val identifier by lazy {
"$amount $currency $bookingDate $valueDate $reference $otherPartyName $otherPartyBankId $otherPartyAccountId"
}
@get:JsonIgnore
open val displayedReference: String?
get() = userSetReference ?: referenceNumber
@get:JsonIgnore
open val displayedOtherPartyName: String?
get() = userSetOtherPartyName ?: otherPartyName
@get:JsonIgnore
open val displayedOtherPartyNameOrPostingText: String?
get() = displayedOtherPartyName ?: postingText
override fun toString() = "${valueDate.dayOfMonth}.${valueDate.monthNumber}.${valueDate.year} ${amount.toString().padStart(4, ' ')} ${if (currency == "EUR") "€" else currency} ${otherPartyName ?: ""} - $reference"
}

View file

@ -1,44 +1,18 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
import kotlin.jvm.JvmInline
internal const val DecimalPrecision = 20 // 20 to match Big.js's behavior
fun Amount.toFloat() =
this.toString().toFloat()
fun Amount.toDouble() =
this.toString().toDouble()
val Amount.isNegative: Boolean
get() = this.toString().startsWith("-")
fun Collection<Amount>.sum(): Amount {
var sum: Amount = Amount.Zero
for (element in this) {
sum += element
}
return sum
}
@JvmInline
@NoArgConstructor
expect class Amount(amount: String = "0") {
value class Amount(val amount: String = "0") {
companion object {
val Zero: Amount
val Zero = Amount("0")
fun fromString(amount: String): Amount = Amount(amount)
}
operator fun plus(other: Amount): Amount
operator fun minus(other: Amount): Amount
operator fun times(other: Amount): Amount
operator fun div(other: Amount): Amount
override fun toString(): String
override fun toString() = amount
}

View file

@ -1,127 +0,0 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.tan.TanMedium
import net.codinux.banking.client.model.tan.TanMethod
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class BankAccess(
/**
* The country specific bank code, like in Germany the Bankleitzahl, in USA the Routing Number, in Great Britain
* the Sort Code, in India the FSC Code, ...
*/
val domesticBankCode: String,
var loginName: String,
/**
* User may decides to not save password.
*/
var password: String?,
val bankName: String,
val bic: String?, // not all banks (in the world) have a BIC
val customerName: String,
/**
* The customer is the person or organisation that owns the account at the bank.
*
* The user is the person that access a customer's account via e.g. FinTS. The user may but not necessarily is
* identical with the customer. E.g. an employee from the bookkeeping departement or a tax consultant may only
* access an account for someone else.
*
* So in most cases the userId is identical with the customerId = loginName in our speech, but there are rare cases
* where the userId differs from customerId.
*/
var userId: String? = null,
open val accounts: List<BankAccount> = emptyList(),
/**
* Identifier of selected TanMethod.
*
* As [tanMethods] also contains selected TanMethod, we didn't want to duplicate this object. Use
* [selectedTanMethod] to get selected TanMethod or iterate over [tanMethods] and filter selected one by this id.
*/
var selectedTanMethodIdentifier: String? = null,
open val tanMethods: MutableList<out TanMethod> = mutableListOf(),
/**
* Identifier of selected TanMedium.
*
* As [tanMedia] also contains selected TanMedium, we didn't want to duplicate this object. Use [selectedTanMedium]
* to get selected TanMedium or iterate over [tanMedia] and filter selected one by this medium name.
*/
var selectedTanMediumIdentifier: String? = null,
open val tanMedia: MutableList<out TanMedium> = mutableListOf(),
var bankingGroup: BankingGroup? = null,
open var serverAddress: String? = null,
/**
* The ISO code of the country where the bank resides and to know the system of [domesticBankCode].
*/
val countryCode: String = "de"
) {
var userSetDisplayName: String? = null
var displayIndex: Int = 0
var iconUrl: String? = null
var wrongCredentialsEntered: Boolean = false
/**
* BankingClient specific data of this account that the client needs to fulfill its job.
*
* You should treat it as opaque data, that only makes sense to the BankingClient, and pass it back to the client if set.
*
* For fints4k e.g. contains the FinTS jobs the bank supports, FinTS specific data like KundensystemID and so on.
*
* The deserialized in-memory only value of [serializedClientData] so that we don't have to deserialize [serializedClientData] each time.
*/
var clientData: Any? = null
/**
* Serialized version of [clientData].
*
* The same as with [clientData] you should treat this value as opaque that only makes sense to the client implementation.
*
* [clientData] is the deserialized in-memory model of this value, so that we don't have to serialize this value each time.
* serializedClientData is the serialized version of clientData so that you can store (and restore) it e.g. to your
* database, so that on next application start client implementation doesn't have to fetch all these data again.
* Speeds up e.g. getting account transactions and transferring money with fints4k as FinTS requires quite a lot of
* data before account transactions can be retrieved.
*/
var serializedClientData: String? = null
@get:JsonIgnore
open val displayName: String
get() = userSetDisplayName ?: bankName
@get:JsonIgnore
open val accountsSorted: List<out BankAccount>
get() = accounts.sortedBy { it.displayIndex }
@get:JsonIgnore
open val tanMethodsSorted: List<out TanMethod>
get() = tanMethods.sortedBy { it.identifier }
@get:JsonIgnore
open val tanMediaSorted: List<out TanMedium>
get() = tanMedia.sortedBy { it.status }
@get:JsonIgnore
val selectedTanMethod: TanMethod
get() = tanMethods.first { it.identifier == selectedTanMethodIdentifier }
@get:JsonIgnore
val selectedTanMedium: TanMedium?
get() = tanMedia.firstOrNull { it.identifier == selectedTanMediumIdentifier }
override fun toString() = "$bankName $loginName, ${accounts.size} accounts"
}

View file

@ -1,34 +1,32 @@
package net.codinux.banking.client.model
import kotlinx.datetime.*
import net.codinux.banking.client.model.config.JsonIgnore
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.securitiesaccount.Holding
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class BankAccount(
val identifier: String,
val subAccountNumber: String? = null,
val iban: String? = null,
val productName: String? = null,
var accountHolderName: String,
val type: BankAccountType = BankAccountType.Other,
val currency: String = DefaultValues.DefaultCurrency,
val iban: String? = null,
val subAccountNumber: String? = null,
val productName: String? = null,
val currency: String = "EUR",
var accountLimit: String? = null,
val isAccountTypeSupportedByApplication: Boolean = false,
val isAccountTypeSupportedByApplication: Boolean = true,
val features: Set<BankAccountFeatures> = emptySet(),
var balance: Amount = Amount.Zero,
val serverTransactionsRetentionDays: Int? = null,
open var lastAccountUpdateTime: Instant? = null,
// var balance: BigDecimal = BigDecimal.ZERO,
var balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
var retrievedTransactionsFrom: LocalDate? = null,
var retrievedTransactionsTo: LocalDate? = null,
open val bookedTransactions: MutableList<out AccountTransaction> = mutableListOf(),
open val prebookedTransactions: MutableList<out PrebookedAccountTransaction> = mutableListOf(),
open val holdings: MutableList<out Holding> = mutableListOf(),
var haveAllTransactionsBeenRetrieved: Boolean = false,
val countDaysForWhichTransactionsAreKept: Int? = null,
open val bookedTransactions: MutableList<AccountTransaction> = mutableListOf(),
open val unbookedTransactions: MutableList<UnbookedAccountTransaction> = mutableListOf(),
var userSetDisplayName: String? = null,
var displayIndex: Int = 0,
@ -36,56 +34,5 @@ open class BankAccount(
var hideAccount: Boolean = false,
var includeInAutomaticAccountsUpdate: Boolean = true
) {
@get:JsonIgnore
open val displayName: String
get() = userSetDisplayName ?: productName ?: identifier
open fun addTransactions(transactions: List<out AccountTransaction>) {
(this.bookedTransactions as MutableList<AccountTransaction>).addAll(transactions)
}
open fun addPrebookedTransactions(transactions: List<out PrebookedAccountTransaction>) {
(this.prebookedTransactions as MutableList<PrebookedAccountTransaction>).addAll(transactions)
}
open fun addHoldings(holdings: List<out Holding>) {
(this.holdings as MutableList<Holding>).addAll(holdings)
}
@get:JsonIgnore
open val supportsBalanceRetrieval: Boolean
get() = supportsAnyFeature(BankAccountFeatures.RetrieveBalance)
@get:JsonIgnore
open val supportsTransactionRetrieval: Boolean
get() = supportsAnyFeature(BankAccountFeatures.RetrieveTransactions)
@get:JsonIgnore
open val supportsMoneyTransfer: Boolean
get() = supportsAnyFeature(BankAccountFeatures.TransferMoney)
@get:JsonIgnore
open val supportsInstantTransfer: Boolean
get() = supportsAnyFeature(BankAccountFeatures.InstantTransfer)
open fun supportsAnyFeature(vararg features: BankAccountFeatures): Boolean =
features.any { this.features.contains(it) }
/**
* Determines if all transactions that are retained on bank server have been fetched.
*
* Does this by comparing [serverTransactionsRetentionDays] to [retrievedTransactionsFrom].
*/
open val haveAllRetainedTransactionsBeenRetrieved: Boolean by lazy {
val fromDay = retrievedTransactionsFrom
if (fromDay == null) {
false
} else {
// if countDaysForWhichTransactionsAreKeptOnBankServer is not set, we cannot know for how long bank server keeps transactions. We then assume for 90 days
val storageDays = serverTransactionsRetentionDays ?: 90
fromDay < Clock.System.now().toLocalDateTime(TimeZone.of("Europe/Berlin")).date.minus(storageDays, DateTimeUnit.DAY)
}
}
override fun toString() = "$type $identifier $productName (IBAN: $iban)"
}

View file

@ -4,5 +4,5 @@ enum class BankAccountFeatures {
RetrieveTransactions,
RetrieveBalance,
TransferMoney,
InstantTransfer
InstantPayment
}

View file

@ -8,7 +8,5 @@ open class BankAccountIdentifier(
val subAccountNumber: String?,
val iban: String?
) {
constructor(account: BankAccount) : this(account.identifier, account.subAccountNumber, account.iban)
override fun toString() = "$identifier, $iban"
}

View file

@ -1,13 +0,0 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class BankInfo(
val name: String,
val bic: String,
val serverAddress: String,
val bankingGroup: BankingGroup? = null
) {
override fun toString() = "$name $bic $bankingGroup"
}

View file

@ -1,20 +0,0 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
/**
* Contains only the basic info of a bank, just enough that a client application can display it to the user
* and the user knows exactly which bank is meant / referred to.
*
* As e.g. when adding a new account, client application has no data about the bank locally, so it can use this
* information to display it to the user.
*/
@NoArgConstructor
open class BankViewInfo(
val bankCode: String,
var loginName: String,
val bankName: String,
val bankingGroup: BankingGroup? = null
) {
override fun toString() = "$bankCode $bankName $loginName"
}

View file

@ -1,5 +0,0 @@
package net.codinux.banking.client.model
object DefaultValues {
const val DefaultCurrency = "EUR"
}

View file

@ -6,18 +6,7 @@ import kotlinx.datetime.Instant
open class MessageLogEntry(
open val type: MessageLogEntryType,
open val message: String,
open val messageWithoutSensitiveData: String? = null,
open val messageTrace: String? = null,
open val error: Throwable? = null,
open val time: Instant = Clock.System.now(),
val messageNumberString: String? = null,
val messageNumber: Int? = null,
val jobType: String? = null,
val messageCategory: String? = null,
val bank: BankAccess? = null, // TODO: make non-null
val account: BankAccount? = null
) {
override fun toString() = "$messageNumberString $jobType $messageCategory $type $message"
}
open val time: Instant = Clock.System.now()
)

View file

@ -3,5 +3,5 @@ package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class PrebookedAccountTransaction {
open class UnbookedAccountTransaction {
}

View file

@ -0,0 +1,65 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.tan.TanMedium
import net.codinux.banking.client.model.tan.TanMethod
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class UserAccount(
val bankCode: String,
var loginName: String,
/**
* User may decides to not save password .
*/
var password: String?,
val bankName: String,
val bic: String,
val customerName: String,
val userId: String = loginName,
open val accounts: List<BankAccount> = emptyList(),
/**
* Identifier of selected TanMethod.
*
* As [tanMethods] also contains selected TanMethod, we didn't want to duplicate this object. Use
* [selectedTanMethod] to get selected TanMethod or iterate over [tanMethods] and filter selected one by this id.
*/
val selectedTanMethodId: String? = null,
open val tanMethods: List<TanMethod> = listOf(),
/**
* Identifier of selected TanMedium.
*
* As [tanMedia] also contains selected TanMedium, we didn't want to duplicate this object. Use [selectedTanMedium]
* to get selected TanMedium or iterate over [tanMedia] and filter selected one by this medium name.
*/
val selectedTanMediumName: String? = null,
open val tanMedia: List<TanMedium> = listOf(),
var bankingGroup: BankingGroup? = null,
var iconUrl: String? = null,
) {
var wrongCredentialsEntered: Boolean = false
var userSetDisplayName: String? = null
var displayIndex: Int = 0
@get:JsonIgnore
val selectedTanMethod: TanMethod
get() = tanMethods.first { it.identifier == selectedTanMethodId }
@get:JsonIgnore
val selectedTanMedium: TanMedium?
get() = tanMedia.firstOrNull { it.mediumName == selectedTanMediumName }
override fun toString() = "$bankName $loginName, ${accounts.size} accounts"
}

View file

@ -0,0 +1,17 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
/**
* Contains only the basic info of a [UserAccount], just enough that a client application can display it to the user
* and the user knows exactly which [UserAccount] is meant / referred.
*/
@NoArgConstructor
open class UserAccountViewInfo(
val bankCode: String,
var loginName: String,
val bankName: String,
val bankingGroup: BankingGroup? = null
) {
override fun toString() = "$bankCode $bankName $loginName"
}

View file

@ -1,14 +0,0 @@
package net.codinux.banking.client.model.extensions
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
fun LocalDate.minusDays(days: Int): LocalDate {
return this.minus(days, DateTimeUnit.DAY)
}
val TimeZone.Companion.EuropeBerlin: TimeZone
get() = TimeZone.of("Europe/Berlin")

View file

@ -27,16 +27,14 @@ open class GetAccountDataOptions(
* likes to use a different one, she can select another one in EnterTanDialog.
*
* By default we prefer non visual TanMethods (like AppTan and SMS) over image based TanMethods (like QR-code and
* photoTan) and exclude ChipTanUsb, which is not supported by application, and FlickerCode, which is hard to
* photoTan) and exclude ChipTanUsb, which is not supported by application, and Flickercode, which is hard to
* implement and therefore most applications have not implemented.
*
* Console apps can only handle non visual TanMethods.
* But also graphical applications prefer non visual TanMethods as then they only have to display a text field to input
* TAN, and then image based TanMethods as then they additionally only have to display an image.
*/
val preferredTanMethods: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications,
val tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications,
val preferredTanMethods: List<TanMethodType>? = TanMethodType.NonVisualOrImageBased,
val abortIfTanIsRequired: Boolean = false,

View file

@ -1,12 +1,11 @@
package net.codinux.banking.client.model.request
import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.client.model.BankInfo
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.options.GetAccountDataOptions
@NoArgConstructor
open class GetAccountDataRequest(bankCode: String, loginName: String, password: String, val options: GetAccountDataOptions? = null, val bankInfo: BankInfo? = null)
open class GetAccountDataRequest(bankCode: String, loginName: String, password: String, val options: GetAccountDataOptions? = null)
: AccountCredentials(bankCode, loginName, password) {
constructor(credentials: AccountCredentials, options: GetAccountDataOptions? = null)

View file

@ -1,82 +0,0 @@
package net.codinux.banking.client.model.request
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccountIdentifier
import net.codinux.banking.client.model.DefaultValues
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.tan.TanMethodType
@NoArgConstructor
open class TransferMoneyRequest(
/* Sender settings */
/**
* The account from which the money should be withdrawn.
* If not specified client retrieves all bank accounts and checks if there is exactly one that supports money transfer.
* If no or more than one bank account supports money transfer, the error codes NoAccountSupportsMoneyTransfer or MoreThanOneAccountSupportsMoneyTransfer are returned.
*/
open val senderAccount: BankAccountIdentifier? = null,
/* Recipient settings */
open val recipientName: String,
/**
* The identifier of recipient's account. In most cases the IBAN.
*/
open val recipientAccountIdentifier: String,
/**
* The identifier of recipient's bank. In most cases the BIC.
* Can be omitted for German banks as the BIC can be derived from IBAN.
*/
open val recipientBankIdentifier: String? = null,
/* Transfer data */
open val amount: Amount,
open val currency: String = DefaultValues.DefaultCurrency,
/**
* The purpose of payment. An optional value that tells the reason for the transfer.
*
* May not be longer than 140 characters. Some characters are forbidden (TODO: add reference of forbidden characters).
*/
open val paymentReference: String? = null, // Alternativ: Purpose of payment
/**
* If transfer should be executed as 'real-time transfer', that is the money is in less than 10 seconds
* transferred to the account of the recipient.
*
* May costs extra fees.
*
* Not supported by all sender and recipient banks.
*/
open val instantTransfer: Boolean = false, // Alternativ: Instant payment ("Instant payment" ist ebenfalls weit verbreitet und wird oft im Kontext von digitalen Zahlungen verwendet, bei denen die Zahlung in Echtzeit erfolgt. Es kann jedoch breiter gefasst sein und umfasst nicht nur Banktransfers, sondern auch andere Arten von Sofortzahlungen (z.B. mobile Zahlungen).)
/**
* Specifies which [TanMethodType] should be preferred when having to choose between multiple available for user
* without requesting the user to choose one.
*
* By default we don't ask the user which TanMethod she prefers but choose one that could match best. If she really
* likes to use a different one, she can select another one in EnterTanDialog.
*
* By default we prefer non visual TanMethods (like AppTan and SMS) over image based TanMethods (like QR-code and
* photoTan) and exclude ChipTanUsb, which is not supported by application, and FlickerCode, which is hard to
* implement and therefore most applications have not implemented.
*
* Console apps can only handle non visual TanMethods.
* But also graphical applications prefer non visual TanMethods as then they only have to display a text field to input
* TAN, and then image based TanMethods as then they additionally only have to display an image.
*/
val preferredTanMethods: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications,
val tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications,
val clientData: Any? = null,
var serializedClientData: String? = null
) {
override fun toString() = "$amount to $recipientName - $paymentReference"
}

View file

@ -1,70 +0,0 @@
package net.codinux.banking.client.model.request
import net.codinux.banking.client.model.*
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.tan.TanMethodType
/**
* For documentation see [TransferMoneyRequest].
*/
@NoArgConstructor
open class TransferMoneyRequestForUser(
/* Sender settings */
val bankCode: String,
val loginName: String,
val password: String,
senderAccount: BankAccountIdentifier? = null,
/* Recipient settings */
recipientName: String,
recipientAccountIdentifier: String,
recipientBankIdentifier: String? = null,
/* Transfer data */
amount: Amount,
currency: String = DefaultValues.DefaultCurrency,
paymentReference: String? = null,
instantTransfer: Boolean = false,
preferredTanMethods: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications,
tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications,
clientData: Any? = null,
serializedClientData: String? = null
) : TransferMoneyRequest(senderAccount, recipientName, recipientAccountIdentifier, recipientBankIdentifier, amount, currency, paymentReference, instantTransfer, preferredTanMethods, tanMethodsNotSupportedByApplication, clientData, serializedClientData) {
constructor(bankCode: String, loginName: String, password: String, request: TransferMoneyRequest)
: this(bankCode, loginName, password, request.senderAccount, request.recipientName, request.recipientAccountIdentifier, request.recipientBankIdentifier,
request.amount, request.currency, request.paymentReference, request.instantTransfer, request.preferredTanMethods, request.tanMethodsNotSupportedByApplication)
constructor(
bank: BankAccess, account: BankAccount?,
recipientName: String, recipientAccountIdentifier: String, recipientBankIdentifier: String? = null,
amount: Amount, currency: String = DefaultValues.DefaultCurrency, paymentReference: String? = null, instantTransfer: Boolean = false,
preferredTanMethods: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications
) : this(bank.domesticBankCode, bank.loginName, bank.password!!, account?.let { BankAccountIdentifier(it.identifier, it.subAccountNumber, it.iban) },
recipientName, recipientAccountIdentifier, recipientBankIdentifier, amount, currency, paymentReference, instantTransfer,
listOf(bank.selectedTanMethod.type) + (preferredTanMethods ?: emptyList()), TanMethodType.TanMethodsNotSupportedByMostApplications,
bank.clientData, bank.serializedClientData
) {
this.bank = bank
this.account = account
}
open var bank: BankAccess? = null
protected set
open var account: BankAccount? = null
protected set
override fun toString() = "$bankCode $loginName ${super.toString()}"
}

View file

@ -1,21 +1,20 @@
package net.codinux.banking.client.model.response
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.MessageLogEntry
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class GetAccountDataResponse(
val bank: BankAccess
val user: UserAccount
) {
@get:JsonIgnore
val bookedTransactions: List<AccountTransaction>
get() = bank.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
get() = user.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
override fun toString() = bank.toString()
override fun toString() = user.toString()
}

View file

@ -1,24 +0,0 @@
package net.codinux.banking.client.model.response
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
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(
val account: BankAccount,
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
) {
override fun toString() = "${account.productName} $balance, ${bookedTransactions.size} booked transactions from $retrievedTransactionsFrom"
}

View file

@ -1,6 +1,5 @@
package net.codinux.banking.client.model.response
import net.codinux.banking.client.model.MessageLogEntry
import net.codinux.banking.client.model.config.NoArgConstructor
// TODO: may differentiate between ClientResponse, which is either Success or Error, and RestResponse, which can be Success, Error and TanRequired
@ -9,22 +8,21 @@ open class Response<T> protected constructor(
val type: ResponseType,
val data: T? = null,
val error: Error? = null,
val tanRequired: TanRequired? = null,
val messageLog: List<MessageLogEntry> = emptyList()
val tanRequired: TanRequired? = null
) {
companion object {
fun <T> success(data: T, messageLog: List<MessageLogEntry> = emptyList()): Response<T> =
Response(ResponseType.Success, data, messageLog = messageLog)
fun <T> success(data: T): Response<T> =
Response(ResponseType.Success, data)
fun <T> error(errorType: ErrorType, internalError: String? = null, errorMessagesFromBank: List<String> = emptyList(), messageLog: List<MessageLogEntry> = emptyList()): Response<T> =
Response(ResponseType.Error, null, Error(errorType, internalError, errorMessagesFromBank), messageLog = messageLog)
fun <T> error(errorType: ErrorType, internalError: String? = null, errorMessagesFromBank: List<String> = emptyList()): Response<T> =
Response(ResponseType.Error, null, Error(errorType, internalError, errorMessagesFromBank))
fun <T> tanRequired(tanRequired: TanRequired, messageLog: List<MessageLogEntry> = emptyList()): Response<T> =
Response(ResponseType.TanRequired, null, null, tanRequired, messageLog)
fun <T> tanRequired(tanRequired: TanRequired): Response<T> =
Response(ResponseType.TanRequired, null, null, tanRequired)
fun <T> bankReturnedError(errorMessagesFromBank: List<String>, messageLog: List<MessageLogEntry> = emptyList()): Response<T> =
Response.error(ErrorType.BankReturnedError, null, errorMessagesFromBank, messageLog)
fun <T> bankReturnedError(errorMessagesFromBank: List<String>): Response<T> =
Response.error(ErrorType.BankReturnedError, null, errorMessagesFromBank)
}

View file

@ -1,10 +0,0 @@
package net.codinux.banking.client.model.response
import net.codinux.banking.client.model.MessageLogEntry
import net.codinux.banking.client.model.config.NoArgConstructor
/**
* Transfer money process does not return any data, only if successful or not (and in latter case an error message).
*/
@NoArgConstructor
open class TransferMoneyResponse

View file

@ -1,53 +0,0 @@
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(
open var name: String,
open var isin: String? = null,
open var wkn: String? = null,
open var quantity: Double? = null,
open var currency: String? = null,
/**
* Gesamter Kurswert aller Einheiten des Wertpapiers
*/
open var totalBalance: Amount? = null,
/**
* Aktueller Kurswert einer einzelnen Einheit des Wertpapiers
*/
open var marketValue: Amount? = null,
/**
* Änderung in Prozent Aktueller Kurswert gegenüber Einstandspreis.
*/
open var performancePercentage: Float? = null,
/**
* Gesamter Einstandspreis (Kaufpreis)
*/
open var totalCostPrice: Amount? = null,
/**
* (Durchschnittlicher) Einstandspreis/-kurs einer Einheit des Wertpapiers
*/
open var averageCostPrice: Amount? = null,
/**
* Zeitpunkt zu dem der Kurswert bestimmt wurde
*/
open var pricingTime: Instant? = null,
open var buyingDate: LocalDate? = null,
var userSetDisplayName: String? = null,
) {
open val identifier: String by lazy { "${isin}_$wkn" }
override fun toString() = "$name $totalBalance $currency"
}

View file

@ -5,7 +5,7 @@ import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class EnterTanResult(
val enteredTan: String?,
val changeTanMethodTo: TanMethod? = null,
// val changeTanMethodTo: TanMethod? = null,
// val changeTanMediumTo: TanMedium? = null,
// val changeTanMediumResultCallback: ((BankingClientResponse) -> Unit)? = null
)

View file

@ -7,13 +7,13 @@ import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class FlickerCode(
val challengeHHD_UC: String,
val parsedDataSet: String? = null,
val parsedDataSet: String,
val decodingError: String? = null
) {
@get:JsonIgnore
val decodingSuccessful: Boolean
get() = parsedDataSet != null
get() = decodingError == null
override fun toString(): String {

View file

@ -1,19 +1,17 @@
package net.codinux.banking.client.model.tan
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import net.codinux.banking.client.model.BankAccountViewInfo
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.BankViewInfo
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.UserAccountViewInfo
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class TanChallenge(
open val type: TanChallengeType,
open val forAction: ActionRequiringTan,
open val messageToShowToUser: String,
val type: TanChallengeType,
val forAction: ActionRequiringTan,
val messageToShowToUser: String,
/**
* Identifier of selected TanMethod.
@ -21,16 +19,16 @@ open class TanChallenge(
* As [availableTanMethods] also contains selected TanMethod, we didn't want to duplicate this object. Use
* [selectedTanMethod] to get selected TanMethod or iterate over [availableTanMethods] and filter selected one by this id.
*/
open val selectedTanMethodIdentifier: String,
val selectedTanMethodId: String,
/**
* When adding an account, frontend has no UserAccount object in BankingClientCallback to know which TanMethods are
* available for user.
* available for User.
* Also on other calls to bank server, bank server may returned an updated list of available TanMethods, so that
* [BankAccess] may contains an outdated list of available TanMethods.
* [UserAccount] may contains an outdated list of available TanMethods.
*
* Therefore i added list with up to date TanMethods here to ensure EnterTanDialog can display user's up to date TanMethods.
*/
open val availableTanMethods: List<TanMethod>,
val availableTanMethods: List<TanMethod>,
/**
* Identifier of selected TanMedium.
@ -38,57 +36,29 @@ open class TanChallenge(
* As [availableTanMedia] also contains selected TanMedium, we didn't want to duplicate this object. Use
* [selectedTanMedium] to get selected TanMedium or iterate over [availableTanMedia] and filter selected one by this medium name.
*/
open val selectedTanMediumIdentifier: String? = null,
open val availableTanMedia: List<TanMedium> = emptyList(),
val selectedTanMediumName: String? = null,
val availableTanMedia: List<TanMedium> = emptyList(),
open val tanImage: TanImage? = null,
open val flickerCode: FlickerCode? = null,
open val bank: BankViewInfo,
open val account: BankAccountViewInfo? = null,
/**
* Datum und Uhrzeit, bis zu welchem Zeitpunkt eine TAN auf Basis der gesendeten Challenge gültig ist. Nach Ablauf der Gültigkeitsdauer wird die entsprechende TAN entwertet.
*
* In server's time zone, that is Europe/Berlin.
*/
val tanExpirationTime: Instant? = null,
val challengeCreationTimestamp: Instant = Clock.System.now()
val tanImage: TanImage? = null,
val flickerCode: FlickerCode? = null,
val user: UserAccountViewInfo,
val account: BankAccountViewInfo? = null
) {
@get:JsonIgnore
open val selectedTanMethod: TanMethod
get() = availableTanMethods.first { it.identifier == selectedTanMethodIdentifier }
val selectedTanMethod: TanMethod
get() = availableTanMethods.first { it.identifier == selectedTanMethodId }
@get:JsonIgnore
open val selectedTanMedium: TanMedium?
get() = availableTanMedia.firstOrNull { it.mediumName == selectedTanMediumIdentifier }
/**
* Not implemented for all client, only implementing client for now: FinTs4jBankingClient.
*
* If a TAN expires either when [TanChallenge.tanExpirationTime] or a default timeout (15 min) is exceeded,
* you can add a callback to get notified when TAN expired e.g. to close a EnterTanDialog.
*/
open fun addTanExpiredCallback(callback: () -> Unit) {
}
/**
* Principally a no-op method, not implemented for all client, only implementing client for now: FinTs4jBankingClient.
*
* If a TAN is requested for a decoupled TAN method like [TanMethodType.DecoupledTan] or [TanMethodType.DecoupledPushTan],
* you can add a callback to get notified when user approved TAN in her app e.g. to close a EnterTanDialog.
*/
open fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) {
}
val selectedTanMedium: TanMedium?
get() = availableTanMedia.firstOrNull { it.mediumName == selectedTanMediumName }
override fun toString(): String {
return "$selectedTanMethod $forAction: $messageToShowToUser" + when (type) {
TanChallengeType.EnterTan -> ""
TanChallengeType.Image -> ", Image: $tanImage"
TanChallengeType.FlickerCode -> ", FlickerCode: $flickerCode"
TanChallengeType.Flickercode -> ", FlickerCode: $flickerCode"
}
}

View file

@ -3,7 +3,7 @@ package net.codinux.banking.client.model.tan
enum class TanChallengeType {
Image,
FlickerCode,
Flickercode,
EnterTan
}

View file

@ -4,10 +4,6 @@ import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
/**
* 'TanGenerator' is in most cases a debit card, but can also be something like "BestSign" app (Postbank).
* In latter case [cardNumber] can also be, contrary to specification, be an empty string.
*/
open class TanGeneratorTanMedium(
val cardNumber: String,
val cardSequenceNumber: String? = null,

View file

@ -6,22 +6,22 @@ import net.codinux.banking.client.model.config.NoArgConstructor
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class TanImage(
val mimeType: String? = null,
val imageBytesBase64: String? = null,
val mimeType: String,
val imageBytesBase64: String,
val decodingError: String? = null
) {
@get:JsonIgnore
val decodingSuccessful: Boolean
get() = mimeType != null && imageBytesBase64 != null
get() = decodingError == null
override fun toString(): String {
mimeType?.let {
return mimeType
if (decodingSuccessful == false) {
return "Decoding error: $decodingError"
}
return "Decoding error: $decodingError"
return mimeType
}
}

View file

@ -14,54 +14,7 @@ open class TanMedium(
/**
* Only set if [type] is [TanMediumType.MobilePhone].
*/
val mobilePhone: MobilePhoneTanMedium? = null,
var userSetDisplayName: String? = null
val mobilePhone: MobilePhoneTanMedium? = null
) {
/**
* Using only [mediumName] as identifier does not work as e.g. Sparkasse names all their TanGenerator TAN media
* "SparkassenCard (Debitkarte)" - so it's not possible to differentiate between them solely by medium name.
*/
val identifier: String by lazy {
// TODO: translate
var id = mediumName ?: when (type) {
TanMediumType.MobilePhone -> "Mobiltelefon"
TanMediumType.TanGenerator -> "Tan Generator"
TanMediumType.Generic -> "Unbenanntes TAN Medium"
}
if (mobilePhone != null) {
id += " " + (mobilePhone.concealedPhoneNumber ?: mobilePhone.phoneNumber)
}
if (tanGenerator != null) {
if (tanGenerator.cardNumber.isNotBlank()) {
id += " " + tanGenerator.cardNumber
}
if (tanGenerator.cardSequenceNumber.isNullOrBlank() == false) {
id += " " + tanGenerator.cardSequenceNumber
}
if (tanGenerator.validFrom != null && tanGenerator.validTo != null) {
id += ", gültig von " + tanGenerator.validFrom.let { "${it.dayOfMonth}.${it.monthNumber}${it.year}" } +
" - " + tanGenerator.validTo.let { "${it.dayOfMonth}.${it.monthNumber}${it.year}" }
} else if (tanGenerator.validTo != null) {
id += ", gültig bis " + tanGenerator.validTo.let { "${it.dayOfMonth}.${it.monthNumber}${it.year}" }
}
}
id
}
val displayName: String by lazy {
userSetDisplayName
?: (identifier + " " + when (status) {
TanMediumStatus.Used -> "Aktiv"
TanMediumStatus.Available -> "Verfügbar"
TanMediumStatus.ActiveFollowUpCard -> " Folgekarte, aktiv bei erster Nutzung"
TanMediumStatus.AvailableFollowUpCard -> " Folgekarte, die erst aktiviert werden muss"
})
}
override fun toString() = "$mediumName $status"
}

View file

@ -1,22 +1,14 @@
package net.codinux.banking.client.model.tan
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class TanMethod(
open val displayName: String,
open val type: TanMethodType,
open val identifier: String,
open val maxTanInputLength: Int? = null,
open val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric,
open var userSetDisplayName: String? = null
val displayName: String,
val type: TanMethodType,
val identifier: String,
val maxTanInputLength: Int? = null,
val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
) {
@get:JsonIgnore
open val isNumericTan: Boolean
get() = allowedTanFormat == AllowedTanFormat.Numeric
override fun toString() = "$displayName ($type, ${identifier})"
}

View file

@ -3,9 +3,9 @@ package net.codinux.banking.client.model.tan
enum class TanMethodType {
EnterTan,
ChipTanManual,
ChipTanManuell,
ChipTanFlickerCode,
ChipTanFlickercode,
ChipTanUsb,
@ -17,52 +17,24 @@ enum class TanMethodType {
AppTan,
DecoupledTan,
DecoupledPushTan,
photoTan,
QrCode
;
val isDecoupledMethod: Boolean
get() = this == DecoupledTan || this == DecoupledPushTan
companion object {
val NonVisual = listOf(TanMethodType.DecoupledTan, TanMethodType.DecoupledPushTan, TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.ChipTanManual, TanMethodType.EnterTan)
val NonVisual = listOf(TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.ChipTanManuell, TanMethodType.EnterTan)
val NonVisualWithoutChipTanManual = NonVisual.toMutableList().apply { remove(TanMethodType.ChipTanManual) }.toList()
val ImageBased = listOf(
TanMethodType.QrCode, TanMethodType.photoTan, // non ChipTan
TanMethodType.ChipTanQrCode, TanMethodType.ChipTanPhotoTanMatrixCode // ChipTan; QrCode (e.g. used by Sparkassen) is faster than MatrixCode (e.g. used by Volksbanken)
)
val ImageBased = listOf(TanMethodType.QrCode, TanMethodType.ChipTanQrCode, TanMethodType.photoTan, TanMethodType.ChipTanPhotoTanMatrixCode)
val NonVisualOrImageBased = buildList {
addAll(NonVisualWithoutChipTanManual)
addAll(listOf(TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.EnterTan))
addAll(ImageBased)
addAll(listOf(TanMethodType.ChipTanManual)) // this is quite inconvenient for user, so i added it as last
addAll(listOf(TanMethodType.ChipTanManuell)) // this is quite inconvenient for user, so i added it at last
}
/**
* The same as [NonVisualOrImageBased] but including [ChipTanFlickerCode] - for applications supporting it - as
* FlickerCode is still the most used ChipTan procedure.
*/
val NonVisualOrImageBasedOrFlickerCode = NonVisualOrImageBased.toMutableList().apply {
val index = this.indexOf(ChipTanQrCode)
this.add(index, ChipTanFlickerCode)
}.toList()
val TanMethodsPreferredByMostApplications = NonVisualOrImageBased
val TanMethodsNotSupportedByMostApplications = listOf(TanMethodType.ChipTanUsb)
}
}

View file

@ -1,36 +0,0 @@
package net.codinux.banking.client.model
import kotlin.test.Test
import kotlin.test.assertEquals
class AmountTest {
@Test
fun add() {
val result = Amount("0.1") + Amount("0.2")
assertEquals(Amount("0.3"), result)
}
@Test
fun minus() {
val result = Amount("0.1") - Amount("0.2")
assertEquals(Amount("-0.1"), result)
}
@Test
fun multiply() {
val result = Amount("0.1") * Amount("0.2")
assertEquals(Amount("0.02"), result)
}
@Test
fun divide() {
val result = Amount("1") / Amount("3")
assertEquals(Amount("0.33333333333333333333"), result)
}
}

View file

@ -1,51 +0,0 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@JsModule("big.js")
@JsNonModule
open external class Big(value: String) {
fun plus(other: Big): Big
fun minus(other: Big): Big
fun times(other: Big): Big
fun div(other: Big): Big
override fun toString(): String
}
@NoArgConstructor
actual class Amount actual constructor(amount: String): Big(amount) {
actual companion object {
actual val Zero = Amount("0.00")
}
actual operator fun plus(other: Amount): Amount {
return Amount(super.plus(other).toString())
}
actual operator fun minus(other: Amount): Amount {
return Amount(super.minus(other).toString())
}
actual operator fun times(other: Amount): Amount {
return Amount(super.times(other).toString())
}
actual operator fun div(other: Amount): Amount {
return Amount(super.div(other).toString())
}
override fun equals(other: Any?): Boolean {
return other is Amount && this.toString() == other.toString()
}
override fun hashCode(): Int {
return super.hashCode()
}
actual override fun toString(): String = super.toString()
}

View file

@ -1,40 +0,0 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
import java.math.BigDecimal
import java.math.RoundingMode
@NoArgConstructor
actual class Amount actual constructor(amount: String) : BigDecimal(amount) {
actual companion object {
actual val Zero = Amount("0.00")
}
actual operator fun plus(other: Amount): Amount =
Amount(this.add(other).toString())
actual operator fun minus(other: Amount): Amount =
Amount(this.subtract(other).toString())
actual operator fun times(other: Amount): Amount =
Amount(this.multiply(other).toString())
actual operator fun div(other: Amount): Amount =
// without RoundingMode a java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. will be thrown
Amount(this.divide(other, DecimalPrecision, RoundingMode.HALF_UP).toString()) // 20 to match Big.js's behaviour
/* why are these methods required when deriving from BigDecimal? */
override fun toByte(): Byte {
1 + 1
return 0 // will never be called; where is this method coming from?
}
override fun toShort(): Short {
return 0 // will never be called; where is this method coming from?
}
}

View file

@ -1,47 +0,0 @@
package net.codinux.banking.client.model
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import com.ionspin.kotlin.bignum.decimal.DecimalMode
import com.ionspin.kotlin.bignum.decimal.RoundingMode
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
actual class Amount actual constructor(amount: String) {
actual companion object {
actual val Zero: Amount = Amount("0.00")
private val decimalMode = DecimalMode(DecimalPrecision.toLong(), RoundingMode.ROUND_HALF_CEILING)
}
internal val amount: BigDecimal = BigDecimal.parseString(amount)
actual operator fun plus(other: Amount): Amount {
return Amount(amount.add(other.amount).toString())
}
actual operator fun minus(other: Amount): Amount {
return Amount(amount.subtract(other.amount).toString())
}
actual operator fun times(other: Amount): Amount {
return Amount(amount.multiply(other.amount).toString())
}
actual operator fun div(other: Amount): Amount {
return Amount(amount.divide(other.amount, decimalMode).toString())
}
override fun equals(other: Any?): Boolean {
return other is Amount && this.amount == other.amount
}
override fun hashCode(): Int {
return amount.hashCode()
}
actual override fun toString(): String = amount.toPlainString()
}

View file

@ -1,47 +0,0 @@
package net.codinux.banking.client.model
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import com.ionspin.kotlin.bignum.decimal.DecimalMode
import com.ionspin.kotlin.bignum.decimal.RoundingMode
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
actual class Amount actual constructor(amount: String) {
actual companion object {
actual val Zero: Amount = Amount("0.00")
private val decimalMode = DecimalMode(DecimalPrecision.toLong(), RoundingMode.ROUND_HALF_CEILING)
}
internal val amount: BigDecimal = BigDecimal.parseString(amount)
actual operator fun plus(other: Amount): Amount {
return Amount(amount.add(other.amount).toString())
}
actual operator fun minus(other: Amount): Amount {
return Amount(amount.subtract(other.amount).toString())
}
actual operator fun times(other: Amount): Amount {
return Amount(amount.multiply(other.amount).toString())
}
actual operator fun div(other: Amount): Amount {
return Amount(amount.divide(other.amount, decimalMode).toString())
}
override fun equals(other: Any?): Boolean {
return other is Amount && this.amount == other.amount
}
override fun hashCode(): Int {
return amount.hashCode()
}
actual override fun toString(): String = amount.toPlainString()
}

View file

@ -1,53 +0,0 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@JsModule("big.js")
open external class Big(value: String) {
fun plus(other: Big): Big
fun minus(other: Big): Big
fun times(other: Big): Big
fun div(other: Big): Big
override fun toString(): String
}
@NoArgConstructor
actual class Amount actual constructor(amount: String) {
actual companion object {
actual val Zero = Amount("0.00")
}
internal val amount = Big(amount)
actual operator fun plus(other: Amount): Amount {
return Amount(amount.plus(other.amount).toString())
}
actual operator fun minus(other: Amount): Amount {
return Amount(amount.minus(other.amount).toString())
}
actual operator fun times(other: Amount): Amount {
return Amount(amount.times(other.amount).toString())
}
actual operator fun div(other: Amount): Amount {
return Amount(amount.div(other.amount).toString())
}
override fun equals(other: Any?): Boolean {
return other is Amount && amount.toString() == other.amount.toString()
}
override fun hashCode(): Int {
return super.hashCode()
}
actual override fun toString(): String = amount.toString()
}

View file

@ -4,11 +4,13 @@ import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
plugins {
kotlin("multiplatform")
id("maven-publish")
}
kotlin {
jvmToolchain(11)
jvmToolchain(8)
jvm {
withJava()
@ -32,7 +34,7 @@ kotlin {
browser {
testTask {
useKarma {
// useChromeHeadless()
useChromeHeadless()
useFirefoxHeadless()
}
}
@ -75,7 +77,7 @@ kotlin {
dependencies {
api(project(":BankingClient"))
implementation("net.codinux.banking:fints4k:1.0.0-Alpha-15")
api("net.codinux.banking:fints4k:1.0.0-Alpha-12")
api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
}
@ -113,6 +115,21 @@ kotlin {
ext["customArtifactId"] = "fints4k-banking-client"
//ext["customArtifactId"] = "fints4k-banking-client"
//
//apply(from = "../gradle/scripts/publish-codinux.gradle.kts")
apply(from = "../gradle/scripts/publish-codinux-repo.gradle.kts")
publishing {
repositories {
maven {
name = "codinux"
url = uri("https://maven.dankito.net/api/packages/codinux/maven")
credentials(PasswordCredentials::class.java) {
username = project.property("codinuxRegistryWriterUsername") as String
password = project.property("codinuxRegistryWriterPassword") as String
}
}
}
}

View file

@ -3,7 +3,7 @@ package net.codinux.banking.client.fints4k
import net.codinux.banking.client.BankingClientCallback
import net.codinux.banking.client.model.MessageLogEntryType
import net.codinux.banking.fints.callback.FinTsClientCallback
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
import net.codinux.banking.fints.model.BankData
import net.codinux.banking.fints.model.EnterTanGeneratorAtcResult
import net.codinux.banking.fints.model.MessageLogEntry
@ -22,21 +22,22 @@ open class BridgeFintTsToBankingClientCallback(
bankingClientCallback.enterTan(mapper.mapTanChallenge(tanChallenge)) { enterTanResult ->
if (enterTanResult.enteredTan != null) {
tanChallenge.userEnteredTan(enterTanResult.enteredTan!!)
} else if (enterTanResult.changeTanMethodTo != null) {
val fintsTanMethod = tanChallenge.bank.tanMethodsAvailableForUser.first { it.securityFunction.code == enterTanResult.changeTanMethodTo!!.identifier }
tanChallenge.userAsksToChangeTanMethod(fintsTanMethod)
} else {
tanChallenge.userDidNotEnterTan()
}
}
}
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanMedium): EnterTanGeneratorAtcResult {
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
return EnterTanGeneratorAtcResult.userDidNotEnterAtc()
}
override fun messageLogAdded(messageLogEntry: MessageLogEntry) {
val mapped = mapper.mapMessageLogEntry(messageLogEntry)
val mapped = net.codinux.banking.client.model.MessageLogEntry(
MessageLogEntryType.valueOf(messageLogEntry.type.name),
messageLogEntry.message, messageLogEntry.messageTrace,
messageLogEntry.error, messageLogEntry.time
)
bankingClientCallback.messageLogAdded(mapped)
}

View file

@ -5,7 +5,7 @@ import net.codinux.banking.client.BankingClientForUserBase
import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.fints.config.FinTsClientConfiguration
open class FinTs4kBankingClientForUser(credentials: AccountCredentials, config: FinTsClientConfiguration = FinTsClientConfiguration(), callback: BankingClientCallback)
open class FinTs4KBankingClientForUser(credentials: AccountCredentials, config: FinTsClientConfiguration = FinTsClientConfiguration(), callback: BankingClientCallback)
: BankingClientForUserBase(credentials, FinTs4kBankingClient(config, callback)) {
constructor(bankCode: String, loginName: String, password: String, callback: BankingClientCallback)

View file

@ -2,14 +2,10 @@ package net.codinux.banking.client.fints4k
import net.codinux.banking.client.BankingClient
import net.codinux.banking.client.BankingClientCallback
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.BankAccountFeatures
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.response.*
import net.codinux.banking.client.model.tan.TanMethodType
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
import net.codinux.banking.fints.FinTsClient
import net.codinux.banking.fints.config.FinTsClientConfiguration
@ -20,48 +16,16 @@ open class FinTs4kBankingClient(
constructor(callback: BankingClientCallback) : this(FinTsClientConfiguration(), callback)
constructor(options: FinTsClientOptions, callback: BankingClientCallback)
: this(FinTsClientConfiguration(net.codinux.banking.fints.config.FinTsClientOptions(options.collectMessageLog, false, options.removeSensitiveDataFromMessageLog, options.appendFinTsMessagesToLog, options.closeDialogs, options.version, options.productName)), callback)
protected val mapper = FinTs4kMapper()
protected open val mapper = FinTs4kMapper()
protected open val client = FinTsClient(config, BridgeFintTsToBankingClientCallback(callback, mapper))
protected val client = FinTsClient(config, BridgeFintTsToBankingClientCallback(callback, mapper))
override suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse> {
val response = client.getAccountDataAsync(mapper.mapToGetAccountDataParameter(request, request.options ?: GetAccountDataOptions()))
return mapper.map(response, request.bankInfo)
}
override suspend fun updateAccountTransactionsAsync(bank: BankAccess, accounts: List<BankAccount>?, preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>?): Response<List<GetTransactionsResponse>> {
val accountsToRequest = (accounts ?: bank.accounts).filter { it.supportsAnyFeature(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.RetrieveTransactions) }
if (accountsToRequest.isNotEmpty()) {
val responses = accountsToRequest.map { account ->
val preferredTanMethods = listOf(bank.selectedTanMethod.type) + (preferredTanMethodsIfSelectedTanMethodIsNotAvailable ?: emptyList())
val parameter = mapper.mapToUpdateAccountTransactionsParameter(bank, account, preferredTanMethods)
val response = client.getAccountDataAsync(parameter)
mapper.mapCommonResponseData(bank, response) // so that basic account data doesn't have to be retrieved another time if user has multiple accounts
Triple(account, parameter, response)
}
return mapper.map(bank, responses)
}
return Response.error(ErrorType.NoneOfTheAccountsSupportsRetrievingData, "Keines der Konten unterstützt das Abholen der Umsätze oder des Kontostands") // TODO: translate
}
override suspend fun transferMoneyAsync(request: TransferMoneyRequestForUser): Response<TransferMoneyResponse> {
val response = client.transferMoneyAsync(mapper.mapToTransferMoneyParameter(request))
return mapper.mapTransferMoneyResponse(response, request.bank, request.account)
return mapper.map(response)
}
}

View file

@ -1,22 +1,12 @@
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.MessageLogEntry
import net.codinux.banking.client.model.MessageLogEntryType
import net.codinux.banking.client.model.extensions.EuropeBerlin
import net.codinux.banking.client.model.tan.*
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.response.*
import net.codinux.banking.client.model.tan.ActionRequiringTan
import net.codinux.banking.client.model.tan.AllowedTanFormat
import net.codinux.banking.client.model.tan.TanChallenge
import net.codinux.banking.client.model.tan.TanImage
import net.codinux.banking.client.model.tan.TanMethod
@ -25,127 +15,46 @@ import net.dankito.banking.client.model.BankAccountIdentifierImpl
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.parameter.RetrieveTransactions
import net.dankito.banking.client.model.response.ErrorCode
import net.dankito.banking.client.model.response.FinTsClientResponse
import net.codinux.banking.fints.mapper.FinTsModelMapper
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.*
import net.codinux.banking.fints.model.*
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
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMediumStatus
import net.codinux.banking.fints.model.*
import net.codinux.banking.fints.transactions.swift.model.Holding
import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
open class FinTs4kMapper {
companion object {
val TanMethodTypesToMigrate = mapOf(
net.codinux.banking.fints.model.TanMethodType.ChipTanManuell.name to TanMethodType.ChipTanManual.name,
net.codinux.banking.fints.model.TanMethodType.ChipTanFlickercode.name to TanMethodType.ChipTanFlickerCode.name
)
val TanMethodTypesToMigrateReverse = TanMethodTypesToMigrate.map { it.value to it.key }.toMap()
}
protected val fintsModelMapper = FinTsModelMapper()
protected val bankingGroupMapper = BankingGroupMapper()
open fun mapToGetAccountDataParameter(request: GetAccountDataRequest, options: GetAccountDataOptions) = GetAccountDataParameter(
request.bankCode, request.loginName, request.password,
options.accounts.map { mapBankAccountIdentifier(it) },
open fun mapToGetAccountDataParameter(credentials: AccountCredentials, options: GetAccountDataOptions) = GetAccountDataParameter(
credentials.bankCode, credentials.loginName, credentials.password,
options.accounts.map { BankAccountIdentifierImpl(it.identifier, it.subAccountNumber, it.iban) },
options.retrieveBalance,
RetrieveTransactions.valueOf(options.retrieveTransactions.name), options.retrieveTransactionsFrom, options.retrieveTransactionsTo,
preferredTanMethods = options.preferredTanMethods?.map { mapTanMethodType(it) },
tanMethodsNotSupportedByApplication = options.tanMethodsNotSupportedByApplication.map { mapTanMethodType(it) },
abortIfTanIsRequired = options.abortIfTanIsRequired,
defaultBankValues = request.bankInfo?.let { mapToBankData(request, it) }
abortIfTanIsRequired = options.abortIfTanIsRequired
)
protected open fun mapToBankData(credentials: AccountCredentials, bank: BankInfo): BankData = BankData(
credentials.bankCode, credentials.loginName, credentials.password,
bank.serverAddress, bank.bic, bank.name
)
open fun mapToUpdateAccountTransactionsParameter(bank: BankAccess, account: BankAccount, preferredTanMethods: List<TanMethodType>? = null): GetAccountDataParameter {
val defaults = GetAccountDataOptions()
val accountIdentifier = BankAccountIdentifierImpl(account.identifier, account.subAccountNumber, account.iban)
val from = account.lastAccountUpdateTime?.toLocalDateTime(TimeZone.EuropeBerlin)?.date // TODO: in case lastTransactionsUpdateTime is not set, this would retrieve all transactions (and require a TAN im most cases)
val retrieveTransactions = if (from != null) RetrieveTransactions.AccordingToRetrieveFromAndTo else RetrieveTransactions.valueOf(defaults.retrieveTransactions.name)
return GetAccountDataParameter(bank.domesticBankCode, bank.loginName, bank.password!!, listOf(accountIdentifier), true,
retrieveTransactions, from,
preferredTanMethods = preferredTanMethods?.map { mapTanMethodType(it) },
preferredTanMedium = bank.selectedTanMediumIdentifier,
finTsModel = bank.clientData as? BankData,
serializedFinTsModel = bank.serializedClientData
)
}
open fun mapBankAccountIdentifier(account: BankAccountIdentifier): BankAccountIdentifierImpl =
BankAccountIdentifierImpl(account.identifier, account.subAccountNumber, account.iban)
protected open fun mapTanMethodType(type: TanMethodType): net.codinux.banking.fints.model.TanMethodType =
net.codinux.banking.fints.model.TanMethodType.valueOf(TanMethodTypesToMigrateReverse[type.name] ?: type.name)
protected open fun mapAllowedTanFormat(allowedTanFormat: AllowedTanFormat?): net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat =
allowedTanFormat?.let { net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat.valueOf(it.name) } ?: net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat.Alphanumeric
net.codinux.banking.fints.model.TanMethodType.valueOf(type.name)
open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse, bankInfo: BankInfo? = null): Response<GetAccountDataResponse> =
if (response.successful && response.customerAccount != null) {
val bank = mapBank(response.customerAccount!!, bankInfo, response)
Response.success(GetAccountDataResponse(bank), mapMessageLog(response, bank))
open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<GetAccountDataResponse> {
return if (response.successful && response.customerAccount != null) {
Response.success(GetAccountDataResponse(mapUser(response.customerAccount!!)))
} else {
mapError(response, mapMessageLog(response))
mapError(response)
}
open fun map(bank: BankAccess, responses: List<Triple<BankAccount, GetAccountDataParameter, net.dankito.banking.client.model.response.GetAccountDataResponse>>): Response<List<GetTransactionsResponse>> {
val type = if (responses.all { it.third.successful }) ResponseType.Success else ResponseType.Error
// TODO: update BankAccess and BankAccount objects according to retrieved data
val mappedResponses = responses.map { (account, param, getAccountDataResponse) ->
val fintsBank = getAccountDataResponse.customerAccount
val finTsBankAccount = fintsBank?.accounts?.firstOrNull { it.identifier == account.identifier && it.subAccountNumber == account.subAccountNumber }
val messageLog = mapMessageLog(getAccountDataResponse, bank, account)
if (getAccountDataResponse.successful && fintsBank != null && finTsBankAccount != null) {
if (finTsBankAccount.lastAccountUpdateTime != null) {
account.lastAccountUpdateTime = finTsBankAccount.lastAccountUpdateTime
}
if (account.retrievedTransactionsFrom == null || (finTsBankAccount.retrievedTransactionsFrom != null
&& finTsBankAccount.retrievedTransactionsFrom!! < account.retrievedTransactionsFrom!!)) {
account.retrievedTransactionsFrom = finTsBankAccount.retrievedTransactionsFrom
}
val balance = mapMoney(finTsBankAccount.balance)
account.balance = balance
mapCommonResponseData(bank, getAccountDataResponse)
Response.success(GetTransactionsResponse(account, balance, mapBookedTransactions(finTsBankAccount), emptyList(),
mapHoldings(finTsBankAccount.statementOfHoldings, finTsBankAccount.currency, finTsBankAccount.lastAccountUpdateTime),
finTsBankAccount.lastAccountUpdateTime ?: Clock.System.now(), param.retrieveTransactionsFrom, param.retrieveTransactionsTo),
messageLog)
} else {
mapError(getAccountDataResponse, messageLog)
}
}
val data = mappedResponses.filter { it.type == ResponseType.Success }.mapNotNull { it.data }
return (object : Response<List<GetTransactionsResponse>>(type, data, mappedResponses.firstNotNullOfOrNull { it.error }, messageLog = mappedResponses.flatMap { it.messageLog }) { })
}
open fun mapToBankViewInfo(bank: BankData): BankViewInfo = BankViewInfo(
open fun mapToUserAccountViewInfo(bank: BankData): UserAccountViewInfo = UserAccountViewInfo(
bank.bankCode, bank.customerId, bank.bankName, getBankingGroup(bank.bankName, bank.bic)
)
@ -156,35 +65,29 @@ open class FinTs4kMapper {
)
protected open fun mapBank(bank: net.dankito.banking.client.model.CustomerAccount, info: BankInfo? = null, response: FinTsClientResponse? = null) = BankAccess(
bank.bankCode, bank.loginName, bank.password,
info?.name ?: bank.bankName, bank.bic, bank.customerName, bank.userId,
bank.accounts.map { mapAccount(it) }.sortedBy { it.type }
.onEachIndexed { index, bankAccount -> bankAccount.displayIndex = index },
protected open fun mapUser(user: net.dankito.banking.client.model.CustomerAccount) = UserAccount(
user.bankCode, user.loginName, user.password,
user.bankName, user.bic, user.customerName, user.userId,
user.accounts.map { mapAccount(it) },
bank.selectedTanMethod?.securityFunction?.code, bank.tanMethods.map { mapTanMethod(it) }.toMutableList(),
bank.selectedTanMedium?.mediumName, bank.tanMedia.map { mapTanMedium(it) }.toMutableList(),
user.selectedTanMethod?.securityFunction?.code, user.tanMethods.map { mapTanMethod(it) },
user.selectedTanMedium?.mediumName, user.tanMedia.map { mapTanMedium(it) },
info?.bankingGroup ?: getBankingGroup(bank.bankName, bank.bic),
bank.finTsServerAddress,
"de"
).apply {
response?.let { mapCommonResponseData(this, it) }
}
getBankingGroup(user.bankName, user.bic)
)
protected open fun getBankingGroup(bankName: String, bic: String): BankingGroup? =
bankingGroupMapper.getBankingGroup(bankName, bic)
protected open fun mapAccount(account: net.dankito.banking.client.model.BankAccount) = BankAccount(
account.identifier, account.subAccountNumber, account.iban, account.productName, account.accountHolderName,
mapAccountType(account.type), account.currency, account.accountLimit,
account.isAccountTypeSupportedByApplication, mapFeatures(account),
mapMoney(account.balance),
account.serverTransactionsRetentionDays,
account.lastAccountUpdateTime, account.retrievedTransactionsFrom,
bookedTransactions = mapBookedTransactions(account).toMutableList(),
holdings = mapHoldings(account.statementOfHoldings, account.currency, account.lastAccountUpdateTime).toMutableList()
account.identifier, account.accountHolderName, mapAccountType(account.type), account.iban, account.subAccountNumber,
account.productName, account.currency, account.accountLimit, account.isAccountTypeSupportedByApplication,
mapFeatures(account),
mapAmount(account.balance), account.retrievedTransactionsFrom, account.retrievedTransactionsTo,
// TODO: map haveAllTransactionsBeenRetrieved
countDaysForWhichTransactionsAreKept = account.countDaysForWhichTransactionsAreKept,
bookedTransactions = account.bookedTransactions.map { mapTransaction(it) }.toMutableList()
)
protected open fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType =
@ -201,110 +104,36 @@ open class FinTs4kMapper {
add(BankAccountFeatures.TransferMoney)
}
if (account.supportsInstantPayment) {
add(BankAccountFeatures.InstantTransfer)
add(BankAccountFeatures.InstantPayment)
}
}
protected open fun mapBookedTransactions(account: net.dankito.banking.client.model.BankAccount): List<AccountTransaction> =
account.bookedTransactions.map { mapTransaction(it) }
protected open fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction(
mapMoney(transaction.amount), transaction.amount.currency.code, transaction.reference,
mapAmount(transaction.amount), transaction.amount.currency.code, transaction.unparsedReference,
transaction.bookingDate, transaction.valueDate,
transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
transaction.bookingText, null,
transaction.statementNumber, transaction.sequenceNumber,
mapNullableAmount(transaction.openingBalance), mapNullableAmount(transaction.closingBalance),
transaction.postingText,
mapNullableMoney(transaction.openingBalance), mapNullableMoney(transaction.closingBalance),
transaction.statementNumber, transaction.sheetNumber,
transaction.customerReference, transaction.bankReference,
transaction.furtherInformation,
transaction.endToEndReference, transaction.mandateReference,
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference,
transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount,
transaction.sepaReference,
transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType,
transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement,
transaction.journalNumber, transaction.textKeyAddition,
transaction.currencyType, transaction.bookingKey,
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution,
transaction.supplementaryDetails,
transaction.orderReferenceNumber, transaction.referenceNumber,
transaction.isReversal
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber
)
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 mapNullableAmount(amount: Money?) = amount?.let { mapAmount(it) }
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
)
// visible for testing
internal fun getTotalBalance(holding: Holding): Amount? {
return if (holding.totalBalance != null) {
mapNullableAmount(holding.totalBalance)
} else if (holding.quantity != null && holding.marketValue != null) {
Amount((holding.quantity!! * holding.marketValue.toString().toDouble()).toString())
} else {
null
}
}
// visible for testing
internal fun getTotalCostPrice(holding: Holding): Amount? {
return if (holding.totalCostPrice != null) {
mapNullableAmount(holding.totalCostPrice)
} else if (holding.quantity != null && holding.averageCostPrice != null) {
Amount((holding.quantity!! * holding.averageCostPrice.toString().toDouble()).toString())
} else {
null
}
}
// visible for testing
internal fun calculatePerformance(holding: Holding): Float? {
val totalBalance = getTotalBalance(holding)
val totalCostPrice = getTotalCostPrice(holding)
if (totalBalance != null && totalCostPrice != null) {
return ((totalBalance - totalCostPrice) / totalCostPrice).toFloat() * 100
}
val marketValue = mapNullableAmount(holding.marketValue)
val costPrice = mapNullableAmount(holding.averageCostPrice)
if (marketValue != null && costPrice != null) {
return ((marketValue - costPrice) / costPrice).toFloat() * 100
}
return null
}
protected open fun mapNullableMoney(amount: Money?) = amount?.let { mapMoney(it) }
protected open fun mapMoney(amount: Money) = Amount(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(amount.string.replace(',', '.'))
protected open fun mapAmount(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.'))
open fun mapTanChallenge(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallenge {
@ -312,44 +141,35 @@ open class FinTs4kMapper {
val action = mapActionRequiringTan(challenge.forAction)
val tanMethods = challenge.bank.tanMethodsAvailableForUser.map { mapTanMethod(it) }
val selectedTanMethodIdentifier = challenge.tanMethod.securityFunction.code
val selectedTanMethodId = challenge.tanMethod.securityFunction.code
val tanMedia = challenge.bank.tanMedia.map { mapTanMedium(it) }
// TanMedium has not natural id in FinTS model so we have to create our own one
val selectedTanMediumIdentifier = challenge.bank.selectedTanMedium?.let { selected -> tanMedia.firstOrNull { it == selected } }?.identifier
val selectedTanMediumName = challenge.bank.selectedTanMedium?.mediumName
val bank = mapToBankViewInfo(challenge.bank)
val user = mapToUserAccountViewInfo(challenge.bank)
val account = challenge.account?.let { mapToBankAccountViewInfo(it) }
val tanImage = if (challenge is ImageTanChallenge) mapTanImage(challenge.image) else null
val flickerCode = if (challenge is FlickerCodeTanChallenge) mapFlickerCode(challenge.flickerCode) else null
return object : TanChallenge(type, action, challenge.messageToShowToUser, selectedTanMethodIdentifier, tanMethods, selectedTanMediumIdentifier, tanMedia, tanImage, flickerCode, bank, account, challenge.tanExpirationTime, challenge.challengeCreationTimestamp) {
override fun addTanExpiredCallback(callback: () -> Unit) {
challenge.addTanExpiredCallback(callback)
}
override fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) {
challenge.addUserApprovedDecoupledTanCallback(callback)
}
}
return TanChallenge(type, action, challenge.messageToShowToUser, selectedTanMethodId, tanMethods, selectedTanMediumName, tanMedia, tanImage, flickerCode, user, account)
}
protected open fun mapTanChallengeType(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallengeType = when {
challenge is ImageTanChallenge -> TanChallengeType.Image
challenge is FlickerCodeTanChallenge -> TanChallengeType.FlickerCode
challenge is FlickerCodeTanChallenge -> TanChallengeType.Flickercode
else -> TanChallengeType.EnterTan
}
protected open fun mapActionRequiringTan(action: net.codinux.banking.fints.model.ActionRequiringTan): ActionRequiringTan =
ActionRequiringTan.valueOf(action.name)
open fun mapTanMethod(method: net.codinux.banking.fints.model.TanMethod): TanMethod = TanMethod(
protected open fun mapTanMethod(method: net.codinux.banking.fints.model.TanMethod): TanMethod = TanMethod(
method.displayName, mapTanMethodType(method.type), method.securityFunction.code, method.maxTanInputLength, mapAllowedTanFormat(method.allowedTanFormat)
)
protected open fun mapTanMethodType(type: net.codinux.banking.fints.model.TanMethodType): TanMethodType =
TanMethodType.valueOf(TanMethodTypesToMigrate[type.name] ?: type.name)
TanMethodType.valueOf(type.name)
protected open fun mapAllowedTanFormat(allowedTanFormat: net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat?): AllowedTanFormat =
allowedTanFormat?.let { AllowedTanFormat.valueOf(it.name) } ?: AllowedTanFormat.Alphanumeric
@ -358,18 +178,14 @@ open class FinTs4kMapper {
TanImage(image.mimeType, mapToBase64(image.imageBytes), mapException(image.decodingError))
@OptIn(ExperimentalEncodingApi::class)
protected open fun mapToBase64(bytes: ByteArray?): String? {
if (bytes == null || bytes.isEmpty()) {
return null
}
protected open fun mapToBase64(bytes: ByteArray): String {
return Base64.Default.encode(bytes)
}
protected open fun mapTanMedium(tanMedium: TanMedium) = net.codinux.banking.client.model.tan.TanMedium(
mapTanMediumType(tanMedium), tanMedium.mediumName, mapTanMediumStatus(tanMedium.status),
tanMedium.tanGenerator?.let { mapTanGeneratorTanMedium(it) },
tanMedium.mobilePhone?.let { mapMobilePhoneTanMedium(it) }
(tanMedium as? TanGeneratorTanMedium)?.let { mapTanGeneratorTanMedium(it) },
(tanMedium as? MobilePhoneTanMedium)?.let { mapMobilePhoneTanMedium(it) }
)
protected open fun mapTanMediumStatus(status: TanMediumStatus): net.codinux.banking.client.model.tan.TanMediumStatus = when (status) {
@ -388,9 +204,9 @@ open class FinTs4kMapper {
tanMedium.validFrom, tanMedium.validTo
)
protected open fun mapTanMediumType(tanMedium: TanMedium): TanMediumType = when (tanMedium.mediumClass) {
TanMediumKlasse.MobiltelefonMitMobileTan -> TanMediumType.MobilePhone
TanMediumKlasse.TanGenerator -> TanMediumType.TanGenerator
protected open fun mapTanMediumType(tanMedium: TanMedium): TanMediumType = when {
tanMedium is MobilePhoneTanMedium -> TanMediumType.MobilePhone
tanMedium is TanGeneratorTanMedium -> TanMediumType.TanGenerator
else -> TanMediumType.Generic
}
@ -398,81 +214,16 @@ open class FinTs4kMapper {
FlickerCode(flickerCode.challengeHHD_UC, flickerCode.parsedDataSet, mapException(flickerCode.decodingError))
/* Transfer Money */
open fun mapToTransferMoneyParameter(request: TransferMoneyRequestForUser): TransferMoneyParameter = TransferMoneyParameter(
request.bankCode, request.loginName, request.password, request.senderAccount?.let { mapBankAccountIdentifier(it) },
request.recipientName, request.recipientAccountIdentifier, request.recipientBankIdentifier,
mapToMoney(request.amount, request.currency), request.paymentReference, request.instantTransfer,
request.preferredTanMethods?.map { mapTanMethodType(it) },
request.tanMethodsNotSupportedByApplication.map { mapTanMethodType(it) },
finTsModel = request.clientData as? BankData,
serializedFinTsModel = request.serializedClientData
)
open fun mapTransferMoneyResponse(response: net.dankito.banking.client.model.response.TransferMoneyResponse, bank: BankAccess? = null, account: BankAccount? = null): Response<TransferMoneyResponse> =
if (response.successful) {
bank?.let { mapCommonResponseData(it, response) }
Response.success(TransferMoneyResponse(), mapMessageLog(response, bank, account))
} else {
mapError(response, mapMessageLog(response, bank, account))
}
open fun mapToMoney(amount: Amount, currency: String): Money = Money(amount.toString(), currency)
open fun mapMessageLog(response: FinTsClientResponse, bank: BankAccess? = null, account: BankAccount? = null) =
mapMessageLog(response.messageLog, bank, account)
open fun mapMessageLog(messageLog: List<net.codinux.banking.fints.model.MessageLogEntry>, bank: BankAccess? = null, account: BankAccount? = null) =
messageLog.map { mapMessageLogEntry(it, bank, account) }
open fun mapMessageLogEntry(messageLogEntry: net.codinux.banking.fints.model.MessageLogEntry, bank: BankAccess? = null, account: BankAccount? = null): MessageLogEntry {
// TODO: may map messageLogEntry.context.BankData to BankAccess
val context = messageLogEntry.context
val fintsAccount = context.account
val effectiveAccount = account ?: bank?.accounts?.firstOrNull { it.identifier == fintsAccount?.accountIdentifier && it.subAccountNumber == fintsAccount.subAccountAttribute }
val messageNumberString = "${context.jobNumber.toString().padStart(2, '0')}_${context.dialogNumber.toString().padStart(2, '0')}_${context.messageNumber.toString().padStart(2, '0')}"
return MessageLogEntry(
MessageLogEntryType.valueOf(messageLogEntry.type.name),
messageLogEntry.message, messageLogEntry.messageWithoutSensitiveData,
messageLogEntry.error, messageLogEntry.time,
messageNumberString,
messageNumberString.replace("_", "").toIntOrNull(),
context.jobType.toString(),
context.messageType.toString(),
bank,
effectiveAccount
)
}
protected open fun <T> mapError(response: FinTsClientResponse, messageLog: List<MessageLogEntry>): Response<T> =
if (response.error != null) {
protected open fun <T> mapError(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<T> {
return if (response.error != null) {
Response.error(ErrorType.valueOf(response.error!!.name), if (response.error == ErrorCode.BankReturnedError) null else response.errorMessage,
if (response.error == ErrorCode.BankReturnedError && response.errorMessage !== null) listOf(response.errorMessage!!) else emptyList(), messageLog)
if (response.error == ErrorCode.BankReturnedError && response.errorMessage !== null) listOf(response.errorMessage!!) else emptyList())
} else {
Response.error(ErrorType.UnknownError, response.errorMessage, messageLog = messageLog)
Response.error(ErrorType.UnknownError, response.errorMessage)
}
}
protected open fun mapException(exception: Exception?): String? =
exception?.stackTraceToString()
open fun mapCommonResponseData(bank: BankAccess, response: FinTsClientResponse) {
response.finTsModel?.let {
bank.clientData = it
}
response.serializedFinTsModel?.let {
bank.serializedClientData = it
}
}
}

View file

@ -1,33 +0,0 @@
package net.codinux.banking.client.fints4k
data class FinTsClientOptions(
/**
* If FinTS messages sent to and received from bank servers and errors should be collected.
*
* Set to false by default.
*/
val collectMessageLog: Boolean = false,
// /**
// * If set to true then [net.codinux.banking.fints.callback.FinTsClientCallback.messageLogAdded] get fired when a
// * FinTS message get sent to bank server, a FinTS message is received from bank server or an error occurred.
// *
// * Defaults to false.
// */
// val fireCallbackOnMessageLogs: Boolean = false,
/**
* If sensitive data like user name, password, login name should be removed from FinTS messages before being logged.
*
* Defaults to true.
*/
val removeSensitiveDataFromMessageLog: Boolean = true,
val appendFinTsMessagesToLog: Boolean = false,
val closeDialogs: Boolean = true,
val version: String = "1.0.0", // TODO: get version dynamically
val productName: String = "15E53C26816138699C7B6A3E8"
)

View file

@ -3,10 +3,11 @@ package net.codinux.banking.client.fints4k
import kotlinx.coroutines.test.runTest
import net.codinux.banking.client.SimpleBankingClientCallback
import net.codinux.banking.client.model.response.ResponseType
import net.codinux.banking.client.model.tan.EnterTanResult
import kotlin.test.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@Ignore
class FinTs4kBankingClientTest {
companion object {
@ -18,11 +19,8 @@ class FinTs4kBankingClientTest {
}
private val underTest = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
println("WARN: TAN is required to execute test: ${tanChallenge.messageToShowToUser}")
private val underTest = FinTs4KBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
val enteredTan: String? = null
callback(EnterTanResult(enteredTan))
})

View file

@ -1,53 +0,0 @@
package net.codinux.banking.client.fints4k
import net.codinux.banking.client.model.Amount
import kotlin.test.Test
import net.codinux.banking.fints.transactions.swift.model.Holding
import kotlin.test.assertEquals
class FinTs4kMapperTest {
private val underTest = FinTs4kMapper()
@Test
fun getTotalBalance_TotalBalanceIsNull_CalculateByQuantityAndMarketValue() {
val holding = Holding("", null, null, null, 4.0, null, null, null, fints4kAmount("13.33"))
val result = underTest.getTotalBalance(holding)
assertEquals(Amount("53.32"), result)
}
@Test
fun getTotalCostPrice_TotalCostPriceIsNull_CalculateByQuantityAndAverageCostPrice() {
val holding = Holding("", null, null, null, 47.0, fints4kAmount("16.828"), null)
val result = underTest.getTotalCostPrice(holding)
assertEquals(Amount("790.9159999999999"), result)
}
@Test
fun calculatePerformance_ByTotalBalanceAndTotalCostPrice() {
val holding = Holding("", null, null, null, null, null, fints4kAmount("20217.12"), null, null, null, fints4kAmount("19027.04"))
val result = underTest.calculatePerformance(holding)
assertEquals(6.2546773f, result!!, 0.000001f) // for JS the result has too many decimal places, so add a tolerance
}
@Test
fun calculatePerformance_ByMarketValueAndAverageCostPrice() {
val holding = Holding("", null, null, null, null, fints4kAmount("16.828"), null, null, fints4kAmount("16.75"), null, null)
val result = underTest.calculatePerformance(holding)
assertEquals(-0.4635132f, result!!, 0.0000001f) // for JS the result has too many decimal places, so add a tolerance
}
private fun fints4kAmount(amount: String) =
net.codinux.banking.fints.model.Amount(amount)
}

111
README.md
View file

@ -1,5 +1,4 @@
# Banking Client
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/net.codinux.banking.client/banking-client/badge.svg?style=plastic)](https://maven-badges.herokuapp.com/maven-central/net.codinux.banking.client/banking-client)
Library to abstract over different banking client implementations like [fints4k](https://git.dankito.net/codinux/fints4k).
@ -12,8 +11,13 @@ not each project has the implement to model again.
### Gradle:
```
plugins {
kotlin("jvm") version "2.0.10" // or kotlin("multiplatform"), depending on your requirements
}
repositories {
// other repositories like mavenCentral(), ...
mavenCentral()
maven {
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
}
@ -21,7 +25,7 @@ repositories {
dependencies {
implementation("net.codinux.banking.client:fints4k-banking-client:0.7.2")
implementation("net.codinux.banking.client:fints4k-banking-client:0.5.0")
}
```
@ -47,11 +51,11 @@ class ShowUsage {
fun getAccountData() {
val client = FinTs4kBankingClient(SimpleBankingClientCallback())
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
val response = client.getAccountData(bankCode, loginName, password) // that's all
val response = client.getAccountData()
printReceivedData(response) // now print retrieved data (save it to database, display it in UI, ...)
printReceivedData(response)
}
@ -78,10 +82,16 @@ class ShowUsage {
This fetches the booked account transactions of the last 90 days. In most cases no TAN is required for this.
In case there is, add TAN handling in Client Callback:
#### GetAccountData parameter
```kotlin
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
val tan: String? = null // if a TAN is required, add a UI or ...
callback.invoke(EnterTanResult(tan)) // ... set a break point here, get TAN e.g. from your TAN app, set tan variable in debugger view and resume debugger
})
```
You can also specify options e.g. which transactions you would like to retrieve:
You can also specify options e.g. which transactions should be retrieved:
```kotlin
val options = GetAccountDataOptions(
@ -94,91 +104,10 @@ val options = GetAccountDataOptions(
val response = client.getAccountData(options)
```
#### TAN handling
Retrieving transactions older than 90 days or sometimes even log in requires a TAN, so add TAN handling in Client Callback:
```kotlin
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
val tan: String? = null // if a TAN is required, read TAN from command line, add a UI, ...
callback.invoke(EnterTanResult(tan)) // ... set a break point here, get TAN e.g. from your TAN app, set tan variable in debugger view and resume debugger
})
```
E.g. TAN handling on the command line:
```kotlin
println("Enter password for $bankCode:")
val password = readln() // as an alternative for hard coding password; of course can also be done for bankCode and login name
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
println("A TAN is required for ${tanChallenge.forAction}. Selected TAN method is '${tanChallenge.selectedTanMethod.displayName}'. Messsage of your credit institute:")
println(tanChallenge.messageToShowToUser)
println("Get TAN from your TAN app etc., enter it and press Enter (or press Enter without an input to abort process):")
val tan: String? = readlnOrNull().takeUnless { it.isNullOrBlank() } // map empty input to null to abort process
callback.invoke(EnterTanResult(tan))
})
```
#### Error handling
Add some error handling by checking `response.error`:
```kotlin
response.error?.let{ error ->
println("Could not fetch account data: ${error.type} ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
println("Could not fetch account data: ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
}
```
### Update Account Transactions
The data model saves when it retrieved account transactions the last time (in `BankAccount.lastAccountUpdateTime`).
So you only need to call `FinTs4kBankingClient.updateAccountTransactions()` to retrieve all transactions starting from
`BankAccount.lastAccountUpdateTime`.
But as we can only specify from which day on account transactions should be retrieved, response may contain some transactions
from the day of `lastAccountUpdateTime` that we already have locally. To filter out these you can use
`BankingModelService().findNewTransactions(retrieveTransactions, existingTransactions)`:
```kotlin
fun updateAccountTransactions() {
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
// simulate account transactions we retrieved last time
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val lastCallToBankServer = client.getAccountData(GetAccountDataOptions(RetrieveTransactions.AccordingToRetrieveFromAndTo, retrieveTransactionsFrom = today.minusDays(90), retrieveTransactionsTo = today.minusDays(30)))
if (lastCallToBankServer.data != null) { // now update account transactions
val existingTransactions = lastCallToBankServer.data!!.bookedTransactions
val updateTransactionsResponse = client.updateAccountTransactions()
if (updateTransactionsResponse.data != null) {
val retrievedTransactions = updateTransactionsResponse.data!!.flatMap { it.bookedTransactions }
val newTransactions = BankingModelService().findNewTransactions(retrievedTransactions, existingTransactions)
// `retrievedTransactions` may contain transactions we already have locally, `newTransactions` only
// transactions that are not in `existingTransactions`
}
}
}
```
## Logging
BankingClient and fints4k both use [klf](https://github.com/codinux-gmbh/klf), a logging facade for Kotlin (Multiplatform)
with appenders for all supported KMP platforms.
So logging works on all platforms out of the box. On JVM, if slf4j is on the classpath, logging can be configured with
any slf4j compatible logging backend (logback, log4j, JBoss Logging, ...).
If you want to see all sent and received FinTS messages, set the log level of `net.codinux.banking.fints.log.MessageLogCollector` to `DEBUG`, either via:
- your logging framework (e.g. logback)
- klf: `net.codinux.log.LoggerFactory.getLogger("net.codinux.banking.fints.log.MessageLogCollector").level = LogLevel.Debug`
- `appendFinTsMessagesToLog` option:
```kotlin
val client = FinTs4kBankingClient(FinTsClientOptions(appendFinTsMessagesToLog = true), SimpleBankingClientCallback())
```
But be aware, in latter case if you create multiple FinTs4kBankingClient instances, the latest value of `appendFinTsMessagesToLog`
overrides the value of all previous FinTs4kBankingClient instances. As with all other options, this configures the logger's level globally,
so the latest configured log level value wins.
```

View file

@ -12,5 +12,5 @@ repositories {
dependencies {
implementation(project(":fints4k-banking-client"))
implementation("net.codinux.banking.client:fints4k-banking-client:0.5.0")
}

View file

@ -1,11 +1,7 @@
package net.codinux.banking.client.fints4k.example
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import net.codinux.banking.client.SimpleBankingClientCallback
import net.codinux.banking.client.fints4k.FinTs4kBankingClient
import net.codinux.banking.client.fints4k.FinTs4kBankingClientForUser
import net.codinux.banking.client.getAccountData
import net.codinux.banking.client.model.options.GetAccountDataOptions
@ -13,9 +9,6 @@ import net.codinux.banking.client.model.options.RetrieveTransactions
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
import net.codinux.banking.client.model.tan.EnterTanResult
import net.codinux.banking.client.service.BankingModelService
import net.codinux.banking.client.updateAccountTransactions
import net.codinux.banking.client.model.extensions.minusDays
fun main() {
val showUsage = ShowUsage()
@ -34,24 +27,17 @@ class ShowUsage {
fun getAccountDataSimpleExample() {
val client = FinTs4kBankingClient(SimpleBankingClientCallback())
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
val response = client.getAccountData(bankCode, loginName, password) // that's all
val response = client.getAccountData()
printReceivedData(response) // now print retrieved data (save it to database, display it in UI, ...)
printReceivedData(response)
}
fun getAccountDataFullExample() {
println("Enter password for $bankCode:")
val password = readln() // as an alternative for hard coding password; of course can also be done for bankCode and login name
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
println("A TAN is required for ${tanChallenge.forAction}. Selected TAN method is '${tanChallenge.selectedTanMethod.displayName}'. Messsage of your credit institute:")
println(tanChallenge.messageToShowToUser)
println("Get TAN from your TAN app etc., enter it and press Enter (or press Enter without an input to abort process):")
val tan: String? = readlnOrNull().takeUnless { it.isNullOrBlank() } // map empty input to null to abort process
callback.invoke(EnterTanResult(tan))
val tan: String? = null // if a TAN is required, add a UI or ...
callback.invoke(EnterTanResult(tan)) // ... set a break point here, get TAN e.g. from your TAN app, set tan variable in debugger view and resume debugger
})
val options = GetAccountDataOptions(
@ -64,46 +50,21 @@ class ShowUsage {
val response = client.getAccountData(options)
response.error?.let{ error ->
println("Could not fetch account data: ${error.type} ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
println("Could not fetch account data: ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
}
printReceivedData(response)
}
fun updateAccountTransactions() {
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
val tan: String? = readlnOrNull() // if a TAN is required, read TAN from command line, add a UI, ...
callback.invoke(EnterTanResult(tan)) // ... set a break point here, get TAN e.g. from your TAN app, set tan variable in debugger view and resume debugger
})
// simulate account transactions we retrieved last time
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val lastCallToBankServer = client.getAccountData(GetAccountDataOptions(RetrieveTransactions.AccordingToRetrieveFromAndTo, retrieveTransactionsFrom = today.minusDays(90), retrieveTransactionsTo = today.minusDays(30)))
if (lastCallToBankServer.data != null) { // now update account transactions
val existingTransactions = lastCallToBankServer.data!!.bookedTransactions
val updateTransactionsResponse = client.updateAccountTransactions()
if (updateTransactionsResponse.data != null) {
val retrievedTransactions = updateTransactionsResponse.data!!.flatMap { it.bookedTransactions }
val newTransactions = BankingModelService().findNewTransactions(retrievedTransactions, existingTransactions)
// `retrievedTransactions` may contain transactions we already have locally, `newTransactions` only
// transactions that are not in `existingTransactions`
}
}
}
private fun printReceivedData(response: Response<GetAccountDataResponse>) {
response.data?.let { data ->
val bank = data.bank
println("Kunde: ${bank.customerName} ${bank.accounts.size} Konten @ ${bank.bic} ${bank.bankName}")
val user = data.user
println("Kunde: ${user.customerName} ${user.accounts.size} Konten @ ${user.bic} ${user.bankName}")
println()
println("Konten:")
bank.accounts.sortedBy { it.type }.forEach { account ->
user.accounts.sortedBy { it.type }.forEach { account ->
println("${account.identifier} ${account.productName} ${account.balance} ${account.currency}")
}

View file

@ -12,7 +12,7 @@ buildscript {
allprojects {
group = "net.codinux.banking.client"
version = "0.7.2"
version = "0.5.1"
repositories {
mavenCentral()
@ -23,7 +23,7 @@ allprojects {
}
ext["sourceCodeRepositoryBaseUrl"] = "git.dankito.net/codinux/BankingClient"
ext["sourceCodeRepositoryBaseUrl"] = "github.com/codinux/BankingClient"
ext["projectDescription"] = "Model and base definitions for Banking Client, a common abstraction for different implements of banking libraries"
}

View file

@ -1,95 +0,0 @@
Für den deutschen Begriff "Bankzugang" gibt es im Englischen verschiedene mögliche Übersetzungen, abhängig vom Kontext. Hier sind einige Alternativen:
1. **Bank access** Diese Übersetzung ist direkt und wird verwendet, wenn es um den allgemeinen Zugang zu einem Bankkonto oder einer Bank geht.
2. **Online banking access** Diese Option wird oft genutzt, wenn es sich spezifisch um den Zugang zu einem Online-Banking-Konto handelt.
3. **Bank account access** Dies wird verwendet, wenn man den Zugang zu einem spezifischen Bankkonto meint.
4. **Bank login** Eine übliche Formulierung für den Zugang zu einem Online-Banking-Portal durch Einloggen mit Anmeldedaten.
5. **Bank credentials** Dies bezieht sich auf die Anmeldeinformationen (Benutzername, Passwort, etc.) für den Zugang zu einem Bankkonto, speziell im digitalen Bereich.
6. **Access to banking services** Diese Variante beschreibt eher den allgemeinen Zugang zu Bankdienstleistungen, sowohl digital als auch in Filialen.
Die beste Wahl hängt vom genauen Kontext ab, z.B. ob es sich um physischen Zugang zu einer Bank, Online-Zugang oder Zugang zu bestimmten Bankdiensten handelt.
Hier sind einige Namensvorschläge für deine Klasse mit entsprechenden Übersetzungen ins Englische:
1. **BankVerbindung**
_BankConnection_
2. **BankZugangsdaten**
_BankAccessData_
3. **OnlineBankProfil**
_OnlineBankProfile_
4. **BankKontoDetails**
_BankAccountDetails_
5. **BankNutzerProfil**
_BankUserProfile_
6. **BankNutzerDaten**
_BankUserData_
7. **BankZugangsprofil**
_BankAccessProfile_
8. **BankKontoverbindung**
_BankAccountConnection_
9. **BankServiceInfo**
_BankServiceInfo_
10. **BankNutzerVerwaltung**
_BankUserManagement_
Diese Namen umfassen sowohl den Zugang zur Bank als auch Daten über die Bank und den Nutzer.
Hier sind einige Namensvorschläge für die Klasse auf Deutsch und Englisch:
1. **BankZugangsdaten**
- **Englisch**: BankAccessData
2. **OnlineBankingZugang**
- **Englisch**: OnlineBankingAccess
3. **BankVerbindung**
- **Englisch**: BankConnection
4. **BankZugang**
- **Englisch**: BankAccess
5. **NutzerBankDetails**
- **Englisch**: UserBankDetails
6. **BankingAuthentifizierung**
- **Englisch**: BankingAuthentication
7. **BankLoginDaten**
- **Englisch**: BankLoginData
8. **BankZugangsInfo**
- **Englisch**: BankAccessInfo
9. **KontoZugangsdaten**
- **Englisch**: AccountAccessData
10. **OnlineBankingVerbindung**
- **Englisch**: OnlineBankingConnection
Diese Namen sollen die wesentlichen Informationen der Klasse widerspiegeln und die verschiedenen Attribute gut abdecken.

View file

@ -1,18 +0,0 @@
In vielen Ländern gibt es eigene Systeme zur eindeutigen Identifizierung von Banken, die ähnlich wie die **Bankleitzahl** (BLZ) in Deutschland funktionieren. Hier sind einige Beispiele:
1. **USA** **Routing Number**: In den USA wird die **Routing Transit Number (RTN)** oder **ABA-Routing-Nummer** verwendet, um Banken zu identifizieren.
2. **Großbritannien** **Sort Code**: In Großbritannien wird der **Sort Code** verwendet, um eine Bank und ihre Filiale zu identifizieren.
3. **International** **SWIFT/BIC**: Das **SWIFT/BIC**-System (Society for Worldwide Interbank Financial Telecommunication / Bank Identifier Code) wird weltweit verwendet, um Banken international eindeutig zu identifizieren.
4. **Kanada** **Institution Number**: In Kanada gibt es eine **Institution Number** (in Kombination mit einer Transitnummer).
5. **Australien** **BSB Number**: In Australien ist es die **BSB Number** (Bank-State-Branch Number).
6. **Indien** **IFSC Code**: In Indien wird der **Indian Financial System Code (IFSC)** verwendet.
7. **Schweiz** **SIC Number**: In der Schweiz gibt es das **SIC (Swiss Interbank Clearing)**-System, um Banken zu identifizieren.
### Guter englischer Name für eine landesweite eindeutige Bankidentifikation
Ein passender englischer Begriff für einen solchen eindeutigen Identifikationswert wäre:
- **National Bank Identifier (NBI)**: Dies beschreibt eine eindeutige Kennung, die eine Bank auf nationaler Ebene identifiziert.
- **Domestic Bank Code (DBC)**: Dies weist ebenfalls auf die Verwendung im nationalen Kontext hin.
- **Bank Identifier Code (BIC)**: Obwohl dieser Begriff oft international verwendet wird, kann er auch auf eine landesweite eindeutige Identifikation hinweisen, wenn er im nationalen Kontext benutzt wird.
Ein allgemeiner Begriff wie **"National Bank Code"** wäre ebenfalls leicht verständlich und eindeutig in seiner Funktion.

View file

@ -1,14 +1,8 @@
kotlin.code.style=official
#org.gradle.parallel=true
kotlinVersion=1.9.25
kotlinVersion=2.0.10
kotlinxDateTimeVersion=0.5.0
jsJodaTimeZoneVersion=2.3.0
# Coroutines 1.9 (currently RC) requires Kotlin 2.0
coroutinesVersion=1.8.1
# 0.3.10 uses Kotlin 2.0.0
ionspinBigNumVersion=0.3.9

@ -1 +1 @@
Subproject commit 88f1b01167e6a34b5b91f8797845bca0b7e4d3ab
Subproject commit bdf8b14738c06016a48e1fc9781ad4d999e1219f

File diff suppressed because it is too large Load diff