Compare commits

..

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

79 changed files with 725 additions and 2796 deletions

View file

@ -53,7 +53,6 @@ kotlin {
linuxX64()
mingwX64()
iosX64()
iosArm64()
iosSimulatorArm64()
macosX64()

View file

@ -1,57 +1,14 @@
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 {
/**
* Retrieves account data like customer name, her bank accounts, their balance and account transactions.
*
* By default the account transactions of the last 90 days are retrieved (which in most cases doesn't require a
* TAN according to PSD2).
*
* If you like to retrieve the transactions of a different period, use the method overload that takes a
* [GetAccountDataRequest] parameter and set [RetrieveTransactions] and may to and from date in its options object.
*/
suspend fun getAccountDataAsync(bankCode: String, loginName: String, password: String) =
getAccountDataAsync(GetAccountDataRequest(bankCode, loginName, password))
/**
* Retrieves account data like customer name, her bank accounts, their balance and account transactions.
*
* By default the account transactions of the last 90 days are retrieved (which in most cases doesn't require a
* TAN according to PSD2).
*
* If you like to retrieve the transactions of a different period, set [RetrieveTransactions] and may to and from
* date in [GetAccountDataRequest.options].
*/
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,6 +1,5 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.MessageLogEntry
import net.codinux.banking.client.model.tan.EnterTanResult
import net.codinux.banking.client.model.tan.TanChallenge
@ -8,11 +7,4 @@ interface BankingClientCallback {
fun enterTan(tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit)
/**
* An optional method, for sure not available for all client implementations (currently only for FinTs4kBankingClient).
*
* Gets fired when a FinTS message get sent to bank server, a FinTS message is received from bank server or an error occurred.
*/
fun messageLogAdded(messageLogEntry: MessageLogEntry)
}

View file

@ -0,0 +1,14 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
interface BankingClientForCustomer {
// for languages not supporting default parameters (Java, Swift, JS, ...)
suspend fun getAccountDataAsync() = getAccountDataAsync(GetAccountDataOptions())
suspend fun getAccountDataAsync(options: GetAccountDataOptions): Response<GetAccountDataResponse>
}

View file

@ -0,0 +1,15 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
abstract class BankingClientForCustomerBase(
protected val credentials: AccountCredentials,
protected val client: BankingClient
) : BankingClientForCustomer {
override suspend fun getAccountDataAsync(options: GetAccountDataOptions) =
client.getAccountDataAsync(GetAccountDataRequest(credentials, options))
}

View file

@ -1,54 +0,0 @@
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 {
/**
* Retrieves account data like customer name, her bank accounts, their balance and account transactions.
*
* By default the account transactions of the last 90 days are retrieved (which in most cases doesn't require a
* TAN according to PSD2).
*
* If you like to retrieve the transactions of a different period, use the method overload that takes a
* [GetAccountDataOptions] parameter and set [RetrieveTransactions] and may to and from date.
*/
// for languages not supporting default parameters (Java, Swift, JS, ...)
suspend fun getAccountDataAsync() = getAccountDataAsync(GetAccountDataOptions())
/**
* Retrieves account data like customer name, her bank accounts, their balance and account transactions.
*
* By default the account transactions of the last 90 days are retrieved (which in most cases doesn't require a
* TAN according to PSD2).
*
* If you like to retrieve the transactions of a different period, set [GetAccountDataOptions.retrieveTransactions]
* and may [GetAccountDataOptions.retrieveTransactionsFrom] and [GetAccountDataOptions.retrieveTransactionsTo].
*/
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 +0,0 @@
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))
}

View file

@ -4,7 +4,7 @@ import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
interface BlockingBankingClientForUser {
interface BlockingBankingClientForCustomer {
// for languages not supporting default parameters (Java, Swift, JS, ...)
fun getAccountData() = getAccountData(GetAccountDataOptions())

View file

@ -4,10 +4,10 @@ import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
abstract class BlockingBankingClientForUserBase(
abstract class BlockingBankingClientForCustomerBase(
protected val credentials: AccountCredentials,
protected val client: BlockingBankingClient
) : BlockingBankingClientForUser {
) : BlockingBankingClientForCustomer {
override fun getAccountData(options: GetAccountDataOptions) =
client.getAccountData(GetAccountDataRequest(credentials, options))

View file

@ -1,11 +1,9 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.MessageLogEntry
import net.codinux.banking.client.model.tan.EnterTanResult
import net.codinux.banking.client.model.tan.TanChallenge
open class SimpleBankingClientCallback(
protected val messageLogAdded: ((MessageLogEntry) -> Unit)? = null,
protected val enterTan: ((tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) -> Unit)? = null
) : BankingClientCallback {
@ -17,8 +15,4 @@ open class SimpleBankingClientCallback(
}
}
override fun messageLogAdded(messageLogEntry: MessageLogEntry) {
messageLogAdded?.invoke(messageLogEntry)
}
}

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 BankingClientForCustomer.getAccountData() = runBlocking {
this@getAccountData.getAccountDataAsync()
}
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()
}
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)
fun BankingClientForCustomer.getAccountData(options: GetAccountDataOptions) = runBlocking {
this@getAccountData.getAccountDataAsync(options)
}

View file

@ -12,10 +12,10 @@ fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
this@getAccountData.getAccountDataAsync(request)
}
fun BankingClientForUser.getAccountData() = runBlocking {
fun BankingClientForCustomer.getAccountData() = runBlocking {
this@getAccountData.getAccountDataAsync()
}
fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlocking {
fun BankingClientForCustomer.getAccountData(options: GetAccountDataOptions) = runBlocking {
this@getAccountData.getAccountDataAsync(options)
}

View file

@ -63,7 +63,6 @@ kotlin {
linuxX64()
mingwX64()
iosX64()
iosArm64()
iosSimulatorArm64()
macosX64()
@ -79,8 +78,6 @@ kotlin {
val kotlinxDateTimeVersion: String by project
val jsJodaTimeZoneVersion: String by project
val ionspinBigNumVersion: String by project
sourceSets {
commonMain {
@ -104,26 +101,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,44 @@ 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 category: String? = null,
var userSetDisplayName: 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,
val bookedTransactions: MutableList<AccountTransaction> = mutableListOf(),
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

@ -1,14 +0,0 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class BankAccountIdentifier(
val identifier: String,
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

@ -0,0 +1,34 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class CustomerAccount(
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,
val accounts: List<BankAccount> = emptyList(),
var bankingGroup: BankingGroup? = null,
var iconUrl: String? = null,
) {
var wrongCredentialsEntered: Boolean = false
var userSetDisplayName: String? = null
var displayIndex: Int = 0
override fun toString() = "$bankName $loginName, ${accounts.size} accounts"
}

View file

@ -0,0 +1,16 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
/**
* Contains only the basic info of a [CustomerAccount], just enough that a client application can display it to the user
* and the user knows exactly which [CustomerAccount] is meant / referred.
*/
@NoArgConstructor
open class CustomerAccountViewInfo(
val bankCode: String,
var loginName: String,
val bankName: String
) {
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

@ -1,23 +0,0 @@
package net.codinux.banking.client.model
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
open class MessageLogEntry(
open val type: MessageLogEntryType,
open val message: String,
open val messageWithoutSensitiveData: 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"
}

View file

@ -1,9 +0,0 @@
package net.codinux.banking.client.model
enum class MessageLogEntryType {
Sent,
Received,
Error
}

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

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

@ -1,47 +1,15 @@
package net.codinux.banking.client.model.options
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.BankAccountIdentifier
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.tan.TanMethodType
@NoArgConstructor
open class GetAccountDataOptions(
val retrieveBalance: Boolean = true,
val retrieveTransactions: RetrieveTransactions = RetrieveTransactions.OfLast90Days,
val retrieveTransactionsFrom: LocalDate? = null,
val retrieveTransactionsTo: LocalDate? = null,
val retrieveBalance: Boolean = true,
/**
* Account(s) may should be excluded from data retrieval, so this option enabled to set for which accounts data
* should be retrieved.
*/
val accounts: List<BankAccountIdentifier> = emptyList(),
/**
* 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 abortIfTanIsRequired: Boolean = false,
// there's also the option preferredTanMedium, but can hardly find a use case for it as we
// cannot know the TanMedium name upfront. In most cases there's only one TanMedium (per TanMethod) anyway.
val abortIfTanIsRequired: Boolean = false
) {
override fun toString(): String {
return "retrieveBalance=$retrieveBalance, retrieveTransactions=$retrieveTransactions, abortIfTanIsRequired=$abortIfTanIsRequired"

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.CustomerAccount
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 customer: CustomerAccount
) {
@get:JsonIgnore
val bookedTransactions: List<AccountTransaction>
get() = bank.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
get() = customer.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
override fun toString() = bank.toString()
override fun toString() = customer.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,12 +1,10 @@
package net.codinux.banking.client.model.tan
import net.codinux.banking.client.model.BankAccountIdentifier
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class MobilePhoneTanMedium(
val phoneNumber: String?,
val concealedPhoneNumber: String? = null
val phoneNumber: String?
) {
override fun toString() = phoneNumber ?: "No phone number"
}

View file

@ -1,94 +1,27 @@
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.config.JsonIgnore
import net.codinux.banking.client.model.CustomerAccountViewInfo
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,
/**
* Identifier of selected TanMethod.
*
* 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,
/**
* When adding an account, frontend has no UserAccount object in BankingClientCallback to know which TanMethods are
* 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.
*
* 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>,
/**
* Identifier of selected TanMedium.
*
* 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(),
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 type: TanChallengeType,
val forAction: ActionRequiringTan,
val messageToShowToUser: String,
val tanMethod: TanMethod,
val tanImage: TanImage? = null,
val flickerCode: FlickerCode? = null,
val customer: CustomerAccountViewInfo,
val account: BankAccountViewInfo? = null
// TODO: add availableTanMethods, selectedTanMedium, availableTanMedia
) {
@get:JsonIgnore
open val selectedTanMethod: TanMethod
get() = availableTanMethods.first { it.identifier == selectedTanMethodIdentifier }
@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) {
}
override fun toString(): String {
return "$selectedTanMethod $forAction: $messageToShowToUser" + when (type) {
return "$tanMethod $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

@ -1,19 +1,10 @@
package net.codinux.banking.client.model.tan
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,
val cardType: Int? = null,
val validFrom: LocalDate? = null,
val validTo: LocalDate? = null
val cardNumber: String
) {
override fun toString() = cardNumber
}

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 mimeType
}
}

View file

@ -5,7 +5,7 @@ import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class TanMedium(
val type: TanMediumType,
val mediumName: String?,
val displayName: String,
val status: TanMediumStatus,
/**
* Only set if [type] is [TanMediumType.TanGenerator].
@ -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"
override fun toString() = "$displayName $status"
}

View file

@ -1,25 +1,7 @@
package net.codinux.banking.client.model.tan
enum class TanMediumStatus {
/**
* Die Bank zeigt an, dass es eine TAN-Verifikation gegen dieses Medium vornimmt.
*/
Used,
/**
* Das Medium kann genutzt werden, muss aber zuvor mit TAN-Generator an- bzw. ummelden (HKTAU) aktiv gemeldet werden.
*/
Available,
/**
* Mit der ersten Nutzung der Folgekarte wird die zur Zeit aktive Karte gesperrt.
*/
ActiveFollowUpCard,
/**
* Das Medium kann mit dem Geschäftsvorfall TAN-Medium an- bzw. ummelden (HKTAU) aktiv gemeldet werden.
* Die aktuelle Karte kann dann nicht mehr genutzt werden.
*/
AvailableFollowUpCard
Available
}

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,7 @@ 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 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 NonVisualOrImageBased = buildList {
addAll(NonVisualWithoutChipTanManual)
addAll(ImageBased)
addAll(listOf(TanMethodType.ChipTanManual)) // this is quite inconvenient for user, so i added it as 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,5 +1,3 @@
package net.codinux.banking.client.model.config
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
actual annotation class JsonIgnore

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,5 +1,3 @@
package net.codinux.banking.client.model.config
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
actual annotation class JsonIgnore

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

@ -1,5 +1,3 @@
package net.codinux.banking.client.model.config
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
actual annotation class JsonIgnore

View file

@ -4,11 +4,18 @@ import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
plugins {
kotlin("multiplatform")
id("maven-publish")
}
repositories {
mavenLocal()
}
kotlin {
jvmToolchain(11)
jvmToolchain(8)
jvm {
withJava()
@ -32,7 +39,7 @@ kotlin {
browser {
testTask {
useKarma {
// useChromeHeadless()
useChromeHeadless()
useFirefoxHeadless()
}
}
@ -47,13 +54,12 @@ kotlin {
}
}
// wasmJs() // ktor is not available for wasmJs yet
// wasmJs()
linuxX64()
mingwX64()
iosX64()
iosArm64()
iosSimulatorArm64()
macosX64()
@ -75,7 +81,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-11")
api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
}
@ -113,6 +119,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

@ -1,13 +1,11 @@
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.model.BankData
import net.codinux.banking.fints.model.EnterTanGeneratorAtcResult
import net.codinux.banking.fints.model.MessageLogEntry
import net.codinux.banking.fints.model.TanMethod
import net.dankito.banking.fints.callback.FinTsClientCallback
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
import net.dankito.banking.fints.model.BankData
import net.dankito.banking.fints.model.EnterTanGeneratorAtcResult
import net.dankito.banking.fints.model.TanMethod
open class BridgeFintTsToBankingClientCallback(
protected val bankingClientCallback: BankingClientCallback,
@ -18,26 +16,17 @@ open class BridgeFintTsToBankingClientCallback(
return suggestedTanMethod
}
override suspend fun enterTan(tanChallenge: net.codinux.banking.fints.model.TanChallenge) {
override suspend fun enterTan(tanChallenge: net.dankito.banking.fints.model.TanChallenge) {
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)
bankingClientCallback.messageLogAdded(mapped)
}
}

View file

@ -2,66 +2,25 @@ 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.fints.FinTsClient
import net.codinux.banking.fints.config.FinTsClientConfiguration
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
import net.dankito.banking.fints.FinTsClient
open class FinTs4kBankingClient(
config: FinTsClientConfiguration = FinTsClientConfiguration(),
callback: BankingClientCallback
) : BankingClient {
constructor(callback: BankingClientCallback) : this(FinTsClientConfiguration(), callback)
private val mapper = FinTs4kMapper()
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 open val mapper = FinTs4kMapper()
protected open val client = FinTsClient(config, BridgeFintTsToBankingClientCallback(callback, mapper))
private val client = FinTsClient(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

@ -0,0 +1,13 @@
package net.codinux.banking.client.fints4k
import net.codinux.banking.client.BankingClientCallback
import net.codinux.banking.client.BankingClientForCustomerBase
import net.codinux.banking.client.model.AccountCredentials
open class FinTs4kBankingClientForCustomer(credentials: AccountCredentials, callback: BankingClientCallback)
: BankingClientForCustomerBase(credentials, FinTs4kBankingClient(callback)) {
constructor(bankCode: String, loginName: String, password: String, callback: BankingClientCallback)
: this(AccountCredentials(bankCode, loginName, password), callback)
}

View file

@ -1,17 +0,0 @@
package net.codinux.banking.client.fints4k
import net.codinux.banking.client.BankingClientCallback
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)
: BankingClientForUserBase(credentials, FinTs4kBankingClient(config, callback)) {
constructor(bankCode: String, loginName: String, password: String, callback: BankingClientCallback)
: this(bankCode, loginName, password, FinTsClientConfiguration(), callback)
constructor(bankCode: String, loginName: String, password: String, config: FinTsClientConfiguration = FinTsClientConfiguration(), callback: BankingClientCallback)
: this(AccountCredentials(bankCode, loginName, password), config, callback)
}

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,172 +15,66 @@ 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.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 net.dankito.banking.fints.mapper.FinTsModelMapper
import net.dankito.banking.fints.model.*
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()
}
private val fintsModelMapper = FinTsModelMapper()
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) },
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
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))
fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<GetAccountDataResponse> {
return if (response.successful && response.customerAccount != null) {
Response.success(GetAccountDataResponse(mapCustomer(response.customerAccount!!)))
} else {
mapError(response, mapMessageLog(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)
mapError(response)
}
}
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(
bank.bankCode, bank.customerId, bank.bankName, getBankingGroup(bank.bankName, bank.bic)
fun mapToCustomerAccountViewInfo(bank: BankData): CustomerAccountViewInfo = CustomerAccountViewInfo(
bank.bankCode, bank.customerId, bank.bankName
)
open fun mapToBankAccountViewInfo(account: AccountData): BankAccountViewInfo = BankAccountViewInfo(
fun mapToBankAccountViewInfo(account: AccountData): BankAccountViewInfo = BankAccountViewInfo(
account.accountIdentifier, account.subAccountAttribute,
mapAccountType(fintsModelMapper.map(account.accountType)),
account.iban, account.productName
)
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 },
bank.selectedTanMethod?.securityFunction?.code, bank.tanMethods.map { mapTanMethod(it) }.toMutableList(),
bank.selectedTanMedium?.mediumName, bank.tanMedia.map { mapTanMedium(it) }.toMutableList(),
info?.bankingGroup ?: getBankingGroup(bank.bankName, bank.bic),
bank.finTsServerAddress,
"de"
).apply {
response?.let { mapCommonResponseData(this, it) }
}
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()
private fun mapCustomer(customer: net.dankito.banking.client.model.CustomerAccount): CustomerAccount = CustomerAccount(
customer.bankCode, customer.loginName, customer.password,
customer.bankName, customer.bic, customer.customerName, customer.userId,
customer.accounts.map { mapAccount(it) }
)
protected open fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType =
private fun mapAccount(account: net.dankito.banking.client.model.BankAccount): BankAccount = BankAccount(
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()
)
private fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType =
BankAccountType.valueOf(type.name)
protected open fun mapFeatures(account: net.dankito.banking.client.model.BankAccount): Set<BankAccountFeatures> = buildSet {
private fun mapFeatures(account: net.dankito.banking.client.model.BankAccount): Set<BankAccountFeatures> = buildSet {
if (account.supportsRetrievingBalance) {
add(BankAccountFeatures.RetrieveBalance)
}
@ -201,278 +85,80 @@ 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,
private fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction(
mapAmount(transaction.amount), transaction.amount.currency.code, transaction.unparsedReference,
transaction.bookingDate, transaction.valueDate,
transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId,
transaction.postingText,
mapNullableMoney(transaction.openingBalance), mapNullableMoney(transaction.closingBalance),
transaction.statementNumber, transaction.sheetNumber,
transaction.customerReference, transaction.bankReference,
transaction.furtherInformation,
transaction.endToEndReference, transaction.mandateReference,
transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount,
transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType,
transaction.journalNumber, transaction.textKeyAddition,
transaction.orderReferenceNumber, transaction.referenceNumber,
transaction.isReversal
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
transaction.bookingText, null,
transaction.statementNumber, transaction.sequenceNumber,
mapNullableAmount(transaction.openingBalance), mapNullableAmount(transaction.closingBalance),
// TODO: map other properties
)
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) }
private 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(',', '.'))
private fun mapAmount(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.'))
open fun mapTanChallenge(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallenge {
fun mapTanChallenge(challenge: net.dankito.banking.fints.model.TanChallenge): TanChallenge {
val type = mapTanChallengeType(challenge)
val action = mapActionRequiringTan(challenge.forAction)
val tanMethods = challenge.bank.tanMethodsAvailableForUser.map { mapTanMethod(it) }
val selectedTanMethodIdentifier = 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 bank = mapToBankViewInfo(challenge.bank)
val tanMethod = mapTanMethod(challenge.tanMethod)
val customer = mapToCustomerAccountViewInfo(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)
return TanChallenge(type, action, challenge.messageToShowToUser, tanMethod, tanImage, flickerCode, customer, account)
}
override fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) {
challenge.addUserApprovedDecoupledTanCallback(callback)
}
}
}
protected open fun mapTanChallengeType(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallengeType = when {
private fun mapTanChallengeType(challenge: net.dankito.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 =
private fun mapActionRequiringTan(action: net.dankito.banking.fints.model.ActionRequiringTan): ActionRequiringTan =
ActionRequiringTan.valueOf(action.name)
open fun mapTanMethod(method: net.codinux.banking.fints.model.TanMethod): TanMethod = TanMethod(
private fun mapTanMethod(method: net.dankito.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)
private fun mapTanMethodType(type: net.dankito.banking.fints.model.TanMethodType): TanMethodType =
TanMethodType.valueOf(type.name)
protected open fun mapAllowedTanFormat(allowedTanFormat: net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat?): AllowedTanFormat =
private fun mapAllowedTanFormat(allowedTanFormat: net.dankito.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat?): AllowedTanFormat =
allowedTanFormat?.let { AllowedTanFormat.valueOf(it.name) } ?: AllowedTanFormat.Alphanumeric
protected open fun mapTanImage(image: net.codinux.banking.fints.tan.TanImage): TanImage =
private fun mapTanImage(image: net.dankito.banking.fints.tan.TanImage): TanImage =
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
}
private 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) }
)
protected open fun mapTanMediumStatus(status: TanMediumStatus): net.codinux.banking.client.model.tan.TanMediumStatus = when (status) {
TanMediumStatus.Aktiv -> net.codinux.banking.client.model.tan.TanMediumStatus.Used
TanMediumStatus.Verfuegbar -> net.codinux.banking.client.model.tan.TanMediumStatus.Available
TanMediumStatus.AktivFolgekarte -> net.codinux.banking.client.model.tan.TanMediumStatus.ActiveFollowUpCard
TanMediumStatus.VerfuegbarFolgekarte -> net.codinux.banking.client.model.tan.TanMediumStatus.AvailableFollowUpCard
}
protected open fun mapMobilePhoneTanMedium(tanMedium: MobilePhoneTanMedium) = net.codinux.banking.client.model.tan.MobilePhoneTanMedium(
tanMedium.phoneNumber, tanMedium.concealedPhoneNumber
)
protected open fun mapTanGeneratorTanMedium(tanMedium: TanGeneratorTanMedium) = net.codinux.banking.client.model.tan.TanGeneratorTanMedium(
tanMedium.cardNumber, tanMedium.cardSequenceNumber, tanMedium.cardType,
tanMedium.validFrom, tanMedium.validTo
)
protected open fun mapTanMediumType(tanMedium: TanMedium): TanMediumType = when (tanMedium.mediumClass) {
TanMediumKlasse.MobiltelefonMitMobileTan -> TanMediumType.MobilePhone
TanMediumKlasse.TanGenerator -> TanMediumType.TanGenerator
else -> TanMediumType.Generic
}
protected open fun mapFlickerCode(flickerCode: net.codinux.banking.fints.tan.FlickerCode): FlickerCode =
private fun mapFlickerCode(flickerCode: net.dankito.banking.fints.tan.FlickerCode): FlickerCode =
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) {
private 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? =
private 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

@ -1,72 +0,0 @@
package net.dankito.banking.banklistcreator.prettifier
import net.codinux.banking.client.model.BankingGroup
// TODO: class has been duplicated from BankListCreator, find a better place for it
class BankingGroupMapper {
fun getBankingGroup(bankName: String, bic: String): BankingGroup? {
val lowercase = bankName.lowercase()
return when {
bankName.contains("Sparda") -> BankingGroup.Sparda
bankName.contains("PSD") -> BankingGroup.PSD
bankName.contains("GLS") -> BankingGroup.GLS
// see https://de.wikipedia.org/wiki/Liste_der_Genossenschaftsbanken_in_Deutschland
bankName.contains("BBBank") || bankName.contains("Evangelische Bank") || bankName.contains("LIGA Bank")
|| bankName.contains("Pax") || bankName.contains("Bank für Kirche und Diakonie") || bankName.contains("Bank im Bistum Essen")
|| bankName.contains("Bank für Schiffahrt") || bankName.contains("Bank für Kirche")
-> BankingGroup.SonstigeGenossenschaftsbank
lowercase.contains("deutsche kreditbank") -> BankingGroup.DKB
// may check against https://de.wikipedia.org/wiki/Liste_der_Sparkassen_in_Deutschland
lowercase.contains("sparkasse") -> BankingGroup.Sparkasse
lowercase.contains("comdirect") -> BankingGroup.Comdirect
lowercase.contains("commerzbank") -> BankingGroup.Commerzbank
lowercase.contains("targo") -> BankingGroup.Targobank
lowercase.contains("santander") -> BankingGroup.Santander
bankName.contains("KfW") -> BankingGroup.KfW
bankName.contains("N26") -> BankingGroup.N26
else -> getBankingGroupByBic(bic)
}
}
private fun getBankingGroupByBic(bic: String): BankingGroup? {
if (bic.length < 4) {
return null
}
if (bic.startsWith("CMCIDEDD")) {
return BankingGroup.Targobank
}
val bankCodeOfBic = bic.substring(0, 4)
return when (bankCodeOfBic) {
"GENO", "VBMH", "VOHA", "VBRS", "DBPB", "VBGT", "FFVB", "WIBA", "VRBU", "MVBM", "VOBA", "ULMV", "VBRT", "VBRA", "VBPF", "VOLO" -> BankingGroup.VolksUndRaiffeisenbanken
"BFSW", // Bank fuer Sozialwirtschaft
"BEVO", // Berliner Volksbank
"DAAE", // apoBank
"MHYP", // Münchener Hypothekenbank
"DZBM", // DZB Bank
"EDEK" // Edekabank
-> BankingGroup.SonstigeGenossenschaftsbank
"BYLA", "SOLA", "NOLA", "WELA", "HELA", "MALA", "BRLA", "NASS", "TRIS", "OSDD", "ESSL", "GOPS", "SBCR", "BRUS" -> BankingGroup.Sparkasse // filter out DBK, (Bayr.) Landesbank, ...
"OLBO" -> BankingGroup.OldenburgischeLandesbank
"DEUT" -> BankingGroup.DeutscheBank
"PBNK" -> BankingGroup.Postbank
"COBA", "DRES" -> BankingGroup.Commerzbank // COBA could also be comdirect, but we cannot differentiate this at this level, this has to do getBankingGroup()
"HYVE" -> BankingGroup.Unicredit
"INGB" -> BankingGroup.ING
"SCFB" -> BankingGroup.Santander
"NORS" -> BankingGroup.Norisbank
"DEGU" -> BankingGroup.Degussa
"OBKL" -> BankingGroup.Oberbank
"MARK" -> BankingGroup.Bundesbank
"KFWI", "DTAB" -> BankingGroup.KfW
"NTSB" -> BankingGroup.N26
"CSDB" -> BankingGroup.Consors
else -> null
}
}
}

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 = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback { customer, tanChallenge ->
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)
}

184
README.md
View file

@ -1,184 +0,0 @@
# 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).
It's primary purpose is to abstract away the different implementation details and to create a common model that can be
used in all projects directly or indirectly referencing it - Web Service, Middleware, Native Apps, HTML Apps - so that
not each project has the implement to model again.
## Setup
### Gradle:
```
repositories {
// other repositories like mavenCentral(), ...
maven {
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
}
}
dependencies {
implementation("net.codinux.banking.client:fints4k-banking-client:0.7.2")
}
```
## Usage
For the full example source code see [ShowUsage](SampleApp/src/main/kotlin/net/codinux/banking/client/fints4k/example/ShowUsage.kt).
### Get AccountData
Retrieves data like accounts, balance and booked transactions (Konten, Saldo und Kontoumsätze).
Simple example:
```kotlin
class ShowUsage {
private val bankCode = "" // Bankleitzahl deiner Bank
private val loginName = "" // Online-Banking Login Name mit dem du dich beim Online-Banking deiner Bank anmeldest
private val password = "" // Online-Banking Password mit dem du dich beim Online-Banking deiner Bank anmeldest
fun getAccountData() {
val client = FinTs4kBankingClient(SimpleBankingClientCallback())
val response = client.getAccountData(bankCode, loginName, password) // that's all
printReceivedData(response) // now print retrieved data (save it to database, display it in UI, ...)
}
private fun printReceivedData(response: Response<GetAccountDataResponse>) {
response.data?.let { data ->
val user = data.user
println("Kunde: ${user.customerName} ${user.accounts.size} Konten @ ${user.bic} ${user.bankName}")
println()
println("Konten:")
user.accounts.sortedBy { it.type }.forEach { account ->
println("${account.identifier} ${account.productName} ${account.balance} ${account.currency}")
}
println()
println("Umsätze:")
data.bookedTransactions.forEach { transaction ->
println("${transaction.valueDate} ${transaction.amount} ${transaction.currency} ${transaction.otherPartyName ?: ""} - ${transaction.reference}")
}
}
}
}
```
This fetches the booked account transactions of the last 90 days. In most cases no TAN is required for this.
#### GetAccountData parameter
You can also specify options e.g. which transactions you would like to retrieve:
```kotlin
val options = GetAccountDataOptions(
retrieveBalance = true, // retrieve balance (Saldo / Kontostand) yes or no
retrieveTransactions = RetrieveTransactions.AccordingToRetrieveFromAndTo, // so that fromDate and toDate below determine of which time period transactions (Umsätze) should be retrieved; defaults to OfLast90Days which in most cases doesn't require a TAN
retrieveTransactionsFrom = LocalDate(2023, 1, 1),
retrieveTransactionsTo = LocalDate(2023, 12, 31),
abortIfTanIsRequired = false // e.g. for command line application when entering TAN is either not possible or a TAN procedure is used that cannot be handled via a break point (e.g. showing a TAN image or flicker code)
)
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()}")
}
```
### 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

@ -1,16 +0,0 @@
plugins {
kotlin("jvm") // or kotlin("multiplatform"), depending on your requirements
}
repositories {
mavenCentral()
maven {
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
}
}
dependencies {
implementation(project(":fints4k-banking-client"))
}

View file

@ -1,117 +0,0 @@
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
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()
showUsage.getAccountDataSimpleExample()
// showUsage.getAccountDataFullExample()
}
class ShowUsage {
private val bankCode = "" // Bankleitzahl deiner Bank
private val loginName = "" // Online-Banking Login Name mit dem du dich beim Online-Banking deiner Bank anmeldest
private val password = "" // Online-Banking Password mit dem du dich beim Online-Banking deiner Bank anmeldest
fun getAccountDataSimpleExample() {
val client = FinTs4kBankingClient(SimpleBankingClientCallback())
val response = client.getAccountData(bankCode, loginName, password) // that's all
printReceivedData(response) // now print retrieved data (save it to database, display it in UI, ...)
}
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 options = GetAccountDataOptions(
retrieveBalance = true, // retrieve balance (Saldo / Kontostand) yes or no
retrieveTransactions = RetrieveTransactions.AccordingToRetrieveFromAndTo, // so that fromDate and toDate below determine of which time period transactions (Umsätze) should be retrieved; defaults to OfLast90Days which in most cases doesn't require a TAN
retrieveTransactionsFrom = LocalDate(2023, 1, 1),
retrieveTransactionsTo = LocalDate(2023, 12, 31),
abortIfTanIsRequired = false // e.g. for command line application when entering TAN is either not possible or a TAN procedure is used that cannot be handled via a break point (e.g. showing a TAN image or flicker code)
)
val response = client.getAccountData(options)
response.error?.let{ error ->
println("Could not fetch account data: ${error.type} ${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}")
println()
println("Konten:")
bank.accounts.sortedBy { it.type }.forEach { account ->
println("${account.identifier} ${account.productName} ${account.balance} ${account.currency}")
}
println()
println("Umsätze:")
data.bookedTransactions.forEach { transaction ->
println("${transaction.valueDate} ${transaction.amount} ${transaction.currency} ${transaction.otherPartyName ?: ""} - ${transaction.reference}")
}
}
}
}

View file

@ -12,18 +12,17 @@ buildscript {
allprojects {
group = "net.codinux.banking.client"
version = "0.7.2"
version = "0.5.0"
repositories {
mavenCentral()
mavenLocal()
maven {
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
}
}
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"
}
@ -34,7 +33,7 @@ tasks.register("publishAllToMavenLocal") {
":BankingClientModel:publishToMavenLocal",
":BankingClient:publishToMavenLocal",
":fints4k-banking-client:publishToMavenLocal"
":FinTs4jBankingClient:publishToMavenLocal"
)
}
@ -43,6 +42,6 @@ tasks.register("publishAll") {
":BankingClientModel:publish",
":BankingClient:publish",
":fints4k-banking-client:publish"
":FinTs4jBankingClient:publish"
)
}

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

View file

@ -31,5 +31,3 @@ include("FinTs4jBankingClient")
project(":FinTs4jBankingClient").apply {
name = "fints4k-banking-client"
}
include("SampleApp")