Compare commits
125 commits
Author | SHA1 | Date | |
---|---|---|---|
736ed22a51 | |||
fbd6f8a6e5 | |||
d00f71bedd | |||
68e1408b81 | |||
98260fbfbf | |||
52db7be2f6 | |||
2b0f9fcc71 | |||
5d8079cfd4 | |||
f8dd296b47 | |||
d1cc7b0eb0 | |||
5d127828cb | |||
803e44118c | |||
12304684fc | |||
7448b7e94c | |||
0e26a19d13 | |||
647f848d5f | |||
ce39c1cf7d | |||
f1981dc5f0 | |||
dbccc93981 | |||
2245806d12 | |||
7d90910ffd | |||
a5e809ff68 | |||
ef1177c76f | |||
a33f31df02 | |||
3940b1e77f | |||
67ea188182 | |||
c443656c03 | |||
ee21f684eb | |||
933c761a0d | |||
0b6490f501 | |||
e915b4479f | |||
bbd40d7017 | |||
3f3c480e1c | |||
c0e9db7234 | |||
786b849c8f | |||
7d715cc3b8 | |||
2164bc5a94 | |||
00b26e3bd0 | |||
0bf5b19f4a | |||
cf06bffd1b | |||
43031b24ad | |||
6e5c28f47c | |||
86866bad32 | |||
a21861178b | |||
234738465c | |||
f160c50d18 | |||
9aeca393dc | |||
5964fc611c | |||
ac3bc57875 | |||
40ad61ebab | |||
27e191cba6 | |||
cc1a60cb61 | |||
145686b453 | |||
44a875eeec | |||
62fe93e88e | |||
2853087836 | |||
4fbb0425e6 | |||
9af8d0eb1d | |||
045774ff3f | |||
400c13d6a2 | |||
62482fb5a3 | |||
68317f3b1c | |||
72608a444d | |||
89e21dc38a | |||
8f822e9469 | |||
a6b2ed8729 | |||
83dfd41784 | |||
52af60077a | |||
bf211e238f | |||
fd7a3bc747 | |||
825dc7c8b9 | |||
54940742f7 | |||
5187e34797 | |||
c5b7967ce1 | |||
99c864bcf1 | |||
97604d59c9 | |||
aa7b7afaf0 | |||
73b760ae68 | |||
1f8fecca7c | |||
384ab2fd9e | |||
432e5016a9 | |||
705740a0d0 | |||
d8499b4ce2 | |||
8f5024b169 | |||
4298fc9e40 | |||
4acb58b571 | |||
95b37f0cb8 | |||
2c2db147b4 | |||
e6aec071a2 | |||
376cb08a9f | |||
288af22ac6 | |||
713bfa4b50 | |||
adf8cfa750 | |||
783675d82a | |||
a680b6534c | |||
cb45c181ae | |||
c35026bfcc | |||
675066c216 | |||
d48b708a97 | |||
4153afa814 | |||
ccf38e2c07 | |||
a6caa40267 | |||
55767f88e4 | |||
4c860e7b20 | |||
9512db3402 | |||
7353b0347e | |||
1e5c83c369 | |||
bd052587c5 | |||
36391c8f20 | |||
7f60e02340 | |||
85b66aa040 | |||
a9ebad0793 | |||
5bfe492ada | |||
f17c9bb781 | |||
469ee275c9 | |||
ba662fda15 | |||
80656bdcdd | |||
000a169a00 | |||
9a2bc6b430 | |||
c3c0b2830e | |||
aab562ccf4 | |||
b210c1e7fa | |||
b0b0fa6140 | |||
f1dad3bc26 | |||
62276e2a02 |
62 changed files with 2214 additions and 742 deletions
|
@ -1,9 +1,14 @@
|
||||||
package net.codinux.banking.client
|
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.options.RetrieveTransactions
|
||||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
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.GetAccountDataResponse
|
||||||
|
import net.codinux.banking.client.model.response.GetTransactionsResponse
|
||||||
import net.codinux.banking.client.model.response.Response
|
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 {
|
interface BankingClient {
|
||||||
|
|
||||||
|
@ -30,4 +35,23 @@ interface BankingClient {
|
||||||
*/
|
*/
|
||||||
suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse>
|
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>
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,9 +1,15 @@
|
||||||
package net.codinux.banking.client
|
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.GetAccountDataOptions
|
||||||
import net.codinux.banking.client.model.options.RetrieveTransactions
|
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.GetAccountDataResponse
|
||||||
|
import net.codinux.banking.client.model.response.GetTransactionsResponse
|
||||||
import net.codinux.banking.client.model.response.Response
|
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 {
|
interface BankingClientForUser {
|
||||||
|
|
||||||
|
@ -30,4 +36,19 @@ interface BankingClientForUser {
|
||||||
*/
|
*/
|
||||||
suspend fun getAccountDataAsync(options: GetAccountDataOptions): Response<GetAccountDataResponse>
|
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>
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,15 +1,39 @@
|
||||||
package net.codinux.banking.client
|
package net.codinux.banking.client
|
||||||
|
|
||||||
import net.codinux.banking.client.model.AccountCredentials
|
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.options.GetAccountDataOptions
|
||||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
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(
|
abstract class BankingClientForUserBase(
|
||||||
protected val credentials: AccountCredentials,
|
protected val credentials: AccountCredentials,
|
||||||
protected val client: BankingClient
|
protected val client: BankingClient
|
||||||
) : BankingClientForUser {
|
) : BankingClientForUser {
|
||||||
|
|
||||||
|
private lateinit var bank: BankAccess
|
||||||
|
|
||||||
override suspend fun getAccountDataAsync(options: GetAccountDataOptions) =
|
override suspend fun getAccountDataAsync(options: GetAccountDataOptions) =
|
||||||
client.getAccountDataAsync(GetAccountDataRequest(credentials, options))
|
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))
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,21 +1,59 @@
|
||||||
package net.codinux.banking.client
|
package net.codinux.banking.client
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
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.options.GetAccountDataOptions
|
||||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
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 {
|
fun BankingClient.getAccountData(bankCode: String, loginName: String, password: String) = runBlocking {
|
||||||
this@getAccountData.getAccountDataAsync(bankCode, loginName, password)
|
getAccountDataAsync(bankCode, loginName, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
|
fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
|
||||||
this@getAccountData.getAccountDataAsync(request)
|
getAccountDataAsync(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BankingClient.updateAccountTransactions(bank: BankAccess, accounts: List<BankAccount>? = null, preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications) = runBlocking {
|
||||||
|
updateAccountTransactionsAsync(bank, accounts, preferredTanMethodsIfSelectedTanMethodIsNotAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun BankingClient.transferMoney(bankCode: String, loginName: String, password: String, recipientName: String,
|
||||||
|
recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null) = runBlocking {
|
||||||
|
transferMoneyAsync(bankCode, loginName, password, recipientName, recipientAccountIdentifier, amount, paymentReference)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BankingClient.transferMoney(request: TransferMoneyRequestForUser) = runBlocking {
|
||||||
|
transferMoneyAsync(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* BankingClientForUser */
|
||||||
|
|
||||||
fun BankingClientForUser.getAccountData() = runBlocking {
|
fun BankingClientForUser.getAccountData() = runBlocking {
|
||||||
this@getAccountData.getAccountDataAsync()
|
getAccountDataAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlocking {
|
fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlocking {
|
||||||
this@getAccountData.getAccountDataAsync(options)
|
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)
|
||||||
}
|
}
|
|
@ -79,6 +79,8 @@ kotlin {
|
||||||
|
|
||||||
val kotlinxDateTimeVersion: String by project
|
val kotlinxDateTimeVersion: String by project
|
||||||
val jsJodaTimeZoneVersion: String by project
|
val jsJodaTimeZoneVersion: String by project
|
||||||
|
val ionspinBigNumVersion: String by project
|
||||||
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
|
@ -102,12 +104,26 @@ kotlin {
|
||||||
jsMain {
|
jsMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api(npm("@js-joda/timezone", jsJodaTimeZoneVersion))
|
api(npm("@js-joda/timezone", jsJodaTimeZoneVersion))
|
||||||
|
|
||||||
|
implementation(npm("big.js", "6.0.3"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsTest { }
|
jsTest { }
|
||||||
|
|
||||||
nativeMain { }
|
nativeMain { }
|
||||||
nativeTest { }
|
nativeTest { }
|
||||||
|
|
||||||
|
linuxMain {
|
||||||
|
dependencies {
|
||||||
|
implementation("com.ionspin.kotlin:bignum:$ionspinBigNumVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mingwMain {
|
||||||
|
dependencies {
|
||||||
|
implementation("com.ionspin.kotlin:bignum:$ionspinBigNumVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
package net.codinux.banking.client.model
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
|
import net.codinux.banking.client.model.config.JsonIgnore
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class AccountTransaction(
|
open class AccountTransaction(
|
||||||
val amount: Amount = Amount.Zero,
|
val amount: Amount = Amount.Zero, // TODO: a string is really bad in UI, find a better solution
|
||||||
val currency: String,
|
val currency: String,
|
||||||
val reference: String, // Alternative: purpose (or Remittance information)
|
val reference: String?, // Alternative: Remittance information, Transaction description, (payment) purpose, payment reference
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transaction date (Buchungstag) - der Tag, an dem ein Zahlungsvorgang in das System einer Bank eingegangen ist.
|
* Transaction date (Buchungstag) - der Tag, an dem ein Zahlungsvorgang in das System einer Bank eingegangen ist.
|
||||||
|
@ -22,45 +24,130 @@ open class AccountTransaction(
|
||||||
*/
|
*/
|
||||||
val valueDate: LocalDate,
|
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
|
val otherPartyName: String? = null, // Alternatives: Parties involved, Transaction parties.single names: Beneficiary, Payee respectively Payer, Debtor
|
||||||
val otherPartyBankCode: String? = null,
|
/**
|
||||||
|
* 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 otherPartyAccountId: String? = null,
|
val otherPartyAccountId: String? = null,
|
||||||
|
|
||||||
val bookingText: String? = null,
|
/**
|
||||||
val information: String? = null,
|
* Buchungstext, z. B. DAUERAUFTRAG, BARGELDAUSZAHLUNG, ONLINE-UEBERWEISUNG, FOLGELASTSCHRIFT, ...
|
||||||
|
*/
|
||||||
val statementNumber: Int? = null,
|
val postingText: String? = null,
|
||||||
val sequenceNumber: Int? = null,
|
|
||||||
|
|
||||||
val openingBalance: Amount? = null,
|
val openingBalance: Amount? = null,
|
||||||
val closingBalance: Amount? = null,
|
val closingBalance: Amount? = null,
|
||||||
|
|
||||||
val endToEndReference: String? = null,
|
/**
|
||||||
|
* Auszugsnummer
|
||||||
|
*/
|
||||||
|
val statementNumber: Int? = null,
|
||||||
|
/**
|
||||||
|
* Blattnummer
|
||||||
|
*/
|
||||||
|
val sheetNumber: Int? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kundenreferenz.
|
||||||
|
*/
|
||||||
val customerReference: String? = null,
|
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 mandateReference: String? = null,
|
val mandateReference: String? = null,
|
||||||
val creditorIdentifier: String? = null,
|
val creditorIdentifier: String? = null,
|
||||||
val originatorsIdentificationCode: String? = null,
|
val originatorsIdentificationCode: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summe aus Auslagenersatz und Bearbeitungsprovision bei einer nationalen Rücklastschrift
|
||||||
|
* sowie optionalem Zinsausgleich.
|
||||||
|
*/
|
||||||
val compensationAmount: String? = null,
|
val compensationAmount: String? = null,
|
||||||
|
/**
|
||||||
|
* Betrag der ursprünglichen Lastschrift
|
||||||
|
*/
|
||||||
val originalAmount: String? = null,
|
val originalAmount: String? = null,
|
||||||
val sepaReference: String? = null,
|
/**
|
||||||
|
* Abweichender Überweisender oder Zahlungsempfänger
|
||||||
|
*/
|
||||||
val deviantOriginator: String? = null,
|
val deviantOriginator: String? = null,
|
||||||
|
/**
|
||||||
|
* Abweichender Zahlungsempfänger oder Zahlungspflichtiger
|
||||||
|
*/
|
||||||
val deviantRecipient: String? = null,
|
val deviantRecipient: String? = null,
|
||||||
val referenceWithNoSpecialType: String? = null,
|
val referenceWithNoSpecialType: String? = null,
|
||||||
val primaNotaNumber: String? = null,
|
|
||||||
val textKeySupplement: String? = null,
|
|
||||||
|
|
||||||
val currencyType: String? = null,
|
/**
|
||||||
val bookingKey: String? = null,
|
* Primanoten-Nr.
|
||||||
val referenceForTheAccountOwner: String? = null,
|
*/
|
||||||
val referenceOfTheAccountServicingInstitution: String? = null,
|
val journalNumber: String? = null,
|
||||||
val supplementaryDetails: 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 transactionReferenceNumber: String? = null,
|
/**
|
||||||
val relatedReferenceNumber: 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,
|
||||||
|
|
||||||
var userSetDisplayName: 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 category: String? = null,
|
||||||
var notes: 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"
|
override fun toString() = "${valueDate.dayOfMonth}.${valueDate.monthNumber}.${valueDate.year} ${amount.toString().padStart(4, ' ')} ${if (currency == "EUR") "€" else currency} ${otherPartyName ?: ""} - $reference"
|
||||||
}
|
}
|
|
@ -1,18 +1,44 @@
|
||||||
package net.codinux.banking.client.model
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
import kotlin.jvm.JvmInline
|
|
||||||
|
|
||||||
@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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
value class Amount(val amount: String = "0") {
|
expect class Amount(amount: String = "0") {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Zero = Amount("0")
|
val Zero: Amount
|
||||||
|
|
||||||
fun fromString(amount: String): Amount = Amount(amount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun toString() = 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
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
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"
|
||||||
|
|
||||||
|
}
|
|
@ -1,32 +1,34 @@
|
||||||
package net.codinux.banking.client.model
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.*
|
||||||
|
import net.codinux.banking.client.model.config.JsonIgnore
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
import net.codinux.banking.client.model.securitiesaccount.Holding
|
||||||
|
|
||||||
|
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class BankAccount(
|
open class BankAccount(
|
||||||
val identifier: String,
|
val identifier: String,
|
||||||
|
val subAccountNumber: String? = null,
|
||||||
|
val iban: String? = null,
|
||||||
|
val productName: String? = null,
|
||||||
var accountHolderName: String,
|
var accountHolderName: String,
|
||||||
val type: BankAccountType = BankAccountType.Other,
|
val type: BankAccountType = BankAccountType.Other,
|
||||||
val iban: String? = null,
|
val currency: String = DefaultValues.DefaultCurrency,
|
||||||
val subAccountNumber: String? = null,
|
|
||||||
val productName: String? = null,
|
|
||||||
val currency: String = "EUR",
|
|
||||||
var accountLimit: String? = null,
|
var accountLimit: String? = null,
|
||||||
|
|
||||||
val isAccountTypeSupportedByApplication: Boolean = true,
|
val isAccountTypeSupportedByApplication: Boolean = false,
|
||||||
val features: Set<BankAccountFeatures> = emptySet(),
|
val features: Set<BankAccountFeatures> = emptySet(),
|
||||||
|
|
||||||
// var balance: BigDecimal = BigDecimal.ZERO,
|
var balance: Amount = Amount.Zero,
|
||||||
var balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
|
|
||||||
|
val serverTransactionsRetentionDays: Int? = null,
|
||||||
|
open var lastAccountUpdateTime: Instant? = null,
|
||||||
var retrievedTransactionsFrom: LocalDate? = null,
|
var retrievedTransactionsFrom: LocalDate? = null,
|
||||||
var retrievedTransactionsTo: LocalDate? = null,
|
|
||||||
|
|
||||||
var haveAllTransactionsBeenRetrieved: Boolean = false,
|
open val bookedTransactions: MutableList<out AccountTransaction> = mutableListOf(),
|
||||||
val countDaysForWhichTransactionsAreKept: Int? = null,
|
open val prebookedTransactions: MutableList<out PrebookedAccountTransaction> = mutableListOf(),
|
||||||
|
open val holdings: MutableList<out Holding> = mutableListOf(),
|
||||||
open val bookedTransactions: MutableList<AccountTransaction> = mutableListOf(),
|
|
||||||
open val unbookedTransactions: MutableList<UnbookedAccountTransaction> = mutableListOf(),
|
|
||||||
|
|
||||||
var userSetDisplayName: String? = null,
|
var userSetDisplayName: String? = null,
|
||||||
var displayIndex: Int = 0,
|
var displayIndex: Int = 0,
|
||||||
|
@ -34,5 +36,56 @@ open class BankAccount(
|
||||||
var hideAccount: Boolean = false,
|
var hideAccount: Boolean = false,
|
||||||
var includeInAutomaticAccountsUpdate: Boolean = true
|
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)"
|
override fun toString() = "$type $identifier $productName (IBAN: $iban)"
|
||||||
}
|
}
|
|
@ -4,5 +4,5 @@ enum class BankAccountFeatures {
|
||||||
RetrieveTransactions,
|
RetrieveTransactions,
|
||||||
RetrieveBalance,
|
RetrieveBalance,
|
||||||
TransferMoney,
|
TransferMoney,
|
||||||
InstantPayment
|
InstantTransfer
|
||||||
}
|
}
|
|
@ -8,5 +8,7 @@ open class BankAccountIdentifier(
|
||||||
val subAccountNumber: String?,
|
val subAccountNumber: String?,
|
||||||
val iban: String?
|
val iban: String?
|
||||||
) {
|
) {
|
||||||
|
constructor(account: BankAccount) : this(account.identifier, account.subAccountNumber, account.iban)
|
||||||
|
|
||||||
override fun toString() = "$identifier, $iban"
|
override fun toString() = "$identifier, $iban"
|
||||||
}
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
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"
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
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"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
object DefaultValues {
|
||||||
|
const val DefaultCurrency = "EUR"
|
||||||
|
}
|
|
@ -6,7 +6,18 @@ import kotlinx.datetime.Instant
|
||||||
open class MessageLogEntry(
|
open class MessageLogEntry(
|
||||||
open val type: MessageLogEntryType,
|
open val type: MessageLogEntryType,
|
||||||
open val message: String,
|
open val message: String,
|
||||||
open val messageTrace: String? = null,
|
open val messageWithoutSensitiveData: String? = null,
|
||||||
open val error: Throwable? = null,
|
open val error: Throwable? = null,
|
||||||
open val time: Instant = Clock.System.now()
|
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"
|
||||||
|
}
|
|
@ -3,5 +3,5 @@ package net.codinux.banking.client.model
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class UnbookedAccountTransaction {
|
open class PrebookedAccountTransaction {
|
||||||
}
|
}
|
|
@ -1,65 +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 UserAccount(
|
|
||||||
val bankCode: String,
|
|
||||||
var loginName: String,
|
|
||||||
/**
|
|
||||||
* User may decides to not save password .
|
|
||||||
*/
|
|
||||||
var password: String?,
|
|
||||||
|
|
||||||
val bankName: String,
|
|
||||||
val bic: String,
|
|
||||||
|
|
||||||
val customerName: String,
|
|
||||||
val userId: String = loginName,
|
|
||||||
|
|
||||||
open val accounts: List<BankAccount> = emptyList(),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifier of selected TanMethod.
|
|
||||||
*
|
|
||||||
* As [tanMethods] also contains selected TanMethod, we didn't want to duplicate this object. Use
|
|
||||||
* [selectedTanMethod] to get selected TanMethod or iterate over [tanMethods] and filter selected one by this id.
|
|
||||||
*/
|
|
||||||
val selectedTanMethodId: String? = null,
|
|
||||||
open val tanMethods: List<TanMethod> = listOf(),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifier of selected TanMedium.
|
|
||||||
*
|
|
||||||
* As [tanMedia] also contains selected TanMedium, we didn't want to duplicate this object. Use [selectedTanMedium]
|
|
||||||
* to get selected TanMedium or iterate over [tanMedia] and filter selected one by this medium name.
|
|
||||||
*/
|
|
||||||
val selectedTanMediumName: String? = null,
|
|
||||||
open val tanMedia: List<TanMedium> = listOf(),
|
|
||||||
|
|
||||||
var bankingGroup: BankingGroup? = null,
|
|
||||||
var iconUrl: String? = null,
|
|
||||||
) {
|
|
||||||
|
|
||||||
var wrongCredentialsEntered: Boolean = false
|
|
||||||
|
|
||||||
var userSetDisplayName: String? = null
|
|
||||||
var displayIndex: Int = 0
|
|
||||||
|
|
||||||
|
|
||||||
@get:JsonIgnore
|
|
||||||
val selectedTanMethod: TanMethod
|
|
||||||
get() = tanMethods.first { it.identifier == selectedTanMethodId }
|
|
||||||
|
|
||||||
@get:JsonIgnore
|
|
||||||
val selectedTanMedium: TanMedium?
|
|
||||||
get() = tanMedia.firstOrNull { it.mediumName == selectedTanMediumName }
|
|
||||||
|
|
||||||
|
|
||||||
override fun toString() = "$bankName $loginName, ${accounts.size} accounts"
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package net.codinux.banking.client.model
|
|
||||||
|
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains only the basic info of a [UserAccount], just enough that a client application can display it to the user
|
|
||||||
* and the user knows exactly which [UserAccount] is meant / referred.
|
|
||||||
*/
|
|
||||||
@NoArgConstructor
|
|
||||||
open class UserAccountViewInfo(
|
|
||||||
val bankCode: String,
|
|
||||||
var loginName: String,
|
|
||||||
val bankName: String,
|
|
||||||
val bankingGroup: BankingGroup? = null
|
|
||||||
) {
|
|
||||||
override fun toString() = "$bankCode $bankName $loginName"
|
|
||||||
}
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
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")
|
|
@ -27,14 +27,16 @@ open class GetAccountDataOptions(
|
||||||
* likes to use a different one, she can select another one in EnterTanDialog.
|
* 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
|
* By default we prefer non visual TanMethods (like AppTan and SMS) over image based TanMethods (like QR-code and
|
||||||
* photoTan) and exclude ChipTanUsb, which is not supported by application, and Flickercode, which is hard to
|
* photoTan) and exclude ChipTanUsb, which is not supported by application, and FlickerCode, which is hard to
|
||||||
* implement and therefore most applications have not implemented.
|
* implement and therefore most applications have not implemented.
|
||||||
*
|
*
|
||||||
* Console apps can only handle non visual TanMethods.
|
* 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
|
* 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.
|
* TAN, and then image based TanMethods as then they additionally only have to display an image.
|
||||||
*/
|
*/
|
||||||
val preferredTanMethods: List<TanMethodType>? = TanMethodType.NonVisualOrImageBased,
|
val preferredTanMethods: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications,
|
||||||
|
|
||||||
|
val tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications,
|
||||||
|
|
||||||
val abortIfTanIsRequired: Boolean = false,
|
val abortIfTanIsRequired: Boolean = false,
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
package net.codinux.banking.client.model.request
|
package net.codinux.banking.client.model.request
|
||||||
|
|
||||||
import net.codinux.banking.client.model.AccountCredentials
|
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.config.NoArgConstructor
|
||||||
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class GetAccountDataRequest(bankCode: String, loginName: String, password: String, val options: GetAccountDataOptions? = null)
|
open class GetAccountDataRequest(bankCode: String, loginName: String, password: String, val options: GetAccountDataOptions? = null, val bankInfo: BankInfo? = null)
|
||||||
: AccountCredentials(bankCode, loginName, password) {
|
: AccountCredentials(bankCode, loginName, password) {
|
||||||
|
|
||||||
constructor(credentials: AccountCredentials, options: GetAccountDataOptions? = null)
|
constructor(credentials: AccountCredentials, options: GetAccountDataOptions? = null)
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
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"
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
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()}"
|
||||||
|
}
|
|
@ -1,20 +1,21 @@
|
||||||
package net.codinux.banking.client.model.response
|
package net.codinux.banking.client.model.response
|
||||||
|
|
||||||
import net.codinux.banking.client.model.AccountTransaction
|
import net.codinux.banking.client.model.AccountTransaction
|
||||||
import net.codinux.banking.client.model.UserAccount
|
import net.codinux.banking.client.model.BankAccess
|
||||||
|
import net.codinux.banking.client.model.MessageLogEntry
|
||||||
import net.codinux.banking.client.model.config.JsonIgnore
|
import net.codinux.banking.client.model.config.JsonIgnore
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class GetAccountDataResponse(
|
open class GetAccountDataResponse(
|
||||||
val user: UserAccount
|
val bank: BankAccess
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val bookedTransactions: List<AccountTransaction>
|
val bookedTransactions: List<AccountTransaction>
|
||||||
get() = user.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
|
get() = bank.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
|
||||||
|
|
||||||
|
|
||||||
override fun toString() = user.toString()
|
override fun toString() = bank.toString()
|
||||||
}
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
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"
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package net.codinux.banking.client.model.response
|
package net.codinux.banking.client.model.response
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.MessageLogEntry
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
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
|
// TODO: may differentiate between ClientResponse, which is either Success or Error, and RestResponse, which can be Success, Error and TanRequired
|
||||||
|
@ -8,21 +9,22 @@ open class Response<T> protected constructor(
|
||||||
val type: ResponseType,
|
val type: ResponseType,
|
||||||
val data: T? = null,
|
val data: T? = null,
|
||||||
val error: Error? = null,
|
val error: Error? = null,
|
||||||
val tanRequired: TanRequired? = null
|
val tanRequired: TanRequired? = null,
|
||||||
|
val messageLog: List<MessageLogEntry> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun <T> success(data: T): Response<T> =
|
fun <T> success(data: T, messageLog: List<MessageLogEntry> = emptyList()): Response<T> =
|
||||||
Response(ResponseType.Success, data)
|
Response(ResponseType.Success, data, messageLog = messageLog)
|
||||||
|
|
||||||
fun <T> error(errorType: ErrorType, internalError: String? = null, errorMessagesFromBank: List<String> = emptyList()): Response<T> =
|
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))
|
Response(ResponseType.Error, null, Error(errorType, internalError, errorMessagesFromBank), messageLog = messageLog)
|
||||||
|
|
||||||
fun <T> tanRequired(tanRequired: TanRequired): Response<T> =
|
fun <T> tanRequired(tanRequired: TanRequired, messageLog: List<MessageLogEntry> = emptyList()): Response<T> =
|
||||||
Response(ResponseType.TanRequired, null, null, tanRequired)
|
Response(ResponseType.TanRequired, null, null, tanRequired, messageLog)
|
||||||
|
|
||||||
fun <T> bankReturnedError(errorMessagesFromBank: List<String>): Response<T> =
|
fun <T> bankReturnedError(errorMessagesFromBank: List<String>, messageLog: List<MessageLogEntry> = emptyList()): Response<T> =
|
||||||
Response.error(ErrorType.BankReturnedError, null, errorMessagesFromBank)
|
Response.error(ErrorType.BankReturnedError, null, errorMessagesFromBank, messageLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
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
|
|
@ -0,0 +1,53 @@
|
||||||
|
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"
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class EnterTanResult(
|
open class EnterTanResult(
|
||||||
val enteredTan: String?,
|
val enteredTan: String?,
|
||||||
// val changeTanMethodTo: TanMethod? = null,
|
val changeTanMethodTo: TanMethod? = null,
|
||||||
// val changeTanMediumTo: TanMedium? = null,
|
// val changeTanMediumTo: TanMedium? = null,
|
||||||
// val changeTanMediumResultCallback: ((BankingClientResponse) -> Unit)? = null
|
// val changeTanMediumResultCallback: ((BankingClientResponse) -> Unit)? = null
|
||||||
)
|
)
|
|
@ -7,13 +7,13 @@ import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class FlickerCode(
|
open class FlickerCode(
|
||||||
val challengeHHD_UC: String,
|
val challengeHHD_UC: String,
|
||||||
val parsedDataSet: String,
|
val parsedDataSet: String? = null,
|
||||||
val decodingError: String? = null
|
val decodingError: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val decodingSuccessful: Boolean
|
val decodingSuccessful: Boolean
|
||||||
get() = decodingError == null
|
get() = parsedDataSet != null
|
||||||
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
package net.codinux.banking.client.model.tan
|
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.BankAccountViewInfo
|
||||||
import net.codinux.banking.client.model.UserAccount
|
import net.codinux.banking.client.model.BankAccess
|
||||||
import net.codinux.banking.client.model.UserAccountViewInfo
|
import net.codinux.banking.client.model.BankViewInfo
|
||||||
import net.codinux.banking.client.model.config.JsonIgnore
|
import net.codinux.banking.client.model.config.JsonIgnore
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class TanChallenge(
|
open class TanChallenge(
|
||||||
val type: TanChallengeType,
|
open val type: TanChallengeType,
|
||||||
val forAction: ActionRequiringTan,
|
open val forAction: ActionRequiringTan,
|
||||||
val messageToShowToUser: String,
|
open val messageToShowToUser: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifier of selected TanMethod.
|
* Identifier of selected TanMethod.
|
||||||
|
@ -19,16 +21,16 @@ open class TanChallenge(
|
||||||
* As [availableTanMethods] also contains selected TanMethod, we didn't want to duplicate this object. Use
|
* 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.
|
* [selectedTanMethod] to get selected TanMethod or iterate over [availableTanMethods] and filter selected one by this id.
|
||||||
*/
|
*/
|
||||||
val selectedTanMethodId: String,
|
open val selectedTanMethodIdentifier: String,
|
||||||
/**
|
/**
|
||||||
* When adding an account, frontend has no UserAccount object in BankingClientCallback to know which TanMethods are
|
* When adding an account, frontend has no UserAccount object in BankingClientCallback to know which TanMethods are
|
||||||
* available for User.
|
* available for user.
|
||||||
* Also on other calls to bank server, bank server may returned an updated list of available TanMethods, so that
|
* Also on other calls to bank server, bank server may returned an updated list of available TanMethods, so that
|
||||||
* [UserAccount] may contains an outdated list of available TanMethods.
|
* [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.
|
* Therefore i added list with up to date TanMethods here to ensure EnterTanDialog can display user's up to date TanMethods.
|
||||||
*/
|
*/
|
||||||
val availableTanMethods: List<TanMethod>,
|
open val availableTanMethods: List<TanMethod>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifier of selected TanMedium.
|
* Identifier of selected TanMedium.
|
||||||
|
@ -36,29 +38,57 @@ open class TanChallenge(
|
||||||
* As [availableTanMedia] also contains selected TanMedium, we didn't want to duplicate this object. Use
|
* 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.
|
* [selectedTanMedium] to get selected TanMedium or iterate over [availableTanMedia] and filter selected one by this medium name.
|
||||||
*/
|
*/
|
||||||
val selectedTanMediumName: String? = null,
|
open val selectedTanMediumIdentifier: String? = null,
|
||||||
val availableTanMedia: List<TanMedium> = emptyList(),
|
open val availableTanMedia: List<TanMedium> = emptyList(),
|
||||||
|
|
||||||
val tanImage: TanImage? = null,
|
open val tanImage: TanImage? = null,
|
||||||
val flickerCode: FlickerCode? = null,
|
open val flickerCode: FlickerCode? = null,
|
||||||
val user: UserAccountViewInfo,
|
open val bank: BankViewInfo,
|
||||||
val account: BankAccountViewInfo? = null
|
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()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val selectedTanMethod: TanMethod
|
open val selectedTanMethod: TanMethod
|
||||||
get() = availableTanMethods.first { it.identifier == selectedTanMethodId }
|
get() = availableTanMethods.first { it.identifier == selectedTanMethodIdentifier }
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val selectedTanMedium: TanMedium?
|
open val selectedTanMedium: TanMedium?
|
||||||
get() = availableTanMedia.firstOrNull { it.mediumName == selectedTanMediumName }
|
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 {
|
override fun toString(): String {
|
||||||
return "$selectedTanMethod $forAction: $messageToShowToUser" + when (type) {
|
return "$selectedTanMethod $forAction: $messageToShowToUser" + when (type) {
|
||||||
TanChallengeType.EnterTan -> ""
|
TanChallengeType.EnterTan -> ""
|
||||||
TanChallengeType.Image -> ", Image: $tanImage"
|
TanChallengeType.Image -> ", Image: $tanImage"
|
||||||
TanChallengeType.Flickercode -> ", FlickerCode: $flickerCode"
|
TanChallengeType.FlickerCode -> ", FlickerCode: $flickerCode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package net.codinux.banking.client.model.tan
|
||||||
enum class TanChallengeType {
|
enum class TanChallengeType {
|
||||||
Image,
|
Image,
|
||||||
|
|
||||||
Flickercode,
|
FlickerCode,
|
||||||
|
|
||||||
EnterTan
|
EnterTan
|
||||||
}
|
}
|
|
@ -4,6 +4,10 @@ import kotlinx.datetime.LocalDate
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
@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(
|
open class TanGeneratorTanMedium(
|
||||||
val cardNumber: String,
|
val cardNumber: String,
|
||||||
val cardSequenceNumber: String? = null,
|
val cardSequenceNumber: String? = null,
|
||||||
|
|
|
@ -6,22 +6,22 @@ import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class TanImage(
|
open class TanImage(
|
||||||
val mimeType: String,
|
val mimeType: String? = null,
|
||||||
val imageBytesBase64: String,
|
val imageBytesBase64: String? = null,
|
||||||
val decodingError: String? = null
|
val decodingError: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val decodingSuccessful: Boolean
|
val decodingSuccessful: Boolean
|
||||||
get() = decodingError == null
|
get() = mimeType != null && imageBytesBase64 != null
|
||||||
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
if (decodingSuccessful == false) {
|
mimeType?.let {
|
||||||
return "Decoding error: $decodingError"
|
return mimeType
|
||||||
}
|
}
|
||||||
|
|
||||||
return mimeType
|
return "Decoding error: $decodingError"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -14,7 +14,54 @@ open class TanMedium(
|
||||||
/**
|
/**
|
||||||
* Only set if [type] is [TanMediumType.MobilePhone].
|
* Only set if [type] is [TanMediumType.MobilePhone].
|
||||||
*/
|
*/
|
||||||
val mobilePhone: MobilePhoneTanMedium? = null
|
val mobilePhone: MobilePhoneTanMedium? = null,
|
||||||
|
|
||||||
|
var userSetDisplayName: String? = 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() = "$mediumName $status"
|
||||||
}
|
}
|
|
@ -1,14 +1,22 @@
|
||||||
package net.codinux.banking.client.model.tan
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.JsonIgnore
|
||||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||||
@NoArgConstructor
|
@NoArgConstructor
|
||||||
open class TanMethod(
|
open class TanMethod(
|
||||||
val displayName: String,
|
open val displayName: String,
|
||||||
val type: TanMethodType,
|
open val type: TanMethodType,
|
||||||
val identifier: String,
|
open val identifier: String,
|
||||||
val maxTanInputLength: Int? = null,
|
open val maxTanInputLength: Int? = null,
|
||||||
val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
|
open val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric,
|
||||||
|
open var userSetDisplayName: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
open val isNumericTan: Boolean
|
||||||
|
get() = allowedTanFormat == AllowedTanFormat.Numeric
|
||||||
|
|
||||||
override fun toString() = "$displayName ($type, ${identifier})"
|
override fun toString() = "$displayName ($type, ${identifier})"
|
||||||
}
|
}
|
|
@ -3,9 +3,9 @@ package net.codinux.banking.client.model.tan
|
||||||
enum class TanMethodType {
|
enum class TanMethodType {
|
||||||
EnterTan,
|
EnterTan,
|
||||||
|
|
||||||
ChipTanManuell,
|
ChipTanManual,
|
||||||
|
|
||||||
ChipTanFlickercode,
|
ChipTanFlickerCode,
|
||||||
|
|
||||||
ChipTanUsb,
|
ChipTanUsb,
|
||||||
|
|
||||||
|
@ -17,24 +17,52 @@ enum class TanMethodType {
|
||||||
|
|
||||||
AppTan,
|
AppTan,
|
||||||
|
|
||||||
|
DecoupledTan,
|
||||||
|
|
||||||
|
DecoupledPushTan,
|
||||||
|
|
||||||
photoTan,
|
photoTan,
|
||||||
|
|
||||||
QrCode
|
QrCode
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
val isDecoupledMethod: Boolean
|
||||||
|
get() = this == DecoupledTan || this == DecoupledPushTan
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val NonVisual = listOf(TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.ChipTanManuell, TanMethodType.EnterTan)
|
val NonVisual = listOf(TanMethodType.DecoupledTan, TanMethodType.DecoupledPushTan, TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.ChipTanManual, TanMethodType.EnterTan)
|
||||||
|
|
||||||
val ImageBased = listOf(TanMethodType.QrCode, TanMethodType.ChipTanQrCode, TanMethodType.photoTan, TanMethodType.ChipTanPhotoTanMatrixCode)
|
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 {
|
val NonVisualOrImageBased = buildList {
|
||||||
addAll(listOf(TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.EnterTan))
|
addAll(NonVisualWithoutChipTanManual)
|
||||||
addAll(ImageBased)
|
addAll(ImageBased)
|
||||||
addAll(listOf(TanMethodType.ChipTanManuell)) // this is quite inconvenient for user, so i added it at last
|
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
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()
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
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?
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
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()
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
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()
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
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()
|
||||||
|
|
||||||
|
}
|
|
@ -4,13 +4,11 @@ import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
|
|
||||||
id("maven-publish")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(8)
|
jvmToolchain(11)
|
||||||
|
|
||||||
jvm {
|
jvm {
|
||||||
withJava()
|
withJava()
|
||||||
|
@ -34,7 +32,7 @@ kotlin {
|
||||||
browser {
|
browser {
|
||||||
testTask {
|
testTask {
|
||||||
useKarma {
|
useKarma {
|
||||||
useChromeHeadless()
|
// useChromeHeadless()
|
||||||
useFirefoxHeadless()
|
useFirefoxHeadless()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +75,7 @@ kotlin {
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":BankingClient"))
|
api(project(":BankingClient"))
|
||||||
|
|
||||||
api("net.codinux.banking:fints4k:1.0.0-Alpha-12")
|
implementation("net.codinux.banking:fints4k:1.0.0-Alpha-15")
|
||||||
|
|
||||||
api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
|
api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
|
||||||
}
|
}
|
||||||
|
@ -115,21 +113,6 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ package net.codinux.banking.client.fints4k
|
||||||
import net.codinux.banking.client.BankingClientCallback
|
import net.codinux.banking.client.BankingClientCallback
|
||||||
import net.codinux.banking.client.model.MessageLogEntryType
|
import net.codinux.banking.client.model.MessageLogEntryType
|
||||||
import net.codinux.banking.fints.callback.FinTsClientCallback
|
import net.codinux.banking.fints.callback.FinTsClientCallback
|
||||||
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.model.BankData
|
import net.codinux.banking.fints.model.BankData
|
||||||
import net.codinux.banking.fints.model.EnterTanGeneratorAtcResult
|
import net.codinux.banking.fints.model.EnterTanGeneratorAtcResult
|
||||||
import net.codinux.banking.fints.model.MessageLogEntry
|
import net.codinux.banking.fints.model.MessageLogEntry
|
||||||
|
@ -22,22 +22,21 @@ open class BridgeFintTsToBankingClientCallback(
|
||||||
bankingClientCallback.enterTan(mapper.mapTanChallenge(tanChallenge)) { enterTanResult ->
|
bankingClientCallback.enterTan(mapper.mapTanChallenge(tanChallenge)) { enterTanResult ->
|
||||||
if (enterTanResult.enteredTan != null) {
|
if (enterTanResult.enteredTan != null) {
|
||||||
tanChallenge.userEnteredTan(enterTanResult.enteredTan!!)
|
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 {
|
} else {
|
||||||
tanChallenge.userDidNotEnterTan()
|
tanChallenge.userDidNotEnterTan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
|
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanMedium): EnterTanGeneratorAtcResult {
|
||||||
return EnterTanGeneratorAtcResult.userDidNotEnterAtc()
|
return EnterTanGeneratorAtcResult.userDidNotEnterAtc()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun messageLogAdded(messageLogEntry: MessageLogEntry) {
|
override fun messageLogAdded(messageLogEntry: MessageLogEntry) {
|
||||||
val mapped = net.codinux.banking.client.model.MessageLogEntry(
|
val mapped = mapper.mapMessageLogEntry(messageLogEntry)
|
||||||
MessageLogEntryType.valueOf(messageLogEntry.type.name),
|
|
||||||
messageLogEntry.message, messageLogEntry.messageTrace,
|
|
||||||
messageLogEntry.error, messageLogEntry.time
|
|
||||||
)
|
|
||||||
|
|
||||||
bankingClientCallback.messageLogAdded(mapped)
|
bankingClientCallback.messageLogAdded(mapped)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,14 @@ package net.codinux.banking.client.fints4k
|
||||||
|
|
||||||
import net.codinux.banking.client.BankingClient
|
import net.codinux.banking.client.BankingClient
|
||||||
import net.codinux.banking.client.BankingClientCallback
|
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.options.GetAccountDataOptions
|
||||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||||
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
|
||||||
import net.codinux.banking.client.model.response.Response
|
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.FinTsClient
|
||||||
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
||||||
|
|
||||||
|
@ -16,16 +20,48 @@ open class FinTs4kBankingClient(
|
||||||
|
|
||||||
constructor(callback: BankingClientCallback) : this(FinTsClientConfiguration(), callback)
|
constructor(callback: BankingClientCallback) : this(FinTsClientConfiguration(), callback)
|
||||||
|
|
||||||
|
constructor(options: FinTsClientOptions, callback: BankingClientCallback)
|
||||||
|
: this(FinTsClientConfiguration(net.codinux.banking.fints.config.FinTsClientOptions(options.collectMessageLog, false, options.removeSensitiveDataFromMessageLog, options.appendFinTsMessagesToLog, options.closeDialogs, options.version, options.productName)), callback)
|
||||||
|
|
||||||
protected val mapper = FinTs4kMapper()
|
|
||||||
|
|
||||||
protected val client = FinTsClient(config, BridgeFintTsToBankingClientCallback(callback, mapper))
|
protected open val mapper = FinTs4kMapper()
|
||||||
|
|
||||||
|
protected open val client = FinTsClient(config, BridgeFintTsToBankingClientCallback(callback, mapper))
|
||||||
|
|
||||||
|
|
||||||
override suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse> {
|
override suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse> {
|
||||||
val response = client.getAccountDataAsync(mapper.mapToGetAccountDataParameter(request, request.options ?: GetAccountDataOptions()))
|
val response = client.getAccountDataAsync(mapper.mapToGetAccountDataParameter(request, request.options ?: GetAccountDataOptions()))
|
||||||
|
|
||||||
return mapper.map(response)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@ import net.codinux.banking.client.BankingClientForUserBase
|
||||||
import net.codinux.banking.client.model.AccountCredentials
|
import net.codinux.banking.client.model.AccountCredentials
|
||||||
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
||||||
|
|
||||||
open class FinTs4KBankingClientForUser(credentials: AccountCredentials, config: FinTsClientConfiguration = FinTsClientConfiguration(), callback: BankingClientCallback)
|
open class FinTs4kBankingClientForUser(credentials: AccountCredentials, config: FinTsClientConfiguration = FinTsClientConfiguration(), callback: BankingClientCallback)
|
||||||
: BankingClientForUserBase(credentials, FinTs4kBankingClient(config, callback)) {
|
: BankingClientForUserBase(credentials, FinTs4kBankingClient(config, callback)) {
|
||||||
|
|
||||||
constructor(bankCode: String, loginName: String, password: String, callback: BankingClientCallback)
|
constructor(bankCode: String, loginName: String, password: String, callback: BankingClientCallback)
|
|
@ -1,12 +1,22 @@
|
||||||
package net.codinux.banking.client.fints4k
|
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.*
|
||||||
import net.codinux.banking.client.model.AccountTransaction
|
import net.codinux.banking.client.model.AccountTransaction
|
||||||
import net.codinux.banking.client.model.Amount
|
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.tan.*
|
||||||
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
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.response.*
|
||||||
import net.codinux.banking.client.model.tan.ActionRequiringTan
|
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.TanChallenge
|
||||||
import net.codinux.banking.client.model.tan.TanImage
|
import net.codinux.banking.client.model.tan.TanImage
|
||||||
import net.codinux.banking.client.model.tan.TanMethod
|
import net.codinux.banking.client.model.tan.TanMethod
|
||||||
|
@ -15,46 +25,127 @@ import net.dankito.banking.client.model.BankAccountIdentifierImpl
|
||||||
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
||||||
import net.dankito.banking.client.model.parameter.RetrieveTransactions
|
import net.dankito.banking.client.model.parameter.RetrieveTransactions
|
||||||
import net.dankito.banking.client.model.response.ErrorCode
|
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.mapper.FinTsModelMapper
|
||||||
import net.codinux.banking.fints.model.*
|
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.*
|
||||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
|
||||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.MobilePhoneTanMedium
|
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.MobilePhoneTanMedium
|
||||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
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.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.banklistcreator.prettifier.BankingGroupMapper
|
||||||
|
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
open class FinTs4kMapper {
|
open class FinTs4kMapper {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TanMethodTypesToMigrate = mapOf(
|
||||||
|
net.codinux.banking.fints.model.TanMethodType.ChipTanManuell.name to TanMethodType.ChipTanManual.name,
|
||||||
|
net.codinux.banking.fints.model.TanMethodType.ChipTanFlickercode.name to TanMethodType.ChipTanFlickerCode.name
|
||||||
|
)
|
||||||
|
|
||||||
|
val TanMethodTypesToMigrateReverse = TanMethodTypesToMigrate.map { it.value to it.key }.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected val fintsModelMapper = FinTsModelMapper()
|
protected val fintsModelMapper = FinTsModelMapper()
|
||||||
|
|
||||||
protected val bankingGroupMapper = BankingGroupMapper()
|
protected val bankingGroupMapper = BankingGroupMapper()
|
||||||
|
|
||||||
|
|
||||||
open fun mapToGetAccountDataParameter(credentials: AccountCredentials, options: GetAccountDataOptions) = GetAccountDataParameter(
|
open fun mapToGetAccountDataParameter(request: GetAccountDataRequest, options: GetAccountDataOptions) = GetAccountDataParameter(
|
||||||
credentials.bankCode, credentials.loginName, credentials.password,
|
request.bankCode, request.loginName, request.password,
|
||||||
options.accounts.map { BankAccountIdentifierImpl(it.identifier, it.subAccountNumber, it.iban) },
|
options.accounts.map { mapBankAccountIdentifier(it) },
|
||||||
options.retrieveBalance,
|
options.retrieveBalance,
|
||||||
RetrieveTransactions.valueOf(options.retrieveTransactions.name), options.retrieveTransactionsFrom, options.retrieveTransactionsTo,
|
RetrieveTransactions.valueOf(options.retrieveTransactions.name), options.retrieveTransactionsFrom, options.retrieveTransactionsTo,
|
||||||
preferredTanMethods = options.preferredTanMethods?.map { mapTanMethodType(it) },
|
preferredTanMethods = options.preferredTanMethods?.map { mapTanMethodType(it) },
|
||||||
abortIfTanIsRequired = options.abortIfTanIsRequired
|
tanMethodsNotSupportedByApplication = options.tanMethodsNotSupportedByApplication.map { mapTanMethodType(it) },
|
||||||
|
abortIfTanIsRequired = options.abortIfTanIsRequired,
|
||||||
|
defaultBankValues = request.bankInfo?.let { mapToBankData(request, it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 =
|
protected open fun mapTanMethodType(type: TanMethodType): net.codinux.banking.fints.model.TanMethodType =
|
||||||
net.codinux.banking.fints.model.TanMethodType.valueOf(type.name)
|
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): Response<GetAccountDataResponse> {
|
open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse, bankInfo: BankInfo? = null): Response<GetAccountDataResponse> =
|
||||||
return if (response.successful && response.customerAccount != null) {
|
if (response.successful && response.customerAccount != null) {
|
||||||
Response.success(GetAccountDataResponse(mapUser(response.customerAccount!!)))
|
val bank = mapBank(response.customerAccount!!, bankInfo, response)
|
||||||
|
Response.success(GetAccountDataResponse(bank), mapMessageLog(response, bank))
|
||||||
} else {
|
} else {
|
||||||
mapError(response)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 mapToUserAccountViewInfo(bank: BankData): UserAccountViewInfo = UserAccountViewInfo(
|
open fun mapToBankViewInfo(bank: BankData): BankViewInfo = BankViewInfo(
|
||||||
bank.bankCode, bank.customerId, bank.bankName, getBankingGroup(bank.bankName, bank.bic)
|
bank.bankCode, bank.customerId, bank.bankName, getBankingGroup(bank.bankName, bank.bic)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,29 +156,35 @@ open class FinTs4kMapper {
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
protected open fun mapUser(user: net.dankito.banking.client.model.CustomerAccount) = UserAccount(
|
protected open fun mapBank(bank: net.dankito.banking.client.model.CustomerAccount, info: BankInfo? = null, response: FinTsClientResponse? = null) = BankAccess(
|
||||||
user.bankCode, user.loginName, user.password,
|
bank.bankCode, bank.loginName, bank.password,
|
||||||
user.bankName, user.bic, user.customerName, user.userId,
|
info?.name ?: bank.bankName, bank.bic, bank.customerName, bank.userId,
|
||||||
user.accounts.map { mapAccount(it) },
|
bank.accounts.map { mapAccount(it) }.sortedBy { it.type }
|
||||||
|
.onEachIndexed { index, bankAccount -> bankAccount.displayIndex = index },
|
||||||
|
|
||||||
user.selectedTanMethod?.securityFunction?.code, user.tanMethods.map { mapTanMethod(it) },
|
bank.selectedTanMethod?.securityFunction?.code, bank.tanMethods.map { mapTanMethod(it) }.toMutableList(),
|
||||||
user.selectedTanMedium?.mediumName, user.tanMedia.map { mapTanMedium(it) },
|
bank.selectedTanMedium?.mediumName, bank.tanMedia.map { mapTanMedium(it) }.toMutableList(),
|
||||||
|
|
||||||
getBankingGroup(user.bankName, user.bic)
|
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? =
|
protected open fun getBankingGroup(bankName: String, bic: String): BankingGroup? =
|
||||||
bankingGroupMapper.getBankingGroup(bankName, bic)
|
bankingGroupMapper.getBankingGroup(bankName, bic)
|
||||||
|
|
||||||
|
|
||||||
protected open fun mapAccount(account: net.dankito.banking.client.model.BankAccount) = BankAccount(
|
protected open fun mapAccount(account: net.dankito.banking.client.model.BankAccount) = BankAccount(
|
||||||
account.identifier, account.accountHolderName, mapAccountType(account.type), account.iban, account.subAccountNumber,
|
account.identifier, account.subAccountNumber, account.iban, account.productName, account.accountHolderName,
|
||||||
account.productName, account.currency, account.accountLimit, account.isAccountTypeSupportedByApplication,
|
mapAccountType(account.type), account.currency, account.accountLimit,
|
||||||
mapFeatures(account),
|
account.isAccountTypeSupportedByApplication, mapFeatures(account),
|
||||||
mapAmount(account.balance), account.retrievedTransactionsFrom, account.retrievedTransactionsTo,
|
mapMoney(account.balance),
|
||||||
// TODO: map haveAllTransactionsBeenRetrieved
|
account.serverTransactionsRetentionDays,
|
||||||
countDaysForWhichTransactionsAreKept = account.countDaysForWhichTransactionsAreKept,
|
account.lastAccountUpdateTime, account.retrievedTransactionsFrom,
|
||||||
bookedTransactions = account.bookedTransactions.map { mapTransaction(it) }.toMutableList()
|
bookedTransactions = mapBookedTransactions(account).toMutableList(),
|
||||||
|
holdings = mapHoldings(account.statementOfHoldings, account.currency, account.lastAccountUpdateTime).toMutableList()
|
||||||
)
|
)
|
||||||
|
|
||||||
protected open fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType =
|
protected open fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType =
|
||||||
|
@ -104,36 +201,110 @@ open class FinTs4kMapper {
|
||||||
add(BankAccountFeatures.TransferMoney)
|
add(BankAccountFeatures.TransferMoney)
|
||||||
}
|
}
|
||||||
if (account.supportsInstantPayment) {
|
if (account.supportsInstantPayment) {
|
||||||
add(BankAccountFeatures.InstantPayment)
|
add(BankAccountFeatures.InstantTransfer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected open fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction(
|
protected open fun mapBookedTransactions(account: net.dankito.banking.client.model.BankAccount): List<AccountTransaction> =
|
||||||
mapAmount(transaction.amount), transaction.amount.currency.code, transaction.unparsedReference,
|
account.bookedTransactions.map { mapTransaction(it) }
|
||||||
transaction.bookingDate, transaction.valueDate,
|
|
||||||
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
|
|
||||||
transaction.bookingText, null,
|
|
||||||
transaction.statementNumber, transaction.sequenceNumber,
|
|
||||||
mapNullableAmount(transaction.openingBalance), mapNullableAmount(transaction.closingBalance),
|
|
||||||
|
|
||||||
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference,
|
protected open fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction(
|
||||||
|
mapMoney(transaction.amount), transaction.amount.currency.code, transaction.reference,
|
||||||
|
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.creditorIdentifier, transaction.originatorsIdentificationCode,
|
||||||
transaction.compensationAmount, transaction.originalAmount,
|
transaction.compensationAmount, transaction.originalAmount,
|
||||||
transaction.sepaReference,
|
|
||||||
transaction.deviantOriginator, transaction.deviantRecipient,
|
transaction.deviantOriginator, transaction.deviantRecipient,
|
||||||
transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement,
|
transaction.referenceWithNoSpecialType,
|
||||||
|
|
||||||
transaction.currencyType, transaction.bookingKey,
|
transaction.journalNumber, transaction.textKeyAddition,
|
||||||
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution,
|
|
||||||
transaction.supplementaryDetails,
|
|
||||||
|
|
||||||
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber
|
transaction.orderReferenceNumber, transaction.referenceNumber,
|
||||||
|
transaction.isReversal
|
||||||
)
|
)
|
||||||
|
|
||||||
protected open fun mapNullableAmount(amount: Money?) = amount?.let { mapAmount(it) }
|
protected open fun mapHoldings(statements: List<net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings>, accountCurrency: String, lastAccountUpdateTime: Instant? = null) =
|
||||||
|
statements.flatMap { mapHoldings(it, accountCurrency, lastAccountUpdateTime) }
|
||||||
|
|
||||||
protected open fun mapAmount(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.'))
|
protected open fun mapHoldings(statement: net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings, accountCurrency: String, lastAccountUpdateTime: Instant? = null): List<net.codinux.banking.client.model.securitiesaccount.Holding> {
|
||||||
|
|
||||||
|
val totalBalance = mapNullableAmount(statement.totalBalance)
|
||||||
|
val currency = statement.currency ?: accountCurrency
|
||||||
|
val statementDate: Instant? = /* statement.statementDate ?: statement.preparationDate ?: */ lastAccountUpdateTime // TODO
|
||||||
|
|
||||||
|
return statement.holdings.map { mapHolding(it, currency, statementDate, if (statement.holdings.size == 1) totalBalance else null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun mapHolding(holding: Holding, accountCurrency: String, statementDate: Instant?, totalBalance: Amount? = null) = net.codinux.banking.client.model.securitiesaccount.Holding(
|
||||||
|
holding.name, holding.isin, holding.wkn,
|
||||||
|
|
||||||
|
holding.quantity, holding.currency ?: accountCurrency,
|
||||||
|
|
||||||
|
getTotalBalance(holding), mapNullableAmount(holding.marketValue),
|
||||||
|
calculatePerformance(holding),
|
||||||
|
getTotalCostPrice(holding), mapNullableAmount(holding.averageCostPrice),
|
||||||
|
|
||||||
|
holding.pricingTime ?: statementDate, holding.buyingDate
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(',', '.'))
|
||||||
|
|
||||||
|
|
||||||
open fun mapTanChallenge(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallenge {
|
open fun mapTanChallenge(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallenge {
|
||||||
|
@ -141,35 +312,44 @@ open class FinTs4kMapper {
|
||||||
val action = mapActionRequiringTan(challenge.forAction)
|
val action = mapActionRequiringTan(challenge.forAction)
|
||||||
|
|
||||||
val tanMethods = challenge.bank.tanMethodsAvailableForUser.map { mapTanMethod(it) }
|
val tanMethods = challenge.bank.tanMethodsAvailableForUser.map { mapTanMethod(it) }
|
||||||
val selectedTanMethodId = challenge.tanMethod.securityFunction.code
|
val selectedTanMethodIdentifier = challenge.tanMethod.securityFunction.code
|
||||||
|
|
||||||
val tanMedia = challenge.bank.tanMedia.map { mapTanMedium(it) }
|
val tanMedia = challenge.bank.tanMedia.map { mapTanMedium(it) }
|
||||||
val selectedTanMediumName = challenge.bank.selectedTanMedium?.mediumName
|
// 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 user = mapToUserAccountViewInfo(challenge.bank)
|
val bank = mapToBankViewInfo(challenge.bank)
|
||||||
val account = challenge.account?.let { mapToBankAccountViewInfo(it) }
|
val account = challenge.account?.let { mapToBankAccountViewInfo(it) }
|
||||||
|
|
||||||
val tanImage = if (challenge is ImageTanChallenge) mapTanImage(challenge.image) else null
|
val tanImage = if (challenge is ImageTanChallenge) mapTanImage(challenge.image) else null
|
||||||
val flickerCode = if (challenge is FlickerCodeTanChallenge) mapFlickerCode(challenge.flickerCode) else null
|
val flickerCode = if (challenge is FlickerCodeTanChallenge) mapFlickerCode(challenge.flickerCode) else null
|
||||||
|
|
||||||
return TanChallenge(type, action, challenge.messageToShowToUser, selectedTanMethodId, tanMethods, selectedTanMediumName, tanMedia, tanImage, flickerCode, user, account)
|
return object : TanChallenge(type, action, challenge.messageToShowToUser, selectedTanMethodIdentifier, tanMethods, selectedTanMediumIdentifier, tanMedia, tanImage, flickerCode, bank, account, challenge.tanExpirationTime, challenge.challengeCreationTimestamp) {
|
||||||
|
override fun addTanExpiredCallback(callback: () -> Unit) {
|
||||||
|
challenge.addTanExpiredCallback(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) {
|
||||||
|
challenge.addUserApprovedDecoupledTanCallback(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun mapTanChallengeType(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallengeType = when {
|
protected open fun mapTanChallengeType(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallengeType = when {
|
||||||
challenge is ImageTanChallenge -> TanChallengeType.Image
|
challenge is ImageTanChallenge -> TanChallengeType.Image
|
||||||
challenge is FlickerCodeTanChallenge -> TanChallengeType.Flickercode
|
challenge is FlickerCodeTanChallenge -> TanChallengeType.FlickerCode
|
||||||
else -> TanChallengeType.EnterTan
|
else -> TanChallengeType.EnterTan
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun mapActionRequiringTan(action: net.codinux.banking.fints.model.ActionRequiringTan): ActionRequiringTan =
|
protected open fun mapActionRequiringTan(action: net.codinux.banking.fints.model.ActionRequiringTan): ActionRequiringTan =
|
||||||
ActionRequiringTan.valueOf(action.name)
|
ActionRequiringTan.valueOf(action.name)
|
||||||
|
|
||||||
protected open fun mapTanMethod(method: net.codinux.banking.fints.model.TanMethod): TanMethod = TanMethod(
|
open fun mapTanMethod(method: net.codinux.banking.fints.model.TanMethod): TanMethod = TanMethod(
|
||||||
method.displayName, mapTanMethodType(method.type), method.securityFunction.code, method.maxTanInputLength, mapAllowedTanFormat(method.allowedTanFormat)
|
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 =
|
protected open fun mapTanMethodType(type: net.codinux.banking.fints.model.TanMethodType): TanMethodType =
|
||||||
TanMethodType.valueOf(type.name)
|
TanMethodType.valueOf(TanMethodTypesToMigrate[type.name] ?: type.name)
|
||||||
|
|
||||||
protected open fun mapAllowedTanFormat(allowedTanFormat: net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat?): AllowedTanFormat =
|
protected open fun mapAllowedTanFormat(allowedTanFormat: net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat?): AllowedTanFormat =
|
||||||
allowedTanFormat?.let { AllowedTanFormat.valueOf(it.name) } ?: AllowedTanFormat.Alphanumeric
|
allowedTanFormat?.let { AllowedTanFormat.valueOf(it.name) } ?: AllowedTanFormat.Alphanumeric
|
||||||
|
@ -178,14 +358,18 @@ open class FinTs4kMapper {
|
||||||
TanImage(image.mimeType, mapToBase64(image.imageBytes), mapException(image.decodingError))
|
TanImage(image.mimeType, mapToBase64(image.imageBytes), mapException(image.decodingError))
|
||||||
|
|
||||||
@OptIn(ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
protected open fun mapToBase64(bytes: ByteArray): String {
|
protected open fun mapToBase64(bytes: ByteArray?): String? {
|
||||||
|
if (bytes == null || bytes.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return Base64.Default.encode(bytes)
|
return Base64.Default.encode(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun mapTanMedium(tanMedium: TanMedium) = net.codinux.banking.client.model.tan.TanMedium(
|
protected open fun mapTanMedium(tanMedium: TanMedium) = net.codinux.banking.client.model.tan.TanMedium(
|
||||||
mapTanMediumType(tanMedium), tanMedium.mediumName, mapTanMediumStatus(tanMedium.status),
|
mapTanMediumType(tanMedium), tanMedium.mediumName, mapTanMediumStatus(tanMedium.status),
|
||||||
(tanMedium as? TanGeneratorTanMedium)?.let { mapTanGeneratorTanMedium(it) },
|
tanMedium.tanGenerator?.let { mapTanGeneratorTanMedium(it) },
|
||||||
(tanMedium as? MobilePhoneTanMedium)?.let { mapMobilePhoneTanMedium(it) }
|
tanMedium.mobilePhone?.let { mapMobilePhoneTanMedium(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
protected open fun mapTanMediumStatus(status: TanMediumStatus): net.codinux.banking.client.model.tan.TanMediumStatus = when (status) {
|
protected open fun mapTanMediumStatus(status: TanMediumStatus): net.codinux.banking.client.model.tan.TanMediumStatus = when (status) {
|
||||||
|
@ -204,9 +388,9 @@ open class FinTs4kMapper {
|
||||||
tanMedium.validFrom, tanMedium.validTo
|
tanMedium.validFrom, tanMedium.validTo
|
||||||
)
|
)
|
||||||
|
|
||||||
protected open fun mapTanMediumType(tanMedium: TanMedium): TanMediumType = when {
|
protected open fun mapTanMediumType(tanMedium: TanMedium): TanMediumType = when (tanMedium.mediumClass) {
|
||||||
tanMedium is MobilePhoneTanMedium -> TanMediumType.MobilePhone
|
TanMediumKlasse.MobiltelefonMitMobileTan -> TanMediumType.MobilePhone
|
||||||
tanMedium is TanGeneratorTanMedium -> TanMediumType.TanGenerator
|
TanMediumKlasse.TanGenerator -> TanMediumType.TanGenerator
|
||||||
else -> TanMediumType.Generic
|
else -> TanMediumType.Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,16 +398,81 @@ open class FinTs4kMapper {
|
||||||
FlickerCode(flickerCode.challengeHHD_UC, flickerCode.parsedDataSet, mapException(flickerCode.decodingError))
|
FlickerCode(flickerCode.challengeHHD_UC, flickerCode.parsedDataSet, mapException(flickerCode.decodingError))
|
||||||
|
|
||||||
|
|
||||||
protected open fun <T> mapError(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<T> {
|
/* Transfer Money */
|
||||||
return if (response.error != null) {
|
|
||||||
Response.error(ErrorType.valueOf(response.error!!.name), if (response.error == ErrorCode.BankReturnedError) null else response.errorMessage,
|
open fun mapToTransferMoneyParameter(request: TransferMoneyRequestForUser): TransferMoneyParameter = TransferMoneyParameter(
|
||||||
if (response.error == ErrorCode.BankReturnedError && response.errorMessage !== null) listOf(response.errorMessage!!) else emptyList())
|
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 {
|
} else {
|
||||||
Response.error(ErrorType.UnknownError, response.errorMessage)
|
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) {
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
Response.error(ErrorType.UnknownError, response.errorMessage, messageLog = messageLog)
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun mapException(exception: Exception?): String? =
|
protected open fun mapException(exception: Exception?): String? =
|
||||||
exception?.stackTraceToString()
|
exception?.stackTraceToString()
|
||||||
|
|
||||||
|
|
||||||
|
open fun mapCommonResponseData(bank: BankAccess, response: FinTsClientResponse) {
|
||||||
|
response.finTsModel?.let {
|
||||||
|
bank.clientData = it
|
||||||
|
}
|
||||||
|
|
||||||
|
response.serializedFinTsModel?.let {
|
||||||
|
bank.serializedClientData = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
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"
|
||||||
|
)
|
|
@ -3,11 +3,10 @@ package net.codinux.banking.client.fints4k
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.codinux.banking.client.SimpleBankingClientCallback
|
import net.codinux.banking.client.SimpleBankingClientCallback
|
||||||
import net.codinux.banking.client.model.response.ResponseType
|
import net.codinux.banking.client.model.response.ResponseType
|
||||||
import kotlin.test.Test
|
import net.codinux.banking.client.model.tan.EnterTanResult
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.*
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
|
@Ignore
|
||||||
class FinTs4kBankingClientTest {
|
class FinTs4kBankingClientTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -19,8 +18,11 @@ class FinTs4kBankingClientTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val underTest = FinTs4KBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
|
private val underTest = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
|
||||||
|
println("WARN: TAN is required to execute test: ${tanChallenge.messageToShowToUser}")
|
||||||
|
|
||||||
|
val enteredTan: String? = null
|
||||||
|
callback(EnterTanResult(enteredTan))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package net.codinux.banking.client.fints4k
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.Amount
|
||||||
|
import kotlin.test.Test
|
||||||
|
import net.codinux.banking.fints.transactions.swift.model.Holding
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class FinTs4kMapperTest {
|
||||||
|
|
||||||
|
private val underTest = FinTs4kMapper()
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getTotalBalance_TotalBalanceIsNull_CalculateByQuantityAndMarketValue() {
|
||||||
|
val holding = Holding("", null, null, null, 4.0, null, null, null, fints4kAmount("13.33"))
|
||||||
|
|
||||||
|
val result = underTest.getTotalBalance(holding)
|
||||||
|
|
||||||
|
assertEquals(Amount("53.32"), result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getTotalCostPrice_TotalCostPriceIsNull_CalculateByQuantityAndAverageCostPrice() {
|
||||||
|
val holding = Holding("", null, null, null, 47.0, fints4kAmount("16.828"), null)
|
||||||
|
|
||||||
|
val result = underTest.getTotalCostPrice(holding)
|
||||||
|
|
||||||
|
assertEquals(Amount("790.9159999999999"), result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun calculatePerformance_ByTotalBalanceAndTotalCostPrice() {
|
||||||
|
val holding = Holding("", null, null, null, null, null, fints4kAmount("20217.12"), null, null, null, fints4kAmount("19027.04"))
|
||||||
|
|
||||||
|
val result = underTest.calculatePerformance(holding)
|
||||||
|
|
||||||
|
assertEquals(6.2546773f, result!!, 0.000001f) // for JS the result has too many decimal places, so add a tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun calculatePerformance_ByMarketValueAndAverageCostPrice() {
|
||||||
|
val holding = Holding("", null, null, null, null, fints4kAmount("16.828"), null, null, fints4kAmount("16.75"), null, null)
|
||||||
|
|
||||||
|
val result = underTest.calculatePerformance(holding)
|
||||||
|
|
||||||
|
assertEquals(-0.4635132f, result!!, 0.0000001f) // for JS the result has too many decimal places, so add a tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun fints4kAmount(amount: String) =
|
||||||
|
net.codinux.banking.fints.model.Amount(amount)
|
||||||
|
|
||||||
|
}
|
111
README.md
111
README.md
|
@ -1,4 +1,5 @@
|
||||||
# Banking Client
|
# Banking Client
|
||||||
|
[](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).
|
Library to abstract over different banking client implementations like [fints4k](https://git.dankito.net/codinux/fints4k).
|
||||||
|
|
||||||
|
@ -11,13 +12,8 @@ not each project has the implement to model again.
|
||||||
### Gradle:
|
### Gradle:
|
||||||
|
|
||||||
```
|
```
|
||||||
plugins {
|
|
||||||
kotlin("jvm") version "2.0.10" // or kotlin("multiplatform"), depending on your requirements
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
// other repositories like mavenCentral(), ...
|
||||||
maven {
|
maven {
|
||||||
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
|
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
|
||||||
}
|
}
|
||||||
|
@ -25,7 +21,7 @@ repositories {
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("net.codinux.banking.client:fints4k-banking-client:0.5.0")
|
implementation("net.codinux.banking.client:fints4k-banking-client:0.7.2")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -51,11 +47,11 @@ class ShowUsage {
|
||||||
|
|
||||||
|
|
||||||
fun getAccountData() {
|
fun getAccountData() {
|
||||||
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
|
val client = FinTs4kBankingClient(SimpleBankingClientCallback())
|
||||||
|
|
||||||
val response = client.getAccountData()
|
val response = client.getAccountData(bankCode, loginName, password) // that's all
|
||||||
|
|
||||||
printReceivedData(response)
|
printReceivedData(response) // now print retrieved data (save it to database, display it in UI, ...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -82,16 +78,10 @@ class ShowUsage {
|
||||||
|
|
||||||
This fetches the booked account transactions of the last 90 days. In most cases no TAN is required for this.
|
This fetches the booked account transactions of the last 90 days. In most cases no TAN is required for this.
|
||||||
|
|
||||||
In case there is, add TAN handling in Client Callback:
|
|
||||||
|
|
||||||
```kotlin
|
#### GetAccountData parameter
|
||||||
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
|
|
||||||
val tan: String? = null // if a TAN is required, add a UI or ...
|
|
||||||
callback.invoke(EnterTanResult(tan)) // ... set a break point here, get TAN e.g. from your TAN app, set tan variable in debugger view and resume debugger
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also specify options e.g. which transactions should be retrieved:
|
You can also specify options e.g. which transactions you would like to retrieve:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val options = GetAccountDataOptions(
|
val options = GetAccountDataOptions(
|
||||||
|
@ -104,10 +94,91 @@ val options = GetAccountDataOptions(
|
||||||
val response = client.getAccountData(options)
|
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`:
|
Add some error handling by checking `response.error`:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
response.error?.let{ error ->
|
response.error?.let{ error ->
|
||||||
println("Could not fetch account data: ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
|
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.
|
|
@ -12,5 +12,5 @@ repositories {
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("net.codinux.banking.client:fints4k-banking-client:0.5.0")
|
implementation(project(":fints4k-banking-client"))
|
||||||
}
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
package net.codinux.banking.client.fints4k.example
|
package net.codinux.banking.client.fints4k.example
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import net.codinux.banking.client.SimpleBankingClientCallback
|
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.fints4k.FinTs4kBankingClientForUser
|
||||||
import net.codinux.banking.client.getAccountData
|
import net.codinux.banking.client.getAccountData
|
||||||
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
@ -9,6 +13,9 @@ import net.codinux.banking.client.model.options.RetrieveTransactions
|
||||||
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
||||||
import net.codinux.banking.client.model.response.Response
|
import net.codinux.banking.client.model.response.Response
|
||||||
import net.codinux.banking.client.model.tan.EnterTanResult
|
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() {
|
fun main() {
|
||||||
val showUsage = ShowUsage()
|
val showUsage = ShowUsage()
|
||||||
|
@ -27,17 +34,24 @@ class ShowUsage {
|
||||||
|
|
||||||
|
|
||||||
fun getAccountDataSimpleExample() {
|
fun getAccountDataSimpleExample() {
|
||||||
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
|
val client = FinTs4kBankingClient(SimpleBankingClientCallback())
|
||||||
|
|
||||||
val response = client.getAccountData()
|
val response = client.getAccountData(bankCode, loginName, password) // that's all
|
||||||
|
|
||||||
printReceivedData(response)
|
printReceivedData(response) // now print retrieved data (save it to database, display it in UI, ...)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAccountDataFullExample() {
|
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 ->
|
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
|
||||||
val tan: String? = null // if a TAN is required, add a UI or ...
|
println("A TAN is required for ${tanChallenge.forAction}. Selected TAN method is '${tanChallenge.selectedTanMethod.displayName}'. Messsage of your credit institute:")
|
||||||
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
|
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(
|
val options = GetAccountDataOptions(
|
||||||
|
@ -50,21 +64,46 @@ class ShowUsage {
|
||||||
val response = client.getAccountData(options)
|
val response = client.getAccountData(options)
|
||||||
|
|
||||||
response.error?.let{ error ->
|
response.error?.let{ error ->
|
||||||
println("Could not fetch account data: ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
|
println("Could not fetch account data: ${error.type} ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
printReceivedData(response)
|
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>) {
|
private fun printReceivedData(response: Response<GetAccountDataResponse>) {
|
||||||
response.data?.let { data ->
|
response.data?.let { data ->
|
||||||
val user = data.user
|
val bank = data.bank
|
||||||
println("Kunde: ${user.customerName} ${user.accounts.size} Konten @ ${user.bic} ${user.bankName}")
|
println("Kunde: ${bank.customerName} ${bank.accounts.size} Konten @ ${bank.bic} ${bank.bankName}")
|
||||||
|
|
||||||
println()
|
println()
|
||||||
println("Konten:")
|
println("Konten:")
|
||||||
user.accounts.sortedBy { it.type }.forEach { account ->
|
bank.accounts.sortedBy { it.type }.forEach { account ->
|
||||||
println("${account.identifier} ${account.productName} ${account.balance} ${account.currency}")
|
println("${account.identifier} ${account.productName} ${account.balance} ${account.currency}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ buildscript {
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "net.codinux.banking.client"
|
group = "net.codinux.banking.client"
|
||||||
version = "0.5.1"
|
version = "0.7.2"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@ -23,7 +23,7 @@ allprojects {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ext["sourceCodeRepositoryBaseUrl"] = "github.com/codinux/BankingClient"
|
ext["sourceCodeRepositoryBaseUrl"] = "git.dankito.net/codinux/BankingClient"
|
||||||
|
|
||||||
ext["projectDescription"] = "Model and base definitions for Banking Client, a common abstraction for different implements of banking libraries"
|
ext["projectDescription"] = "Model and base definitions for Banking Client, a common abstraction for different implements of banking libraries"
|
||||||
}
|
}
|
||||||
|
|
95
docs/Vokabular/BankAccess.md
Normal file
95
docs/Vokabular/BankAccess.md
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
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.
|
18
docs/Vokabular/BankCode.md
Normal file
18
docs/Vokabular/BankCode.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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.
|
|
@ -1,8 +1,14 @@
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
||||||
|
#org.gradle.parallel=true
|
||||||
|
|
||||||
kotlinVersion=2.0.10
|
|
||||||
|
kotlinVersion=1.9.25
|
||||||
|
|
||||||
kotlinxDateTimeVersion=0.5.0
|
kotlinxDateTimeVersion=0.5.0
|
||||||
jsJodaTimeZoneVersion=2.3.0
|
jsJodaTimeZoneVersion=2.3.0
|
||||||
|
# Coroutines 1.9 (currently RC) requires Kotlin 2.0
|
||||||
coroutinesVersion=1.8.1
|
coroutinesVersion=1.8.1
|
||||||
|
|
||||||
|
# 0.3.10 uses Kotlin 2.0.0
|
||||||
|
ionspinBigNumVersion=0.3.9
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit bdf8b14738c06016a48e1fc9781ad4d999e1219f
|
Subproject commit 88f1b01167e6a34b5b91f8797845bca0b7e4d3ab
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue