Implemented handling TAN in FinTs4kBankingClient

This commit is contained in:
dankito 2024-08-18 17:27:42 +02:00
parent d57f6d8600
commit d331a4810c
8 changed files with 155 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<GetAccountDataResponse> {
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 <T> mapError(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<T> {
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()
}

View File

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