From d331a4810c16b46617faedbfddca59a8e20204a4 Mon Sep 17 00:00:00 2001 From: dankito Date: Sun, 18 Aug 2024 17:27:42 +0200 Subject: [PATCH] Implemented handling TAN in FinTs4kBankingClient --- .../banking/client/BankingClientCallback.kt | 10 ++ .../client/SimpleBankingClientCallback.kt | 18 ++++ .../banking/client/model/tan/TanChallenge.kt | 8 +- .../BridgeFintTsToBankingClientCallback.kt | 32 ++++++ .../client/fints4k/FinTs4kBankingClient.kt | 11 +-- .../FinTs4kBankingClientForCustomer.kt | 9 +- .../banking/client/fints4k/FinTs4kMapper.kt | 98 ++++++++++++++----- .../fints4k/FinTs4kBankingClientTest.kt | 5 +- 8 files changed, 155 insertions(+), 36 deletions(-) create mode 100644 BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientCallback.kt create mode 100644 BankingClient/src/commonMain/kotlin/net/codinux/banking/client/SimpleBankingClientCallback.kt create mode 100644 FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/BridgeFintTsToBankingClientCallback.kt diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientCallback.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientCallback.kt new file mode 100644 index 00000000..e23c4c0e --- /dev/null +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientCallback.kt @@ -0,0 +1,10 @@ +package net.codinux.banking.client + +import net.codinux.banking.client.model.tan.EnterTanResult +import net.codinux.banking.client.model.tan.TanChallenge + +interface BankingClientCallback { + + fun enterTan(tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) + +} \ No newline at end of file diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/SimpleBankingClientCallback.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/SimpleBankingClientCallback.kt new file mode 100644 index 00000000..e1fd00f4 --- /dev/null +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/SimpleBankingClientCallback.kt @@ -0,0 +1,18 @@ +package net.codinux.banking.client + +import net.codinux.banking.client.model.tan.EnterTanResult +import net.codinux.banking.client.model.tan.TanChallenge + +open class SimpleBankingClientCallback( + protected val enterTan: ((tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) -> Unit)? = null +) : BankingClientCallback { + + override fun enterTan(tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) { + if (enterTan != null) { + enterTan.invoke(tanChallenge, callback) + } else { + callback(EnterTanResult(null)) + } + } + +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/tan/TanChallenge.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/tan/TanChallenge.kt index eb525597..2bd722af 100644 --- a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/tan/TanChallenge.kt +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/tan/TanChallenge.kt @@ -1,5 +1,6 @@ package net.codinux.banking.client.model.tan +import net.codinux.banking.client.model.BankAccount import net.codinux.banking.client.model.CustomerAccount import net.codinux.banking.client.model.config.NoArgConstructor @@ -7,16 +8,17 @@ import net.codinux.banking.client.model.config.NoArgConstructor open class TanChallenge( val type: TanChallengeType, val forAction: ActionRequiringTan, - val customer: CustomerAccount, val messageToShowToUser: String, val tanMethod: TanMethod, val tanImage: TanImage? = null, - val flickerCode: FlickerCode? = null + val flickerCode: FlickerCode? = null, + val customer: CustomerAccount, + val account: BankAccount? = null // TODO: add availableTanMethods, selectedTanMedium, availableTanMedia ) { override fun toString(): String { - return "$tanMethod: $messageToShowToUser" + when (type) { + return "$tanMethod $forAction: $messageToShowToUser" + when (type) { TanChallengeType.EnterTan -> "" TanChallengeType.Image -> ", Image: $tanImage" TanChallengeType.Flickercode -> ", FlickerCode: $flickerCode" diff --git a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/BridgeFintTsToBankingClientCallback.kt b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/BridgeFintTsToBankingClientCallback.kt new file mode 100644 index 00000000..014226a6 --- /dev/null +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/BridgeFintTsToBankingClientCallback.kt @@ -0,0 +1,32 @@ +package net.codinux.banking.client.fints4k + +import net.codinux.banking.client.BankingClientCallback +import net.dankito.banking.fints.callback.FinTsClientCallback +import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium +import net.dankito.banking.fints.model.BankData +import net.dankito.banking.fints.model.EnterTanGeneratorAtcResult +import net.dankito.banking.fints.model.TanMethod + +open class BridgeFintTsToBankingClientCallback( + protected val bankingClientCallback: BankingClientCallback, + protected val mapper: FinTs4kMapper +) : FinTsClientCallback { + + override suspend fun askUserForTanMethod(supportedTanMethods: List, suggestedTanMethod: TanMethod?): TanMethod? { + return suggestedTanMethod + } + + override suspend fun enterTan(tanChallenge: net.dankito.banking.fints.model.TanChallenge) { + bankingClientCallback.enterTan(mapper.mapTanChallenge(tanChallenge)) { enterTanResult -> + if (enterTanResult.enteredTan != null) { + tanChallenge.userEnteredTan(enterTanResult.enteredTan!!) + } else { + tanChallenge.userDidNotEnterTan() + } + } + } + + override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult { + return EnterTanGeneratorAtcResult.userDidNotEnterAtc() + } +} \ No newline at end of file diff --git a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt index 7513c21f..c9c1b350 100644 --- a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt @@ -1,21 +1,20 @@ package net.codinux.banking.client.fints4k import net.codinux.banking.client.BankingClient -import net.codinux.banking.client.model.AccountCredentials +import net.codinux.banking.client.BankingClientCallback import net.codinux.banking.client.model.options.GetAccountDataOptions import net.codinux.banking.client.model.request.GetAccountDataRequest import net.codinux.banking.client.model.response.GetAccountDataResponse import net.codinux.banking.client.model.response.Response import net.dankito.banking.fints.FinTsClient -import net.dankito.banking.fints.callback.SimpleFinTsClientCallback -open class FinTs4kBankingClient : BankingClient { +open class FinTs4kBankingClient( + callback: BankingClientCallback +) : BankingClient { private val mapper = FinTs4kMapper() - private val client = FinTsClient(SimpleFinTsClientCallback { tanChallenge -> -// callback.enterTan() - }) + private val client = FinTsClient(BridgeFintTsToBankingClientCallback(callback, mapper)) override suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response { diff --git a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientForCustomer.kt b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientForCustomer.kt index f877844c..0680e75d 100644 --- a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientForCustomer.kt +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientForCustomer.kt @@ -1,12 +1,13 @@ package net.codinux.banking.client.fints4k +import net.codinux.banking.client.BankingClientCallback import net.codinux.banking.client.BankingClientForCustomerBase import net.codinux.banking.client.model.AccountCredentials -open class FinTs4kBankingClientForCustomer(credentials: AccountCredentials) - : BankingClientForCustomerBase(credentials, FinTs4kBankingClient()) { +open class FinTs4kBankingClientForCustomer(credentials: AccountCredentials, callback: BankingClientCallback) + : BankingClientForCustomerBase(credentials, FinTs4kBankingClient(callback)) { - constructor(bankCode: String, loginName: String, password: String) - : this(AccountCredentials(bankCode, loginName, password)) + constructor(bankCode: String, loginName: String, password: String, callback: BankingClientCallback) + : this(AccountCredentials(bankCode, loginName, password), callback) } \ No newline at end of file diff --git a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt index bc9172b1..03568e8c 100644 --- a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt @@ -1,15 +1,29 @@ package net.codinux.banking.client.fints4k import net.codinux.banking.client.model.* +import net.codinux.banking.client.model.AccountTransaction +import net.codinux.banking.client.model.Amount +import net.codinux.banking.client.model.tan.* import net.codinux.banking.client.model.options.GetAccountDataOptions import net.codinux.banking.client.model.response.* +import net.codinux.banking.client.model.tan.ActionRequiringTan +import net.codinux.banking.client.model.tan.TanChallenge +import net.codinux.banking.client.model.tan.TanImage +import net.codinux.banking.client.model.tan.TanMethod +import net.codinux.banking.client.model.tan.TanMethodType import net.dankito.banking.client.model.parameter.GetAccountDataParameter import net.dankito.banking.client.model.parameter.RetrieveTransactions import net.dankito.banking.client.model.response.ErrorCode -import net.dankito.banking.fints.model.Money +import net.dankito.banking.fints.mapper.FinTsModelMapper +import net.dankito.banking.fints.model.* +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi open class FinTs4kMapper { + private val fintsModelMapper = FinTsModelMapper() + + fun mapToGetAccountDataParameter(credentials: AccountCredentials, options: GetAccountDataOptions) = GetAccountDataParameter( credentials.bankCode, credentials.loginName, credentials.password, null, @@ -21,33 +35,36 @@ open class FinTs4kMapper { fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response { return if (response.successful && response.customerAccount != null) { - Response.success(mapCustomer(response.customerAccount!!)) + Response.success(GetAccountDataResponse(mapCustomer(response.customerAccount!!))) } else { mapError(response) } } - private fun mapCustomer(customer: net.dankito.banking.client.model.CustomerAccount): GetAccountDataResponse { - val mapped = CustomerAccount( - customer.bankCode, customer.loginName, customer.password, - customer.bankName, customer.bic, customer.customerName, customer.userId, - customer.accounts.map { mapAccount(it) } - ) + private fun mapCustomer(customer: net.dankito.banking.client.model.CustomerAccount): CustomerAccount = CustomerAccount( + customer.bankCode, customer.loginName, customer.password, + customer.bankName, customer.bic, customer.customerName, customer.userId, + customer.accounts.map { mapAccount(it) } + ) - return GetAccountDataResponse(mapped) - } + fun mapCustomer(bank: BankData): CustomerAccount = + mapCustomer(fintsModelMapper.map(bank)) private fun mapAccount(account: net.dankito.banking.client.model.BankAccount): BankAccount = BankAccount( account.identifier, account.accountHolderName, mapAccountType(account.type), account.iban, account.subAccountNumber, - account.productName, account.currency, account.accountLimit, isAccountTypeSupported(account), + account.productName, account.currency, account.accountLimit, account.isAccountTypeSupportedByApplication, mapFeatures(account), mapAmount(account.balance), account.retrievedTransactionsFrom, account.retrievedTransactionsTo, - // TODO: map haveAllTransactionsBeenRetrieved and countDaysForWhichTransactionsAreKept + // TODO: map haveAllTransactionsBeenRetrieved + countDaysForWhichTransactionsAreKept = account.countDaysForWhichTransactionsAreKept, bookedTransactions = account.bookedTransactions.map { mapTransaction(it) }.toMutableList() ) + fun mapAccount(account: AccountData): BankAccount = + mapAccount(fintsModelMapper.map(account)) + private fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType = BankAccountType.valueOf(type.name) @@ -66,16 +83,6 @@ open class FinTs4kMapper { } } - private fun isAccountTypeSupported(account: net.dankito.banking.client.model.BankAccount): Boolean { - // TODO: -// open val isAccountTypeSupportedByApplication: Boolean -// get() = FinTsClient.SupportedAccountTypes.contains(accountType) -// || allowedJobNames.contains(CustomerSegmentId.Balance.id) -// || allowedJobNames.contains(CustomerSegmentId.AccountTransactionsMt940.id) - - return true - } - private fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction( mapAmount(transaction.amount), transaction.amount.currency.code, transaction.unparsedReference, @@ -92,6 +99,50 @@ open class FinTs4kMapper { private fun mapAmount(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.')) + fun mapTanChallenge(challenge: net.dankito.banking.fints.model.TanChallenge): TanChallenge { + val type = mapTanChallengeType(challenge) + val action = mapActionRequiringTan(challenge.forAction) + val tanMethod = mapTanMethod(challenge.tanMethod) + val customer = mapCustomer(challenge.bank) + val account = challenge.account?.let { mapAccount(it) } + + val tanImage = if (challenge is ImageTanChallenge) mapTanImage(challenge.image) else null + val flickerCode = if (challenge is FlickerCodeTanChallenge) mapFlickerCode(challenge.flickerCode) else null + + return TanChallenge(type, action, challenge.messageToShowToUser, tanMethod, tanImage, flickerCode, customer, account) + } + + private fun mapTanChallengeType(challenge: net.dankito.banking.fints.model.TanChallenge): TanChallengeType = when { + challenge is ImageTanChallenge -> TanChallengeType.Image + challenge is FlickerCodeTanChallenge -> TanChallengeType.Flickercode + else -> TanChallengeType.EnterTan + } + + private fun mapActionRequiringTan(action: net.dankito.banking.fints.model.ActionRequiringTan): ActionRequiringTan = + ActionRequiringTan.valueOf(action.name) + + private fun mapTanMethod(method: net.dankito.banking.fints.model.TanMethod): TanMethod = TanMethod( + method.displayName, mapTanMethodType(method.type), method.securityFunction.code, method.maxTanInputLength, mapAllowedTanFormat(method.allowedTanFormat) + ) + + private fun mapTanMethodType(type: net.dankito.banking.fints.model.TanMethodType): TanMethodType = + TanMethodType.valueOf(type.name) + + private fun mapAllowedTanFormat(allowedTanFormat: net.dankito.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat?): AllowedTanFormat = + allowedTanFormat?.let { AllowedTanFormat.valueOf(it.name) } ?: AllowedTanFormat.Alphanumeric + + private fun mapTanImage(image: net.dankito.banking.fints.tan.TanImage): TanImage = + TanImage(image.mimeType, mapToBase64(image.imageBytes), mapException(image.decodingError)) + + @OptIn(ExperimentalEncodingApi::class) + private fun mapToBase64(bytes: ByteArray): String { + return Base64.Default.encode(bytes) + } + + private fun mapFlickerCode(flickerCode: net.dankito.banking.fints.tan.FlickerCode): FlickerCode = + FlickerCode(flickerCode.challengeHHD_UC, flickerCode.parsedDataSet, mapException(flickerCode.decodingError)) + + private fun mapError(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response { return if (response.error != null) { Response.error(ErrorType.valueOf(response.error!!.name), if (response.error == ErrorCode.BankReturnedError) null else response.errorMessage, @@ -101,4 +152,7 @@ open class FinTs4kMapper { } } + private fun mapException(exception: Exception?): String? = + exception?.stackTraceToString() + } \ No newline at end of file diff --git a/FinTs4jBankingClient/src/commonTest/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientTest.kt b/FinTs4jBankingClient/src/commonTest/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientTest.kt index 4a14fd0e..07c3b4f9 100644 --- a/FinTs4jBankingClient/src/commonTest/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientTest.kt +++ b/FinTs4jBankingClient/src/commonTest/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientTest.kt @@ -1,6 +1,7 @@ package net.codinux.banking.client.fints4k import kotlinx.coroutines.test.runTest +import net.codinux.banking.client.SimpleBankingClientCallback import net.codinux.banking.client.model.response.ResponseType import kotlin.test.Test import kotlin.test.assertEquals @@ -18,7 +19,9 @@ class FinTs4kBankingClientTest { } - private val underTest = FinTs4kBankingClientForCustomer(bankCode, loginName, password) + private val underTest = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback { customer, tanChallenge -> + + }) @Test