Compare commits

..

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

32 changed files with 565 additions and 535 deletions

View file

@ -8,7 +8,6 @@ import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.GetTransactionsResponse 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.response.TransferMoneyResponse
import net.codinux.banking.client.model.tan.TanMethodType
interface BankingClient { interface BankingClient {
@ -42,10 +41,7 @@ interface BankingClient {
* *
* Optionally specify which [accounts] should be updated. If not specified all accounts will be updated. * Optionally specify which [accounts] should be updated. If not specified all accounts will be updated.
*/ */
suspend fun updateAccountTransactionsAsync( suspend fun updateAccountTransactionsAsync(bank: BankAccess, accounts: List<BankAccount>? = null): Response<List<GetTransactionsResponse>>
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, suspend fun transferMoneyAsync(bankCode: String, loginName: String, password: String, recipientName: String,

View file

@ -9,7 +9,6 @@ import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.GetTransactionsResponse 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.response.TransferMoneyResponse
import net.codinux.banking.client.model.tan.TanMethodType
interface BankingClientForUser { interface BankingClientForUser {
@ -41,10 +40,7 @@ interface BankingClientForUser {
* Updates account's transactions beginning from [BankAccount.lastAccountUpdateTime]. * Updates account's transactions beginning from [BankAccount.lastAccountUpdateTime].
* This may requires TAN if [BankAccount.lastAccountUpdateTime] is older than 90 days. * This may requires TAN if [BankAccount.lastAccountUpdateTime] is older than 90 days.
*/ */
suspend fun updateAccountTransactionsAsync( suspend fun updateAccountTransactionsAsync(accounts: List<BankAccount>? = null): Response<List<GetTransactionsResponse>>
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(recipientName: String, recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null): Response<TransferMoneyResponse>

View file

@ -10,7 +10,6 @@ import net.codinux.banking.client.model.request.TransferMoneyRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.response.GetTransactionsResponse 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.tan.TanMethodType
abstract class BankingClientForUserBase( abstract class BankingClientForUserBase(
protected val credentials: AccountCredentials, protected val credentials: AccountCredentials,
@ -26,7 +25,7 @@ abstract class BankingClientForUserBase(
} }
} }
override suspend fun updateAccountTransactionsAsync(accounts: List<BankAccount>?, preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>?): Response<List<GetTransactionsResponse>> = override suspend fun updateAccountTransactionsAsync(accounts: List<BankAccount>?): Response<List<GetTransactionsResponse>> =
client.updateAccountTransactionsAsync(bank, accounts) client.updateAccountTransactionsAsync(bank, accounts)

View file

@ -8,7 +8,6 @@ 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.TransferMoneyRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.tan.TanMethodType
/* BankingClient */ /* BankingClient */
@ -20,8 +19,8 @@ fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
getAccountDataAsync(request) getAccountDataAsync(request)
} }
fun BankingClient.updateAccountTransactions(bank: BankAccess, accounts: List<BankAccount>? = null, preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications) = runBlocking { fun BankingClient.updateAccountTransactions(bank: BankAccess, accounts: List<BankAccount>? = null) = runBlocking {
updateAccountTransactionsAsync(bank, accounts, preferredTanMethodsIfSelectedTanMethodIsNotAvailable) updateAccountTransactionsAsync(bank, accounts)
} }
@ -45,8 +44,8 @@ fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlo
getAccountDataAsync(options) getAccountDataAsync(options)
} }
fun BankingClientForUser.updateAccountTransactions(accounts: List<BankAccount>? = null, preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications) = runBlocking { fun BankingClientForUser.updateAccountTransactions() = runBlocking {
updateAccountTransactionsAsync(accounts, preferredTanMethodsIfSelectedTanMethodIsNotAvailable) updateAccountTransactionsAsync()
} }

View file

@ -1,10 +1,8 @@
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, // TODO: a string is really bad in UI, find a better solution val amount: Amount = Amount.Zero, // TODO: a string is really bad in UI, find a better solution
@ -136,18 +134,5 @@ open class AccountTransaction(
"$amount $currency $bookingDate $valueDate $reference $otherPartyName $otherPartyBankId $otherPartyAccountId" "$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

@ -70,49 +70,12 @@ open class BankAccess(
var wrongCredentialsEntered: Boolean = false 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 @get:JsonIgnore
open val displayName: String open val displayName: String
get() = userSetDisplayName ?: bankName 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 @get:JsonIgnore
val selectedTanMethod: TanMethod val selectedTanMethod: TanMethod
get() = tanMethods.first { it.identifier == selectedTanMethodIdentifier } get() = tanMethods.first { it.identifier == selectedTanMethodIdentifier }

View file

@ -53,11 +53,11 @@ open class BankAccount(
} }
@get:JsonIgnore @get:JsonIgnore
open val supportsBalanceRetrieval: Boolean open val supportsTransactionRetrieval: Boolean
get() = supportsAnyFeature(BankAccountFeatures.RetrieveBalance) get() = supportsAnyFeature(BankAccountFeatures.RetrieveBalance)
@get:JsonIgnore @get:JsonIgnore
open val supportsTransactionRetrieval: Boolean open val supportsBalanceRetrieval: Boolean
get() = supportsAnyFeature(BankAccountFeatures.RetrieveTransactions) get() = supportsAnyFeature(BankAccountFeatures.RetrieveTransactions)
@get:JsonIgnore @get:JsonIgnore

View file

@ -6,18 +6,7 @@ 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 messageWithoutSensitiveData: String? = null, open val messageTrace: 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

@ -27,14 +27,14 @@ 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.TanMethodsPreferredByMostApplications, val preferredTanMethods: List<TanMethodType>? = TanMethodType.NonVisualOrImageBased,
val tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications, val tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications,

View file

@ -64,19 +64,16 @@ open class TransferMoneyRequest(
* 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.TanMethodsPreferredByMostApplications, val preferredTanMethods: List<TanMethodType>? = TanMethodType.NonVisualOrImageBased,
val tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications, val tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications
val clientData: Any? = null,
var serializedClientData: String? = null
) { ) {
override fun toString() = "$amount to $recipientName - $paymentReference" override fun toString() = "$amount to $recipientName - $paymentReference"
} }

View file

@ -1,6 +1,8 @@
package net.codinux.banking.client.model.request package net.codinux.banking.client.model.request
import net.codinux.banking.client.model.* 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.config.NoArgConstructor
import net.codinux.banking.client.model.tan.TanMethodType import net.codinux.banking.client.model.tan.TanMethodType
@ -35,36 +37,13 @@ open class TransferMoneyRequestForUser(
instantTransfer: Boolean = false, instantTransfer: Boolean = false,
preferredTanMethods: List<TanMethodType>? = TanMethodType.TanMethodsPreferredByMostApplications, preferredTanMethods: List<TanMethodType>? = TanMethodType.NonVisualOrImageBased,
tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications, tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications,
) : TransferMoneyRequest(senderAccount, recipientName, recipientAccountIdentifier, recipientBankIdentifier, amount, currency, paymentReference, instantTransfer, preferredTanMethods, tanMethodsNotSupportedByApplication) {
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) constructor(bankCode: String, loginName: String, password: String, request: TransferMoneyRequest)
: this(bankCode, loginName, password, request.senderAccount, request.recipientName, request.recipientAccountIdentifier, request.recipientBankIdentifier, : this(bankCode, loginName, password, request.senderAccount, request.recipientName, request.recipientAccountIdentifier, request.recipientBankIdentifier,
request.amount, request.currency, request.paymentReference, request.instantTransfer, request.preferredTanMethods, request.tanMethodsNotSupportedByApplication) 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()}" override fun toString() = "$bankCode $loginName ${super.toString()}"
} }

View file

@ -2,7 +2,6 @@ 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.BankAccess 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

View file

@ -1,6 +1,5 @@
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
@ -9,22 +8,21 @@ 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, messageLog: List<MessageLogEntry> = emptyList()): Response<T> = fun <T> success(data: T): Response<T> =
Response(ResponseType.Success, data, messageLog = messageLog) Response(ResponseType.Success, data)
fun <T> error(errorType: ErrorType, internalError: String? = null, errorMessagesFromBank: List<String> = emptyList(), messageLog: List<MessageLogEntry> = emptyList()): Response<T> = fun <T> error(errorType: ErrorType, internalError: String? = null, errorMessagesFromBank: List<String> = emptyList()): Response<T> =
Response(ResponseType.Error, null, Error(errorType, internalError, errorMessagesFromBank), messageLog = messageLog) Response(ResponseType.Error, null, Error(errorType, internalError, errorMessagesFromBank))
fun <T> tanRequired(tanRequired: TanRequired, messageLog: List<MessageLogEntry> = emptyList()): Response<T> = fun <T> tanRequired(tanRequired: TanRequired): Response<T> =
Response(ResponseType.TanRequired, null, null, tanRequired, messageLog) Response(ResponseType.TanRequired, null, null, tanRequired)
fun <T> bankReturnedError(errorMessagesFromBank: List<String>, messageLog: List<MessageLogEntry> = emptyList()): Response<T> = fun <T> bankReturnedError(errorMessagesFromBank: List<String>): Response<T> =
Response.error(ErrorType.BankReturnedError, null, errorMessagesFromBank, messageLog) Response.error(ErrorType.BankReturnedError, null, errorMessagesFromBank)
} }

View file

@ -1,6 +1,5 @@
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
/** /**

View file

@ -12,7 +12,7 @@ open class Holding(
open var isin: String? = null, open var isin: String? = null,
open var wkn: String? = null, open var wkn: String? = null,
open var quantity: Double? = null, open var quantity: Int? = null,
open var currency: String? = null, open var currency: String? = 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? = null, val parsedDataSet: String,
val decodingError: String? = null val decodingError: String? = null
) { ) {
@get:JsonIgnore @get:JsonIgnore
val decodingSuccessful: Boolean val decodingSuccessful: Boolean
get() = parsedDataSet != null get() = decodingError == null
override fun toString(): String { override fun toString(): String {

View file

@ -88,7 +88,7 @@ open class TanChallenge(
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

@ -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? = null, val mimeType: String,
val imageBytesBase64: String? = null, val imageBytesBase64: String,
val decodingError: String? = null val decodingError: String? = null
) { ) {
@get:JsonIgnore @get:JsonIgnore
val decodingSuccessful: Boolean val decodingSuccessful: Boolean
get() = mimeType != null && imageBytesBase64 != null get() = decodingError == null
override fun toString(): String { override fun toString(): String {
mimeType?.let { if (decodingSuccessful == false) {
return mimeType return "Decoding error: $decodingError"
} }
return "Decoding error: $decodingError" return mimeType
} }
} }

View file

@ -3,9 +3,9 @@ package net.codinux.banking.client.model.tan
enum class TanMethodType { enum class TanMethodType {
EnterTan, EnterTan,
ChipTanManual, ChipTanManuell,
ChipTanFlickerCode, ChipTanFlickercode,
ChipTanUsb, ChipTanUsb,
@ -33,34 +33,16 @@ enum class TanMethodType {
companion object { companion object {
val NonVisual = listOf(TanMethodType.DecoupledTan, TanMethodType.DecoupledPushTan, TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.ChipTanManual, TanMethodType.EnterTan) val NonVisual = listOf(TanMethodType.DecoupledTan, TanMethodType.DecoupledPushTan, TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.ChipTanManuell, TanMethodType.EnterTan)
val NonVisualWithoutChipTanManual = NonVisual.toMutableList().apply { remove(TanMethodType.ChipTanManual) }.toList() val ImageBased = listOf(TanMethodType.QrCode, TanMethodType.ChipTanQrCode, TanMethodType.photoTan, TanMethodType.ChipTanPhotoTanMatrixCode)
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(NonVisualWithoutChipTanManual) addAll(listOf(TanMethodType.DecoupledTan, TanMethodType.DecoupledPushTan, TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.EnterTan))
addAll(ImageBased) addAll(ImageBased)
addAll(listOf(TanMethodType.ChipTanManual)) // this is quite inconvenient for user, so i added it as last addAll(listOf(TanMethodType.ChipTanManuell)) // this is quite inconvenient for user, so i added it 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) val TanMethodsNotSupportedByMostApplications = listOf(TanMethodType.ChipTanUsb)
} }

View file

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

View file

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

View file

@ -9,9 +9,9 @@ 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.TransferMoneyRequestForUser 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.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
import net.codinux.banking.fints.model.BankData
open class FinTs4kBankingClient( open class FinTs4kBankingClient(
config: FinTsClientConfiguration = FinTsClientConfiguration(), config: FinTsClientConfiguration = FinTsClientConfiguration(),
@ -21,7 +21,7 @@ open class FinTs4kBankingClient(
constructor(callback: BankingClientCallback) : this(FinTsClientConfiguration(), callback) constructor(callback: BankingClientCallback) : this(FinTsClientConfiguration(), callback)
constructor(options: FinTsClientOptions, callback: BankingClientCallback) 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) : this(FinTsClientConfiguration(net.codinux.banking.fints.config.FinTsClientOptions(options.collectMessageLog, false, options.removeSensitiveDataFromMessageLog, options.closeDialogs, options.version, options.productName)), callback)
protected open val mapper = FinTs4kMapper() protected open val mapper = FinTs4kMapper()
@ -35,33 +35,35 @@ open class FinTs4kBankingClient(
return mapper.map(response, request.bankInfo) return mapper.map(response, request.bankInfo)
} }
override suspend fun updateAccountTransactionsAsync(bank: BankAccess, accounts: List<BankAccount>?, preferredTanMethodsIfSelectedTanMethodIsNotAvailable: List<TanMethodType>?): Response<List<GetTransactionsResponse>> { override suspend fun updateAccountTransactionsAsync(bank: BankAccess, accounts: List<BankAccount>?): Response<List<GetTransactionsResponse>> {
val accountsToRequest = (accounts ?: bank.accounts).filter { it.supportsAnyFeature(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.RetrieveTransactions) } val accountsToRequest = (accounts ?: bank.accounts).filter { it.supportsAnyFeature(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.RetrieveBalance) }
if (accountsToRequest.isNotEmpty()) { if (accountsToRequest.isNotEmpty()) {
val responses = accountsToRequest.map { account -> var finTsModel: BankData? = null
val preferredTanMethods = listOf(bank.selectedTanMethod.type) + (preferredTanMethodsIfSelectedTanMethodIsNotAvailable ?: emptyList())
val parameter = mapper.mapToUpdateAccountTransactionsParameter(bank, account, preferredTanMethods) val responses = accountsToRequest.map { account ->
val parameter = mapper.mapToUpdateAccountTransactionsParameter(bank, account, finTsModel)
val response = client.getAccountDataAsync(parameter) 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 if (response.finTsModel != null) {
finTsModel = response.finTsModel // so that basic account data doesn't have to be retrieved another time if user has multiple accounts
}
Triple(account, parameter, response) Triple(account, parameter, response)
} }
return mapper.map(bank, responses) return mapper.map(responses)
} }
return Response.error(ErrorType.NoneOfTheAccountsSupportsRetrievingData, "Keines der Konten unterstützt das Abholen der Umsätze oder des Kontostands") // TODO: translate return Response.error(ErrorType.NoneOfTheAccountsSupportsRetrievingData, "Keiner der Konten unterstützt das Abholen der Umsätze oder des Kontostands") // TODO: translate
} }
override suspend fun transferMoneyAsync(request: TransferMoneyRequestForUser): Response<TransferMoneyResponse> { override suspend fun transferMoneyAsync(request: TransferMoneyRequestForUser): Response<TransferMoneyResponse> {
val response = client.transferMoneyAsync(mapper.mapToTransferMoneyParameter(request)) val response = client.transferMoneyAsync(mapper.mapToTransferMoneyParameter(request))
return mapper.mapTransferMoneyResponse(response, request.bank, request.account) return mapper.mapTransferMoneyResponse(response)
} }
} }

View file

@ -7,8 +7,6 @@ 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.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
@ -16,7 +14,6 @@ import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser 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
@ -25,15 +22,14 @@ 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.messages.datenelemente.implementierte.tan.* import net.codinux.banking.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.MobilePhoneTanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMediumStatus
import net.codinux.banking.fints.model.* import net.codinux.banking.fints.model.*
import net.codinux.banking.fints.transactions.swift.model.Holding import net.codinux.banking.fints.transactions.swift.model.Holding
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.MobilePhoneTanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMediumStatus
import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper
import net.dankito.banking.client.model.parameter.TransferMoneyParameter import net.dankito.banking.client.model.parameter.TransferMoneyParameter
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
@ -41,16 +37,6 @@ 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()
@ -72,76 +58,72 @@ open class FinTs4kMapper {
bank.serverAddress, bank.bic, bank.name bank.serverAddress, bank.bic, bank.name
) )
open fun mapToUpdateAccountTransactionsParameter(bank: BankAccess, account: BankAccount, preferredTanMethods: List<TanMethodType>? = null): GetAccountDataParameter { open fun mapToUpdateAccountTransactionsParameter(bank: BankAccess, account: BankAccount, finTsModel: BankData?): GetAccountDataParameter {
val defaults = GetAccountDataOptions() val defaults = GetAccountDataOptions()
val accountIdentifier = BankAccountIdentifierImpl(account.identifier, account.subAccountNumber, account.iban) 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 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) val retrieveTransactions = if (from != null) RetrieveTransactions.AccordingToRetrieveFromAndTo else RetrieveTransactions.valueOf(defaults.retrieveTransactions.name)
// val preferredTanMethods = listOf(mapTanMethodType(bank.selectedTanMethod.type)) // TODO: currently we aren't saving TanMethods in database, re-enable as soon as TanMethods get saved
val preferredTanMethods = defaults.preferredTanMethods?.map { mapTanMethodType(it) }
return GetAccountDataParameter(bank.domesticBankCode, bank.loginName, bank.password!!, listOf(accountIdentifier), true, return GetAccountDataParameter(bank.domesticBankCode, bank.loginName, bank.password!!, listOf(accountIdentifier), true,
retrieveTransactions, from, retrieveTransactions, from,
preferredTanMethods = preferredTanMethods?.map { mapTanMethodType(it) }, preferredTanMethods = preferredTanMethods,
preferredTanMedium = bank.selectedTanMediumIdentifier, preferredTanMedium = bank.selectedTanMediumIdentifier,
finTsModel = bank.clientData as? BankData, finTsModel = finTsModel
serializedFinTsModel = bank.serializedClientData
) )
} }
open fun mapBankAccountIdentifier(account: BankAccountIdentifier): BankAccountIdentifierImpl = open fun mapBankAccountIdentifier(account: BankAccountIdentifier): BankAccountIdentifierImpl =
BankAccountIdentifierImpl(account.identifier, account.subAccountNumber, account.iban) BankAccountIdentifierImpl(account.identifier, account.subAccountNumber, account.iban)
open fun mapTanMethod(method: TanMethod) = net.codinux.banking.fints.model.TanMethod( // TODO: get instance from FinTsData, don't create manually
method.displayName, Sicherheitsfunktion.entries.first { it.code == method.identifier }, mapTanMethodType(method.type), null, method.maxTanInputLength, mapAllowedTanFormat(method.allowedTanFormat)
)
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(TanMethodTypesToMigrateReverse[type.name] ?: type.name) net.codinux.banking.fints.model.TanMethodType.valueOf(type.name)
protected open fun mapAllowedTanFormat(allowedTanFormat: AllowedTanFormat?): net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat = 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 allowedTanFormat?.let { net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat.valueOf(it.name) } ?: net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat.Alphanumeric
open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse, bankInfo: BankInfo? = null): Response<GetAccountDataResponse> = open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse, bank: BankInfo? = null): Response<GetAccountDataResponse> =
if (response.successful && response.customerAccount != null) { if (response.successful && response.customerAccount != null) {
val bank = mapBank(response.customerAccount!!, bankInfo, response) Response.success(GetAccountDataResponse(mapBank(response.customerAccount!!, bank)))
Response.success(GetAccountDataResponse(bank), mapMessageLog(response, bank))
} else { } else {
mapError(response, mapMessageLog(response)) mapError(response)
} }
open fun map(bank: BankAccess, responses: List<Triple<BankAccount, GetAccountDataParameter, net.dankito.banking.client.model.response.GetAccountDataResponse>>): Response<List<GetTransactionsResponse>> { open fun map(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 val type = if (responses.all { it.third.successful }) ResponseType.Success else ResponseType.Error
// TODO: update BankAccess and BankAccount objects according to retrieved data // TODO: update BankAccess and BankAccount objects according to retrieved data
val mappedResponses = responses.map { (account, param, getAccountDataResponse) -> val mappedResponses = responses.map { (account, param, getAccountDataResponse) ->
val fintsBank = getAccountDataResponse.customerAccount val bank = getAccountDataResponse.customerAccount
val finTsBankAccount = fintsBank?.accounts?.firstOrNull { it.identifier == account.identifier && it.subAccountNumber == account.subAccountNumber } val finTsBankAccount = bank?.accounts?.firstOrNull { it.identifier == account.identifier && it.subAccountNumber == account.subAccountNumber }
val messageLog = mapMessageLog(getAccountDataResponse, bank, account) if (getAccountDataResponse.successful && bank != null && finTsBankAccount != null) {
if (getAccountDataResponse.successful && fintsBank != null && finTsBankAccount != null) {
if (finTsBankAccount.lastAccountUpdateTime != null) { if (finTsBankAccount.lastAccountUpdateTime != null) {
account.lastAccountUpdateTime = finTsBankAccount.lastAccountUpdateTime account.lastAccountUpdateTime = finTsBankAccount.lastAccountUpdateTime
} }
if (account.retrievedTransactionsFrom == null || (finTsBankAccount.retrievedTransactionsFrom != null if (account.retrievedTransactionsFrom == null || (finTsBankAccount.retrievedTransactionsFrom != null
&& finTsBankAccount.retrievedTransactionsFrom!! < account.retrievedTransactionsFrom!!)) { && account.retrievedTransactionsFrom!! < finTsBankAccount.retrievedTransactionsFrom!!)) {
account.retrievedTransactionsFrom = finTsBankAccount.retrievedTransactionsFrom account.retrievedTransactionsFrom = finTsBankAccount.retrievedTransactionsFrom
} }
val balance = mapMoney(finTsBankAccount.balance) Response.success(GetTransactionsResponse(account, mapMoney(finTsBankAccount.balance), mapBookedTransactions(finTsBankAccount), emptyList(),
account.balance = balance
mapCommonResponseData(bank, getAccountDataResponse)
Response.success(GetTransactionsResponse(account, balance, mapBookedTransactions(finTsBankAccount), emptyList(),
mapHoldings(finTsBankAccount.statementOfHoldings, finTsBankAccount.currency, finTsBankAccount.lastAccountUpdateTime), mapHoldings(finTsBankAccount.statementOfHoldings, finTsBankAccount.currency, finTsBankAccount.lastAccountUpdateTime),
finTsBankAccount.lastAccountUpdateTime ?: Clock.System.now(), param.retrieveTransactionsFrom, param.retrieveTransactionsTo), finTsBankAccount.lastAccountUpdateTime ?: Clock.System.now(), param.retrieveTransactionsFrom, param.retrieveTransactionsTo))
messageLog)
} else { } else {
mapError(getAccountDataResponse, messageLog) mapError(getAccountDataResponse)
} }
} }
val data = mappedResponses.filter { it.type == ResponseType.Success }.mapNotNull { it.data } 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 }) { }) return (object : Response<List<GetTransactionsResponse>>(type, data, mappedResponses.firstNotNullOfOrNull { it.error }) { })
} }
@ -156,7 +138,7 @@ open class FinTs4kMapper {
) )
protected open fun mapBank(bank: net.dankito.banking.client.model.CustomerAccount, info: BankInfo? = null, response: FinTsClientResponse? = null) = BankAccess( protected open fun mapBank(bank: net.dankito.banking.client.model.CustomerAccount, info: BankInfo? = null) = BankAccess(
bank.bankCode, bank.loginName, bank.password, bank.bankCode, bank.loginName, bank.password,
info?.name ?: bank.bankName, bank.bic, bank.customerName, bank.userId, info?.name ?: bank.bankName, bank.bic, bank.customerName, bank.userId,
bank.accounts.map { mapAccount(it) }.sortedBy { it.type } bank.accounts.map { mapAccount(it) }.sortedBy { it.type }
@ -168,9 +150,7 @@ open class FinTs4kMapper {
info?.bankingGroup ?: getBankingGroup(bank.bankName, bank.bic), info?.bankingGroup ?: getBankingGroup(bank.bankName, bank.bic),
bank.finTsServerAddress, bank.finTsServerAddress,
"de" "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)
@ -337,7 +317,7 @@ open class FinTs4kMapper {
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
} }
@ -349,7 +329,7 @@ open class FinTs4kMapper {
) )
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(TanMethodTypesToMigrate[type.name] ?: type.name) TanMethodType.valueOf(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
@ -358,18 +338,14 @@ 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.tanGenerator?.let { mapTanGeneratorTanMedium(it) }, (tanMedium as? TanGeneratorTanMedium)?.let { mapTanGeneratorTanMedium(it) },
tanMedium.mobilePhone?.let { mapMobilePhoneTanMedium(it) } (tanMedium as? MobilePhoneTanMedium)?.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) {
@ -388,9 +364,9 @@ open class FinTs4kMapper {
tanMedium.validFrom, tanMedium.validTo tanMedium.validFrom, tanMedium.validTo
) )
protected open fun mapTanMediumType(tanMedium: TanMedium): TanMediumType = when (tanMedium.mediumClass) { protected open fun mapTanMediumType(tanMedium: TanMedium): TanMediumType = when {
TanMediumKlasse.MobiltelefonMitMobileTan -> TanMediumType.MobilePhone tanMedium is MobilePhoneTanMedium -> TanMediumType.MobilePhone
TanMediumKlasse.TanGenerator -> TanMediumType.TanGenerator tanMedium is TanGeneratorTanMedium -> TanMediumType.TanGenerator
else -> TanMediumType.Generic else -> TanMediumType.Generic
} }
@ -405,74 +381,29 @@ open class FinTs4kMapper {
request.recipientName, request.recipientAccountIdentifier, request.recipientBankIdentifier, request.recipientName, request.recipientAccountIdentifier, request.recipientBankIdentifier,
mapToMoney(request.amount, request.currency), request.paymentReference, request.instantTransfer, mapToMoney(request.amount, request.currency), request.paymentReference, request.instantTransfer,
request.preferredTanMethods?.map { mapTanMethodType(it) }, request.preferredTanMethods?.map { mapTanMethodType(it) },
request.tanMethodsNotSupportedByApplication.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> = open fun mapTransferMoneyResponse(response: net.dankito.banking.client.model.response.TransferMoneyResponse): Response<TransferMoneyResponse> =
if (response.successful) { if (response.successful) {
bank?.let { mapCommonResponseData(it, response) } Response.success(TransferMoneyResponse())
Response.success(TransferMoneyResponse(), mapMessageLog(response, bank, account))
} else { } else {
mapError(response, mapMessageLog(response, bank, account)) mapError(response)
} }
open fun mapToMoney(amount: Amount, currency: String): Money = Money(amount.toString(), currency) open fun mapToMoney(amount: Amount, currency: String): Money = Money(amount.toString(), currency)
open fun mapMessageLog(response: FinTsClientResponse, bank: BankAccess? = null, account: BankAccount? = null) = protected open fun <T> mapError(response: net.dankito.banking.client.model.response.FinTsClientResponse): Response<T> {
mapMessageLog(response.messageLog, bank, account) return if (response.error != null) {
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, Response.error(ErrorType.valueOf(response.error!!.name), if (response.error == ErrorCode.BankReturnedError) null else response.errorMessage,
if (response.error == ErrorCode.BankReturnedError && response.errorMessage !== null) listOf(response.errorMessage!!) else emptyList(), messageLog) if (response.error == ErrorCode.BankReturnedError && response.errorMessage !== null) listOf(response.errorMessage!!) else emptyList())
} else { } else {
Response.error(ErrorType.UnknownError, response.errorMessage, messageLog = messageLog) Response.error(ErrorType.UnknownError, response.errorMessage)
} }
}
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

@ -24,8 +24,6 @@ data class FinTsClientOptions(
*/ */
val removeSensitiveDataFromMessageLog: Boolean = true, val removeSensitiveDataFromMessageLog: Boolean = true,
val appendFinTsMessagesToLog: Boolean = false,
val closeDialogs: Boolean = true, val closeDialogs: Boolean = true,
val version: String = "1.0.0", // TODO: get version dynamically val version: String = "1.0.0", // TODO: get version dynamically

View file

@ -3,7 +3,6 @@ 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 net.codinux.banking.client.model.tan.EnterTanResult
import kotlin.test.* import kotlin.test.*
@Ignore @Ignore
@ -19,10 +18,7 @@ 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

@ -12,7 +12,7 @@ class FinTs4kMapperTest {
@Test @Test
fun getTotalBalance_TotalBalanceIsNull_CalculateByQuantityAndMarketValue() { fun getTotalBalance_TotalBalanceIsNull_CalculateByQuantityAndMarketValue() {
val holding = Holding("", null, null, null, 4.0, null, null, null, fints4kAmount("13.33")) val holding = Holding("", null, null, null, 4, null, null, null, fints4kAmount("13.33"))
val result = underTest.getTotalBalance(holding) val result = underTest.getTotalBalance(holding)
@ -21,7 +21,7 @@ class FinTs4kMapperTest {
@Test @Test
fun getTotalCostPrice_TotalCostPriceIsNull_CalculateByQuantityAndAverageCostPrice() { fun getTotalCostPrice_TotalCostPriceIsNull_CalculateByQuantityAndAverageCostPrice() {
val holding = Holding("", null, null, null, 47.0, fints4kAmount("16.828"), null) val holding = Holding("", null, null, null, 47, fints4kAmount("16.828"), null)
val result = underTest.getTotalCostPrice(holding) val result = underTest.getTotalCostPrice(holding)

View file

@ -1,5 +1,4 @@
# 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).
@ -12,8 +11,13 @@ 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 {
// other repositories like mavenCentral(), ... mavenCentral()
maven { maven {
setUrl("https://maven.dankito.net/api/packages/codinux/maven") setUrl("https://maven.dankito.net/api/packages/codinux/maven")
} }
@ -21,7 +25,7 @@ repositories {
dependencies { dependencies {
implementation("net.codinux.banking.client:fints4k-banking-client:0.7.2") implementation("net.codinux.banking.client:fints4k-banking-client:0.6.0")
} }
``` ```
@ -163,22 +167,4 @@ fun updateAccountTransactions() {
} }
} }
} }
``` ```
## 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,7 +12,7 @@ buildscript {
allprojects { allprojects {
group = "net.codinux.banking.client" group = "net.codinux.banking.client"
version = "0.7.2" version = "0.6.0"
repositories { repositories {
mavenCentral() mavenCentral()
@ -23,7 +23,7 @@ allprojects {
} }
ext["sourceCodeRepositoryBaseUrl"] = "git.dankito.net/codinux/BankingClient" ext["sourceCodeRepositoryBaseUrl"] = "github.com/codinux/BankingClient"
ext["projectDescription"] = "Model and base definitions for Banking Client, a common abstraction for different implements of banking libraries" ext["projectDescription"] = "Model and base definitions for Banking Client, a common abstraction for different implements of banking libraries"
} }

View file

@ -1,13 +1,10 @@
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 # 0.3.10 uses Kotlin 2.0.0

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

File diff suppressed because it is too large Load diff