Compare commits

..

No commits in common. "98260fbfbfbbf9b69ad5058823f399f0e07e1de7" and "43031b24ade3563c204f8ee86c1da63d560977d4" have entirely different histories.

29 changed files with 558 additions and 481 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,30 +70,6 @@ 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

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-14")
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(),
@ -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
@ -62,25 +58,30 @@ 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(type.name) net.codinux.banking.fints.model.TanMethodType.valueOf(type.name)
@ -88,50 +89,41 @@ open class FinTs4kMapper {
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 }) { })
} }
@ -146,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 }
@ -158,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)
@ -327,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
} }
@ -348,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) {
@ -378,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
} }
@ -395,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

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

@ -12,8 +12,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 +26,7 @@ repositories {
dependencies { dependencies {
implementation("net.codinux.banking.client:fints4k-banking-client:0.7.1") implementation("net.codinux.banking.client:fints4k-banking-client:0.6.1")
} }
``` ```

View File

@ -12,7 +12,7 @@ buildscript {
allprojects { allprojects {
group = "net.codinux.banking.client" group = "net.codinux.banking.client"
version = "0.7.1" version = "0.6.1"
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"
} }

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

File diff suppressed because it is too large Load Diff