Compare commits

..

125 commits
v0.5.1 ... main

Author SHA1 Message Date
736ed22a51 Updated version in README 2024-10-20 11:07:29 +02:00
fbd6f8a6e5 Bumped verion to 0.7.2 2024-10-20 11:05:38 +02:00
d00f71bedd Fixed mapping new TanMethodType names 2024-10-19 21:37:39 +02:00
68e1408b81 Bumped verion to 0.7.2-SNAPSHOT 2024-10-19 21:28:37 +02:00
98260fbfbf Bumped verion to 0.7.1 2024-10-19 21:27:26 +02:00
52db7be2f6 Printing warning if TAN is required 2024-10-16 20:06:49 +02:00
2b0f9fcc71 Fixed that quantity is now of type Double 2024-10-16 20:06:08 +02:00
5d8079cfd4 Fixed unnecessary package declarations and safe calls 2024-10-16 20:05:42 +02:00
f8dd296b47 Implemented setting preferredTanMethods for updateAccountTransactionsAsync() and transferMoneyAsync() 2024-10-16 20:04:59 +02:00
d1cc7b0eb0 Simplified publishing artifacts to codinux repo 2024-10-16 20:03:32 +02:00
5d127828cb Added preferredTanMethods to constructor overload 2024-10-16 15:34:54 +02:00
803e44118c Extracted TanMethodsPreferredByMostApplications 2024-10-16 15:33:26 +02:00
12304684fc Ranking non-ChipTan methods before ChipTan methods (but should make no difference as there shouldn't be any bank supporting both) 2024-10-16 15:19:06 +02:00
7448b7e94c Added NonVisualOrImageBasedOrFlickerCode for applications supporting FlickerCode 2024-10-16 15:10:53 +02:00
0e26a19d13 Renamed Manuell (German) to Manual (English) 2024-10-16 15:06:01 +02:00
647f848d5f Renamed Flickercode (German) to FlickerCode (English) 2024-10-16 15:04:55 +02:00
ce39c1cf7d Extracted NonVisualWithoutChipTanManuell 2024-10-16 14:48:46 +02:00
f1981dc5f0 Bumped version to 0.7.1-SNAPSHOT 2024-10-16 01:54:01 +02:00
dbccc93981 Bumped verion to 0.7.0 2024-10-16 01:46:08 +02:00
2245806d12 Updated fints4k version to 1.0.0-Alpha-15 2024-10-16 01:40:10 +02:00
7d90910ffd Converted clientData to Any so that don't have to deserialize client data each time and added serializedClientData 2024-10-15 19:57:27 +02:00
a5e809ff68 Updated to new fints4k model that now contains serializedFinTsModel 2024-10-15 13:50:13 +02:00
ef1177c76f Added BankAccess, BankAccount, messageNumber, messageType and messageWithoutSensitiveData to MessageLogEntry 2024-10-15 12:58:28 +02:00
a33f31df02 Updated to that quantity now is of type Double 2024-10-15 11:01:25 +02:00
3940b1e77f Moved messageLog to Response 2024-10-15 10:56:56 +02:00
67ea188182 Added bank and account to MessageLogEntry 2024-10-15 03:43:35 +02:00
c443656c03 Passing clientData to fints4k client 2024-10-15 03:22:37 +02:00
ee21f684eb Added constructor for Bank and BankAccount 2024-10-15 03:14:21 +02:00
933c761a0d Added messageLog to GetAccountDataResponse and TransferMoneyResponse 2024-10-15 03:06:59 +02:00
0b6490f501 Added clientData 2024-10-15 01:31:46 +02:00
e915b4479f Updated to new fints4k data model, that TanGeneratorTanMedium and MobilePhoneTanMedium are now derived anymore from TanMedium 2024-10-10 18:54:54 +02:00
bbd40d7017 Fixed typo 2024-10-04 09:08:56 +02:00
3f3c480e1c Fixed that for RetrieveTransactions hasn't been checked 2024-10-04 09:08:48 +02:00
c0e9db7234 Updating balance 2024-10-04 06:33:13 +02:00
786b849c8f Fixed checking if retrievedTransactionsFrom has been updated 2024-10-04 06:33:03 +02:00
7d715cc3b8 Added convenience properties for displayed reference and otherPartyName 2024-09-26 14:24:39 +02:00
2164bc5a94 Fixed getting fints TanMethod in existing TanMethods 2024-09-26 14:23:42 +02:00
00b26e3bd0 Made parsedDataSet, mimeType and imageBytesBase64 nullable, as in case of decoding error they are not set 2024-09-26 14:23:04 +02:00
0bf5b19f4a Updated fints4k version to 1.0.0-Alpha-15-SNAPSHOT 2024-09-26 10:38:28 +02:00
cf06bffd1b Bumped version to 0.6.2-SNAPSHOT 2024-09-19 21:44:24 +02:00
43031b24ad Bumped version to 0.6.1 2024-09-19 21:43:24 +02:00
6e5c28f47c Configured parallel Gradle build, but disabled by default as releasing with this option won't work 2024-09-19 21:30:06 +02:00
86866bad32 Downgraded Kotlin to version 1.9.x so that more applications can use this library 2024-09-19 21:27:25 +02:00
a21861178b Updated fints4k version to 1.0.0-Alpha-14 2024-09-19 21:26:47 +02:00
234738465c Added Maven Central badge 2024-09-18 17:51:32 +02:00
f160c50d18 Added fints4k option appendFinTsMessagesToLog to easily configure if FinTS messages should get added to log 2024-09-18 17:40:34 +02:00
9aeca393dc Updated fints4k to version 1.0.0-Alpha-14-SNAPSHOT 2024-09-18 17:34:08 +02:00
5964fc611c Added properties to sort accounts, tanMethods and tanMedia 2024-09-18 02:13:35 +02:00
ac3bc57875 Changed order 2024-09-18 02:13:00 +02:00
40ad61ebab Fixed mapping 2024-09-18 02:12:44 +02:00
27e191cba6 Bumped version to 0.6.1-SNAPSHOT 2024-09-17 18:02:18 +02:00
cc1a60cb61 Bumped version to 0.6.0 2024-09-17 17:52:52 +02:00
145686b453 Updated to new fints4k version 2024-09-17 17:52:52 +02:00
44a875eeec Added errorType to error output 2024-09-17 17:26:35 +02:00
62fe93e88e Added example for reading TAN from command line 2024-09-17 17:23:30 +02:00
2853087836 Applied renaming selectedTanMethodId to selectedTanMethodIdentifier and selectedTanMediumName to selectedTanMediumIdentifier also to TanChallenge 2024-09-17 17:18:27 +02:00
4fbb0425e6 Added add() methods for list properties 2024-09-17 15:29:33 +02:00
9af8d0eb1d Split AccountTransaction.userSetDisplayName into userSetReference and userSetOtherPartyName, and added userSetDisplayName to TanMedium, TanMethod and Holding 2024-09-17 15:29:03 +02:00
045774ff3f Made collections val MutableList to be overridable with a derived collection class 2024-09-17 00:28:43 +02:00
400c13d6a2 Added countryCode 2024-09-16 17:30:49 +02:00
62482fb5a3 Renamed bankCode to domesticBankCode 2024-09-16 17:22:46 +02:00
68317f3b1c Made changeable properties var and using List instead of MutableList for transactions 2024-09-16 17:16:41 +02:00
72608a444d Renamed User to BankAccess 2024-09-16 16:33:08 +02:00
89e21dc38a Made BIC nullable 2024-09-16 16:14:52 +02:00
8f822e9469 Made properties overridable and variable 2024-09-12 13:57:07 +02:00
a6b2ed8729 Added identifier 2024-09-12 13:17:01 +02:00
83dfd41784 Added Amount.isNegative and Collection<Amount>.sum() extension methods 2024-09-12 03:58:22 +02:00
52af60077a Fixed Amount for wasmJs 2024-09-12 03:56:34 +02:00
bf211e238f Fixed Amount for Apple platforms 2024-09-12 03:47:05 +02:00
fd7a3bc747 Added arithmetic operations to Amount 2024-09-12 03:30:00 +02:00
825dc7c8b9 Copied referenced methods over from fints4k so i could make fints4k dependency an implementation detail 2024-09-11 23:59:20 +02:00
54940742f7 Renamed lastTransactionsRetrievalTime to lastAccountUpdateTime 2024-09-11 23:21:06 +02:00
5187e34797 Implemented mapping Holdings 2024-09-11 22:47:13 +02:00
c5b7967ce1 Added tanMethodsNotSupportedByApplication to filter out TanMethods not supported by application (by default: ChipTanUsb) 2024-09-10 03:26:39 +02:00
99c864bcf1 Belongs to set default bank values, IntelliJ didn't show me these files in commit dialog 2024-09-10 01:24:13 +02:00
97604d59c9 Implemented changing TAN method (but better find instance in FinTsData instead of creating a new instance) 2024-09-10 01:21:33 +02:00
aa7b7afaf0 Passing known bank data on to FinTsClient as e.g. bank names returned from bank server are often quite bad, e.g. DB24 for Deutsche Bank 2024-09-09 23:01:57 +02:00
73b760ae68 Updated tanExpirationTime's new data type 2024-09-09 03:40:49 +02:00
1f8fecca7c Passing fints4k's new addTanExpiredCallback on to BankingClient 2024-09-09 03:40:25 +02:00
384ab2fd9e Renamed finTsServerAddress to serverAddress and made it nullable 2024-09-09 00:37:54 +02:00
432e5016a9 Added JsonIgnore 2024-09-09 00:36:34 +02:00
705740a0d0 Disabled test as it's not a real test 2024-09-08 22:58:52 +02:00
d8499b4ce2 Updated to new fints4k model 2024-09-08 22:58:28 +02:00
8f5024b169 Added tanExpirationTime and challengeCreationTimestamp 2024-09-08 22:50:07 +02:00
4298fc9e40 Added isNumericTan 2024-09-08 22:46:47 +02:00
4acb58b571 Added finTsServerAddress 2024-09-08 22:44:08 +02:00
95b37f0cb8 Added section comment 2024-09-08 22:27:01 +02:00
2c2db147b4 Renamed otherPartyBankCode to otherPartyBankId and made reference nullable 2024-09-08 22:09:59 +02:00
e6aec071a2 Using the default values from GetAccountDataOptions. Fixes that otherwise fints4k uses other preferred TAN methods than BankingClient 2024-09-08 21:56:51 +02:00
376cb08a9f Renamed transactionsRetentionDays to serverTransactionsRetentionDays 2024-09-08 21:55:31 +02:00
288af22ac6 Added AccountTransaction documentation from fints4k 2024-09-05 23:37:25 +02:00
713bfa4b50 Matched AccountTransaction property names that ones from fints4k 2024-09-05 23:26:40 +02:00
adf8cfa750 Added convenience methods for features 2024-09-05 23:11:52 +02:00
783675d82a Extracted constant DefaultCurrency 2024-09-05 23:07:59 +02:00
a680b6534c Implemented setting account's displayIndex 2024-09-05 23:03:11 +02:00
cb45c181ae Renamed UserAccount to User 2024-09-05 23:00:17 +02:00
c35026bfcc Implemented transferMoneyAsync() 2024-09-05 22:54:32 +02:00
675066c216 Added convenience constructor 2024-09-05 22:43:37 +02:00
d48b708a97 Renamed InstantPayment to InstantTransfer 2024-09-05 22:43:14 +02:00
4153afa814 Fixed typo 2024-09-05 22:41:59 +02:00
ccf38e2c07 Added clarification about the userId and made it nullable 2024-09-05 22:41:20 +02:00
a6caa40267 Fixed using identifier instead of non-unique medium name and suffixed selected properties with -Identifier 2024-09-05 22:31:49 +02:00
55767f88e4 Changed property order a bit 2024-09-05 22:28:03 +02:00
4c860e7b20 Changed order of BankAccount properties 2024-09-05 22:25:30 +02:00
9512db3402 Renamed unbooked- to prebookedTransactions 2024-09-05 22:14:01 +02:00
7353b0347e Renamed countDaysForWhichTransactionsAreKeptOnBankServer to transactionsRetentionDays 2024-09-05 22:06:06 +02:00
1e5c83c369 Renamed haveAllTransactionsBeenRetrieved to haveAllRetainedTransactionsBeenRetrieved and made it evaluating countDaysForWhichTransactionsAreKeptOnBankServer and retrievedTransactionsFrom instead of being set to a fixed value 2024-09-05 22:05:07 +02:00
bd052587c5 Added example how to update account transactions 2024-09-03 22:53:26 +02:00
36391c8f20 Also updating retrievedTransactionsFrom on BankAccount 2024-09-03 22:31:05 +02:00
7f60e02340 Implemented BankingModelService to filter out duplicates in retrieved AccountTransactions 2024-09-03 22:30:52 +02:00
85b66aa040 Also implemented an identifier for AccountTransaction 2024-09-03 22:23:52 +02:00
a9ebad0793 Fixed identifying TAN media as e.g. Sparkasse named all their TanGenerator media "SparkassenCard (Debitkarte)" - so it was not possible to differentiate between them 2024-09-03 22:23:27 +02:00
5bfe492ada Moved balance up 2024-09-03 22:18:32 +02:00
f17c9bb781 Could remove unused date time extensions 2024-09-03 21:35:50 +02:00
469ee275c9 Updated to fints4k changes: lastTransactionRetrievalTime has been renamed to lastTransactionsRetrievalTime and its type has been changed to Instant 2024-09-03 21:35:26 +02:00
ba662fda15 Fixed using fints4k-banking-client directly 2024-09-03 20:39:01 +02:00
80656bdcdd Fixed name 2024-09-03 20:37:54 +02:00
000a169a00 Added updateAccountTransactions() to BankingClient 2024-09-03 02:17:22 +02:00
9a2bc6b430 Also added displayName for UserAccount and both respect now userSetDisplayName 2024-09-03 02:13:02 +02:00
c3c0b2830e Added displayName to be able to replace shortcut productName ?: identifier in application 2024-09-03 01:30:01 +02:00
aab562ccf4 Updated to that fints4k replaced retrievedTransactionsTo with lastTransactionRetrievalTime 2024-09-03 01:08:04 +02:00
b210c1e7fa Made properties open 2024-09-02 19:40:18 +02:00
b0b0fa6140 Fixed mapping unparsedReference / sepaReference to reference 2024-09-02 19:39:45 +02:00
f1dad3bc26 Mapped fints4k Decoupled TAN model 2024-09-02 19:38:37 +02:00
62276e2a02 Bumped version to 0.5.2-SNAPSHOT 2024-09-01 20:20:25 +02:00
62 changed files with 2214 additions and 742 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

@ -1,4 +1,5 @@
# Banking Client # Banking Client
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/net.codinux.banking.client/banking-client/badge.svg?style=plastic)](https://maven-badges.herokuapp.com/maven-central/net.codinux.banking.client/banking-client)
Library to abstract over different banking client implementations like [fints4k](https://git.dankito.net/codinux/fints4k). 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.

View file

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

View file

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

View file

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

View 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.

View 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.

View file

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