From 41d02ec3438e4ae9745888b3e6952447fd6624a7 Mon Sep 17 00:00:00 2001 From: dankito Date: Mon, 21 Dec 2020 20:59:47 +0100 Subject: [PATCH] Extracted ModelMapper --- .../banking/fints/FinTsClientForCustomer.kt | 4 +- .../dankito/banking/fints/FinTsJobExecutor.kt | 339 +----------------- .../banking/fints/model/mapper/ModelMapper.kt | 335 +++++++++++++++++ .../bankdetails/BanksFinTsDetailsRetriever.kt | 23 +- 4 files changed, 367 insertions(+), 334 deletions(-) create mode 100644 fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/mapper/ModelMapper.kt diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientForCustomer.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientForCustomer.kt index 2e36dbe7..dabd2f98 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientForCustomer.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientForCustomer.kt @@ -4,6 +4,7 @@ import net.dankito.banking.fints.callback.FinTsClientCallback import net.dankito.banking.fints.log.MessageLogCollector import net.dankito.banking.fints.messages.MessageBuilder import net.dankito.banking.fints.model.* +import net.dankito.banking.fints.model.mapper.ModelMapper import net.dankito.banking.fints.response.ResponseParser import net.dankito.banking.fints.response.client.AddAccountResponse import net.dankito.banking.fints.response.client.FinTsClientResponse @@ -25,10 +26,11 @@ open class FinTsClientForCustomer( responseParser: ResponseParser = ResponseParser(), mt940Parser: IAccountTransactionsParser = Mt940AccountTransactionsParser(), messageLogCollector: MessageLogCollector = MessageLogCollector(), + modelMapper: ModelMapper = ModelMapper(messageBuilder), product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically){} ) { - protected val client = FinTsClient(FinTsJobExecutor(callback, webClient, base64Service, messageBuilder, responseParser, mt940Parser, messageLogCollector, product)) + protected val client = FinTsClient(FinTsJobExecutor(callback, webClient, base64Service, messageBuilder, responseParser, mt940Parser, messageLogCollector, modelMapper, product)) open val messageLogWithoutSensitiveData: List diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt index 9661be6f..0f89349e 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt @@ -5,14 +5,12 @@ import net.dankito.banking.fints.log.IMessageLogAppender import net.dankito.banking.fints.log.MessageLogCollector import net.dankito.banking.fints.messages.MessageBuilder import net.dankito.banking.fints.messages.MessageBuilderResult -import net.dankito.banking.fints.messages.datenelemente.implementierte.Dialogsprache -import net.dankito.banking.fints.messages.datenelemente.implementierte.KundensystemStatusWerte -import net.dankito.banking.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion import net.dankito.banking.fints.messages.datenelemente.implementierte.signatur.VersionDesSicherheitsverfahrens import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.* import net.dankito.banking.fints.messages.segmente.id.CustomerSegmentId import net.dankito.banking.fints.messages.segmente.id.ISegmentId import net.dankito.banking.fints.model.* +import net.dankito.banking.fints.model.mapper.ModelMapper import net.dankito.banking.fints.response.BankResponse import net.dankito.banking.fints.response.InstituteSegmentId import net.dankito.banking.fints.response.ResponseParser @@ -50,6 +48,7 @@ open class FinTsJobExecutor( protected open val responseParser: ResponseParser = ResponseParser(), protected open val mt940Parser: IAccountTransactionsParser = Mt940AccountTransactionsParser(), protected open val messageLogCollector: MessageLogCollector = MessageLogCollector(), + protected open val modelMapper: ModelMapper = ModelMapper(messageBuilder), protected open val product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically ) { @@ -140,10 +139,8 @@ open class FinTsJobExecutor( protected open fun handleGetUsersTanMethodsResponse(response: BankResponse, dialogContext: DialogContext, callback: (BankResponse) -> Unit) { val getUsersTanMethodsResponse = GetUserTanMethodsResponse(response) - if (getUsersTanMethodsResponse.successful) { // TODO: really update data only on complete successfully response? as it may contain useful information anyway // TODO: extract method for this code part - updateBankData(dialogContext.bank, getUsersTanMethodsResponse) - updateCustomerData(dialogContext.bank, getUsersTanMethodsResponse) - } + // TODO: really update data only on complete successfully response? as it may contain useful information anyway // TODO: extract method for this code part + updateBankAndCustomerDataIfResponseSuccessful(dialogContext, getUsersTanMethodsResponse) // even though it is required by specification some banks don't support retrieving user's TAN method by setting TAN method to '999' if (bankDoesNotSupportRetrievingUsersTanMethods(getUsersTanMethodsResponse)) { @@ -201,8 +198,7 @@ open class FinTsJobExecutor( closeDialog(dialogContext) if (response.successful) { - updateBankData(bank, response) - updateCustomerData(bank, response) + updateBankAndCustomerData(bank, response) } callback(response) @@ -313,8 +309,7 @@ open class FinTsJobExecutor( getAndHandleResponseForMessage(message, dialogContext) { response -> if (response.successful) { - updateBankData(bank, response) - updateCustomerData(bank, response) + updateBankAndCustomerData(bank, response) closeDialog(dialogContext) } @@ -722,11 +717,7 @@ open class FinTsJobExecutor( val message = messageBuilder.createInitDialogMessage(dialogContext) getAndHandleResponseForMessage(message, dialogContext) { response -> - - if (response.successful) { - updateBankData(dialogContext.bank, response) - updateCustomerData(dialogContext.bank, response) - } + updateBankAndCustomerDataIfResponseSuccessful(dialogContext, response) callback(response) } @@ -738,10 +729,7 @@ open class FinTsJobExecutor( val message = messageBuilder.createInitDialogMessageWithoutStrongCustomerAuthentication(dialogContext, segmentIdForTwoStepTanProcess) getAndHandleResponseForMessage(message, dialogContext) { response -> - if (response.successful) { - updateBankData(dialogContext.bank, response) - updateCustomerData(dialogContext.bank, response) - } + updateBankAndCustomerDataIfResponseSuccessful(dialogContext, response) callback(response) } @@ -832,317 +820,24 @@ open class FinTsJobExecutor( protected open fun updateBankData(bank: BankData, response: BankResponse) { - response.getFirstSegmentById(InstituteSegmentId.BankParameters)?.let { bankParameters -> - bank.bpdVersion = bankParameters.bpdVersion - bank.bankCode = bankParameters.bankCode - bank.countryCode = bankParameters.bankCountryCode - bank.countMaxJobsPerMessage = bankParameters.countMaxJobsPerMessage - bank.supportedHbciVersions = bankParameters.supportedHbciVersions - bank.supportedLanguages = bankParameters.supportedLanguages + modelMapper.updateBankData(bank, response) + } -// bank.bic = bankParameters. // TODO: where's the BIC? - } - - response.getFirstSegmentById(InstituteSegmentId.PinInfo)?.let { pinInfo -> - bank.pinInfo = pinInfo - } - - response.getFirstSegmentById(InstituteSegmentId.TanInfo)?.let { tanInfo -> - bank.tanMethodSupportedByBank = mapToTanMethods(tanInfo) - } - - response.getFirstSegmentById(InstituteSegmentId.CommunicationInfo)?.let { communicationInfo -> - communicationInfo.parameters.firstOrNull { it.type == Kommunikationsdienst.Https }?.address?.let { address -> - bank.finTs3ServerAddress = if (address.startsWith("https://", true)) address else "https://$address" - } - } - - response.getFirstSegmentById(InstituteSegmentId.SepaAccountInfo)?.let { sepaAccountInfo -> - sepaAccountInfo.account.bic?.let { - bank.bic = it // TODO: really set BIC on bank then? - } - } - - response.getFirstSegmentById(InstituteSegmentId.SepaAccountInfo)?.let { sepaAccountInfo -> - sepaAccountInfo.account.bic?.let { - bank.bic = it // TODO: really set BIC on bank then? - } - } - - response.getFirstSegmentById(InstituteSegmentId.ChangeTanMediaParameters)?.let { parameters -> - bank.changeTanMediumParameters = parameters - } - - if (response.supportedJobs.isNotEmpty()) { - bank.supportedJobs = response.supportedJobs + protected open fun updateBankAndCustomerDataIfResponseSuccessful(dialogContext: DialogContext, response: BankResponse) { + if (response.successful) { + updateBankAndCustomerData(dialogContext.bank, response) } } - protected open fun updateCustomerData(bank: BankData, response: BankResponse) { - response.getFirstSegmentById(InstituteSegmentId.BankParameters)?.let { bankParameters -> - // TODO: ask user if there is more than one supported language? But it seems that almost all banks only support German. - if (bank.selectedLanguage == Dialogsprache.Default && bankParameters.supportedLanguages.isNotEmpty()) { - bank.selectedLanguage = bankParameters.supportedLanguages.first() - } - } + protected open fun updateBankAndCustomerData(bank: BankData, response: BankResponse) { + updateBankData(bank, response) - response.getFirstSegmentById(InstituteSegmentId.Synchronization)?.let { synchronization -> - synchronization.customerSystemId?.let { - bank.customerSystemId = it - - bank.customerSystemStatus = KundensystemStatusWerte.Benoetigt // TODO: didn't find out for sure yet, but i think i read somewhere, that this has to be set when customerSystemId is set - } - } - - response.getSegmentsById(InstituteSegmentId.AccountInfo).forEach { accountInfo -> - var accountHolderName = accountInfo.accountHolderName1 - accountInfo.accountHolderName2?.let { - accountHolderName += it // TODO: add a whitespace in between? - } - bank.customerName = accountHolderName - - findExistingAccount(bank, accountInfo)?.let { account -> - // TODO: update AccountData. But can this ever happen that an account changes? - } - ?: run { - val newAccount = AccountData(accountInfo.accountIdentifier, accountInfo.subAccountAttribute, - accountInfo.bankCountryCode, accountInfo.bankCode, accountInfo.iban, accountInfo.customerId, - mapAccountType(accountInfo), accountInfo.currency, accountHolderName, accountInfo.productName, - accountInfo.accountLimit, accountInfo.allowedJobNames) - - bank.supportedJobs.filterIsInstance().sortedByDescending { it.segmentVersion }.firstOrNull { newAccount.allowedJobNames.contains(it.jobName) }?.let { transactionsParameters -> - newAccount.countDaysForWhichTransactionsAreKept = transactionsParameters.countDaysForWhichTransactionsAreKept - } - - bank.addAccount(newAccount) - } - - // TODO: may also make use of other info - } - - response.getFirstSegmentById(InstituteSegmentId.SepaAccountInfo)?.let { sepaAccountInfo -> - // TODO: make use of information - sepaAccountInfo.account.iban?.let { - - } - } - - response.getFirstSegmentById(InstituteSegmentId.UserParameters)?.let { userParameters -> - bank.updVersion = userParameters.updVersion - - if (bank.customerName.isEmpty()) { - userParameters.username?.let { - bank.customerName = it - } - } - - // TODO: may also make use of other info - } - - response.getFirstSegmentById(InstituteSegmentId.CommunicationInfo)?.let { communicationInfo -> - if (bank.selectedLanguage != communicationInfo.defaultLanguage) { - bank.selectedLanguage = communicationInfo.defaultLanguage - } - } - - val supportedJobs = response.supportedJobs - if (supportedJobs.isNotEmpty()) { // if allowedJobsForBank is empty than bank didn't send any allowed job - for (account in bank.accounts) { - setAllowedJobsForAccount(bank, account, supportedJobs) - } - } - else if (bank.supportedJobs.isNotEmpty()) { - for (account in bank.accounts) { - if (account.allowedJobs.isEmpty()) { - setAllowedJobsForAccount(bank, account, bank.supportedJobs) - } - } - } - - if (response.supportedTanMethodsForUser.isNotEmpty()) { - bank.tanMethodsAvailableForUser = response.supportedTanMethodsForUser.mapNotNull { findTanMethod(it, bank) } - - if (bank.tanMethodsAvailableForUser.firstOrNull { it.securityFunction == bank.selectedTanMethod.securityFunction } == null) { // supportedTanMethods don't contain selectedTanMethod anymore - bank.resetSelectedTanMethod() - } - } - } - - protected open fun findTanMethod(securityFunction: Sicherheitsfunktion, bank: BankData): TanMethod? { - return bank.tanMethodSupportedByBank.firstOrNull { it.securityFunction == securityFunction } - } - - protected open fun setAllowedJobsForAccount(bank: BankData, account: AccountData, supportedJobs: List) { - val allowedJobsForAccount = mutableListOf() - - for (job in supportedJobs) { - if (isJobSupported(account, job)) { - allowedJobsForAccount.add(job) - } - } - - account.allowedJobs = allowedJobsForAccount - - account.setSupportsFeature(AccountFeature.RetrieveAccountTransactions, messageBuilder.supportsGetTransactions(account)) - account.setSupportsFeature(AccountFeature.RetrieveBalance, messageBuilder.supportsGetBalance(account)) - account.setSupportsFeature(AccountFeature.TransferMoney, messageBuilder.supportsBankTransfer(bank, account)) - account.setSupportsFeature(AccountFeature.RealTimeTransfer, messageBuilder.supportsSepaRealTimeTransfer(bank, account)) - } - - protected open fun mapToTanMethods(tanInfo: TanInfo): List { - return tanInfo.tanProcedureParameters.methodParameters.mapNotNull { - mapToTanMethod(it) - } - } - - protected open fun mapToTanMethod(parameters: TanMethodParameters): TanMethod? { - val methodName = parameters.methodName - - // we filter out iTAN and Einschritt-Verfahren as they are not permitted anymore according to PSD2 - if (methodName.toLowerCase() == "itan") { - return null - } - - return TanMethod(methodName, parameters.securityFunction, - mapToTanMethodType(parameters) ?: TanMethodType.EnterTan, mapHhdVersion(parameters), - parameters.maxTanInputLength, parameters.allowedTanFormat, - parameters.nameOfTanMediumRequired == BezeichnungDesTanMediumsErforderlich.BezeichnungDesTanMediumsMussAngegebenWerden, - mapDecoupledTanMethodParameters(parameters)) - } - - protected open fun mapToTanMethodType(parameters: TanMethodParameters): TanMethodType? { - val name = parameters.methodName.toLowerCase() - - return when { - // names are like 'chipTAN (comfort) manuell', 'Smart(-)TAN plus (manuell)' and - // technical identification is 'HHD'. Exception: there's one that states itself as 'chipTAN (Manuell)' - // but its DkTanMethod is set to 'HHDOPT1' -> handle ChipTanManuell before ChipTanFlickercode - parameters.dkTanMethod == DkTanMethod.HHD || name.contains("manuell") -> - TanMethodType.ChipTanManuell - - // names are like 'chipTAN optisch/comfort', 'SmartTAN (plus) optic/USB', 'chipTAN (Flicker)' and - // technical identification is 'HHDOPT1' - parameters.dkTanMethod == DkTanMethod.HHDOPT1 || - tanMethodNameContains(name, "optisch", "optic", "comfort", "flicker") -> - TanMethodType.ChipTanFlickercode - - // 'Smart-TAN plus optisch / USB' seems to be a Flickertan method -> test for 'optisch' first - name.contains("usb") -> TanMethodType.ChipTanUsb - - // QRTAN+ from 1822 direct has nothing to do with chipTAN QR. - name.contains("qr") -> { - if (tanMethodNameContains(name, "chipTAN", "Smart")) TanMethodType.ChipTanQrCode - else TanMethodType.QrCode - } - - // photoTAN from Commerzbank (comdirect), Deutsche Bank, norisbank has nothing to do with chipTAN photo - name.contains("photo") -> { - // e.g. 'Smart-TAN photo' / description 'Challenge' - if (tanMethodNameContains(name, "chipTAN", "Smart")) TanMethodType.ChipTanPhotoTanMatrixCode - // e.g. 'photoTAN-Verfahren', description 'Freigabe durch photoTAN' - else TanMethodType.photoTan - } - - tanMethodNameContains(name, "SMS", "mobile", "mTAN") -> TanMethodType.SmsTan - - // 'flateXSecure' identifies itself as 'PPTAN' instead of 'AppTAN' - // 'activeTAN-Verfahren' can actually be used either with an app or a reader; it's like chipTAN QR but without a chip card - parameters.dkTanMethod == DkTanMethod.App - || tanMethodNameContains(name, "push", "app", "BestSign", "SecureGo", "TAN2go", "activeTAN", "easyTAN", "SecurePlus", "TAN+") - || technicalTanMethodIdentificationContains(parameters, "SECURESIGN", "PPTAN") -> - TanMethodType.AppTan - - // we filter out iTAN and Einschritt-Verfahren as they are not permitted anymore according to PSD2 - else -> null - } - } - - protected open fun mapHhdVersion(parameters: TanMethodParameters): HHDVersion? { - return when { - technicalTanMethodIdentificationContains(parameters, "HHD1.4") -> HHDVersion.HHD_1_4 - technicalTanMethodIdentificationContains(parameters, "HHD1.3") -> HHDVersion.HHD_1_3 - parameters.versionDkTanMethod?.contains("1.4") == true -> HHDVersion.HHD_1_4 - parameters.versionDkTanMethod?.contains("1.3") == true -> HHDVersion.HHD_1_4 - else -> null - } - } - - protected open fun tanMethodNameContains(name: String, vararg namesToTest: String): Boolean { - namesToTest.forEach { nameToTest -> - if (name.contains(nameToTest.toLowerCase())) { - return true - } - } - - return false - } - - protected open fun technicalTanMethodIdentificationContains(parameters: TanMethodParameters, vararg valuesToTest: String): Boolean { - valuesToTest.forEach { valueToTest -> - if (parameters.technicalTanMethodIdentification.contains(valueToTest, true)) { - return true - } - } - - return false - } - - protected open fun mapDecoupledTanMethodParameters(parameters: TanMethodParameters): DecoupledTanMethodParameters? { - parameters.manualConfirmationAllowedForDecoupled?.let { manualConfirmationAllowed -> - return DecoupledTanMethodParameters( - manualConfirmationAllowed, - parameters.periodicStateRequestsAllowedForDecoupled ?: false, // this and the following values are all set when manualConfirmationAllowedForDecoupled is set - parameters.maxNumberOfStateRequestsForDecoupled ?: 0, - parameters.initialDelayInSecondsForStateRequestsForDecoupled ?: Int.MAX_VALUE, - parameters.delayInSecondsForNextStateRequestsForDecoupled ?: Int.MAX_VALUE - ) - } - - return null + modelMapper.updateCustomerData(bank, response) } open fun isJobSupported(bank: BankData, segmentId: ISegmentId): Boolean { - return bank.supportedJobs.map { it.jobName }.contains(segmentId.id) - } - - open fun isJobSupported(account: AccountData, supportedJob: JobParameters): Boolean { - for (allowedJobName in account.allowedJobNames) { - if (allowedJobName == supportedJob.jobName) { - return true - } - } - - return false - } - - protected open fun findExistingAccount(bank: BankData, accountInfo: AccountInfo): AccountData? { - bank.accounts.forEach { account -> - if (account.accountIdentifier == accountInfo.accountIdentifier - && account.productName == accountInfo.productName) { - - return account - } - } - - return null - } - - protected open fun mapAccountType(accountInfo: AccountInfo): AccountType? { - if (accountInfo.accountType == null || accountInfo.accountType == AccountType.Sonstige) { - accountInfo.productName?.let { name -> - // comdirect doesn't set account type field but names its bank accounts according to them like 'Girokonto', 'Tagesgeldkonto', ... - return when { - name.contains("Girokonto", true) -> AccountType.Girokonto - name.contains("Festgeld", true) -> AccountType.Festgeldkonto - name.contains("Tagesgeld", true) -> AccountType.Sparkonto // learnt something new today: according to Wikipedia some direct banks offer a modern version of saving accounts as 'Tagesgeldkonto' - name.contains("Kreditkarte", true) -> AccountType.Kreditkartenkonto - else -> accountInfo.accountType - } - } - } - - return accountInfo.accountType + return modelMapper.isJobSupported(bank, segmentId) } diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/mapper/ModelMapper.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/mapper/ModelMapper.kt new file mode 100644 index 00000000..a783ec14 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/mapper/ModelMapper.kt @@ -0,0 +1,335 @@ +package net.dankito.banking.fints.model.mapper + +import net.dankito.banking.fints.messages.MessageBuilder +import net.dankito.banking.fints.messages.datenelemente.implementierte.Dialogsprache +import net.dankito.banking.fints.messages.datenelemente.implementierte.KundensystemStatusWerte +import net.dankito.banking.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion +import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.BezeichnungDesTanMediumsErforderlich +import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.DkTanMethod +import net.dankito.banking.fints.messages.segmente.id.ISegmentId +import net.dankito.banking.fints.model.* +import net.dankito.banking.fints.response.BankResponse +import net.dankito.banking.fints.response.InstituteSegmentId +import net.dankito.banking.fints.response.segments.* + + +open class ModelMapper( + protected open val messageBuilder: MessageBuilder // TODO: may extract class that contains common methods of ModelMapper and MessageBuilder +) { + + + open fun updateBankData(bank: BankData, response: BankResponse) { + response.getFirstSegmentById(InstituteSegmentId.BankParameters)?.let { bankParameters -> + bank.bpdVersion = bankParameters.bpdVersion + bank.bankCode = bankParameters.bankCode + bank.countryCode = bankParameters.bankCountryCode + bank.countMaxJobsPerMessage = bankParameters.countMaxJobsPerMessage + bank.supportedHbciVersions = bankParameters.supportedHbciVersions + bank.supportedLanguages = bankParameters.supportedLanguages + +// bank.bic = bankParameters. // TODO: where's the BIC? + } + + response.getFirstSegmentById(InstituteSegmentId.PinInfo)?.let { pinInfo -> + bank.pinInfo = pinInfo + } + + response.getFirstSegmentById(InstituteSegmentId.TanInfo)?.let { tanInfo -> + bank.tanMethodSupportedByBank = mapToTanMethods(tanInfo) + } + + response.getFirstSegmentById(InstituteSegmentId.CommunicationInfo)?.let { communicationInfo -> + communicationInfo.parameters.firstOrNull { it.type == Kommunikationsdienst.Https }?.address?.let { address -> + bank.finTs3ServerAddress = if (address.startsWith("https://", true)) address else "https://$address" + } + } + + response.getFirstSegmentById(InstituteSegmentId.SepaAccountInfo)?.let { sepaAccountInfo -> + sepaAccountInfo.account.bic?.let { + bank.bic = it // TODO: really set BIC on bank then? + } + } + + response.getFirstSegmentById(InstituteSegmentId.SepaAccountInfo)?.let { sepaAccountInfo -> + sepaAccountInfo.account.bic?.let { + bank.bic = it // TODO: really set BIC on bank then? + } + } + + response.getFirstSegmentById(InstituteSegmentId.ChangeTanMediaParameters)?.let { parameters -> + bank.changeTanMediumParameters = parameters + } + + if (response.supportedJobs.isNotEmpty()) { + bank.supportedJobs = response.supportedJobs + } + } + + open fun updateCustomerData(bank: BankData, response: BankResponse) { + response.getFirstSegmentById(InstituteSegmentId.BankParameters)?.let { bankParameters -> + // TODO: ask user if there is more than one supported language? But it seems that almost all banks only support German. + if (bank.selectedLanguage == Dialogsprache.Default && bankParameters.supportedLanguages.isNotEmpty()) { + bank.selectedLanguage = bankParameters.supportedLanguages.first() + } + } + + response.getFirstSegmentById(InstituteSegmentId.Synchronization)?.let { synchronization -> + synchronization.customerSystemId?.let { + bank.customerSystemId = it + + bank.customerSystemStatus = KundensystemStatusWerte.Benoetigt // TODO: didn't find out for sure yet, but i think i read somewhere, that this has to be set when customerSystemId is set + } + } + + response.getSegmentsById(InstituteSegmentId.AccountInfo).forEach { accountInfo -> + var accountHolderName = accountInfo.accountHolderName1 + accountInfo.accountHolderName2?.let { + accountHolderName += it // TODO: add a whitespace in between? + } + bank.customerName = accountHolderName + + findExistingAccount(bank, accountInfo)?.let { account -> + // TODO: update AccountData. But can this ever happen that an account changes? + } + ?: run { + val newAccount = AccountData(accountInfo.accountIdentifier, accountInfo.subAccountAttribute, + accountInfo.bankCountryCode, accountInfo.bankCode, accountInfo.iban, accountInfo.customerId, + mapAccountType(accountInfo), accountInfo.currency, accountHolderName, accountInfo.productName, + accountInfo.accountLimit, accountInfo.allowedJobNames) + + bank.supportedJobs.filterIsInstance().sortedByDescending { it.segmentVersion }.firstOrNull { newAccount.allowedJobNames.contains(it.jobName) }?.let { transactionsParameters -> + newAccount.countDaysForWhichTransactionsAreKept = transactionsParameters.countDaysForWhichTransactionsAreKept + } + + bank.addAccount(newAccount) + } + + // TODO: may also make use of other info + } + + response.getFirstSegmentById(InstituteSegmentId.SepaAccountInfo)?.let { sepaAccountInfo -> + // TODO: make use of information + sepaAccountInfo.account.iban?.let { + + } + } + + response.getFirstSegmentById(InstituteSegmentId.UserParameters)?.let { userParameters -> + bank.updVersion = userParameters.updVersion + + if (bank.customerName.isEmpty()) { + userParameters.username?.let { + bank.customerName = it + } + } + + // TODO: may also make use of other info + } + + response.getFirstSegmentById(InstituteSegmentId.CommunicationInfo)?.let { communicationInfo -> + if (bank.selectedLanguage != communicationInfo.defaultLanguage) { + bank.selectedLanguage = communicationInfo.defaultLanguage + } + } + + val supportedJobs = response.supportedJobs + if (supportedJobs.isNotEmpty()) { // if allowedJobsForBank is empty than bank didn't send any allowed job + for (account in bank.accounts) { + setAllowedJobsForAccount(bank, account, supportedJobs) + } + } + else if (bank.supportedJobs.isNotEmpty()) { + for (account in bank.accounts) { + if (account.allowedJobs.isEmpty()) { + setAllowedJobsForAccount(bank, account, bank.supportedJobs) + } + } + } + + if (response.supportedTanMethodsForUser.isNotEmpty()) { + bank.tanMethodsAvailableForUser = response.supportedTanMethodsForUser.mapNotNull { findTanMethod(it, bank) } + + if (bank.tanMethodsAvailableForUser.firstOrNull { it.securityFunction == bank.selectedTanMethod.securityFunction } == null) { // supportedTanMethods don't contain selectedTanMethod anymore + bank.resetSelectedTanMethod() + } + } + } + + protected open fun findTanMethod(securityFunction: Sicherheitsfunktion, bank: BankData): TanMethod? { + return bank.tanMethodSupportedByBank.firstOrNull { it.securityFunction == securityFunction } + } + + protected open fun setAllowedJobsForAccount(bank: BankData, account: AccountData, supportedJobs: List) { + val allowedJobsForAccount = mutableListOf() + + for (job in supportedJobs) { + if (isJobSupported(account, job)) { + allowedJobsForAccount.add(job) + } + } + + account.allowedJobs = allowedJobsForAccount + + account.setSupportsFeature(AccountFeature.RetrieveAccountTransactions, messageBuilder.supportsGetTransactions(account)) + account.setSupportsFeature(AccountFeature.RetrieveBalance, messageBuilder.supportsGetBalance(account)) + account.setSupportsFeature(AccountFeature.TransferMoney, messageBuilder.supportsBankTransfer(bank, account)) + account.setSupportsFeature(AccountFeature.RealTimeTransfer, messageBuilder.supportsSepaRealTimeTransfer(bank, account)) + } + + protected open fun mapToTanMethods(tanInfo: TanInfo): List { + return tanInfo.tanProcedureParameters.methodParameters.mapNotNull { + mapToTanMethod(it) + } + } + + protected open fun mapToTanMethod(parameters: TanMethodParameters): TanMethod? { + val methodName = parameters.methodName + + // we filter out iTAN and Einschritt-Verfahren as they are not permitted anymore according to PSD2 + if (methodName.toLowerCase() == "itan") { + return null + } + + return TanMethod(methodName, parameters.securityFunction, + mapToTanMethodType(parameters) ?: TanMethodType.EnterTan, mapHhdVersion(parameters), + parameters.maxTanInputLength, parameters.allowedTanFormat, + parameters.nameOfTanMediumRequired == BezeichnungDesTanMediumsErforderlich.BezeichnungDesTanMediumsMussAngegebenWerden, + mapDecoupledTanMethodParameters(parameters)) + } + + protected open fun mapToTanMethodType(parameters: TanMethodParameters): TanMethodType? { + val name = parameters.methodName.toLowerCase() + + return when { + // names are like 'chipTAN (comfort) manuell', 'Smart(-)TAN plus (manuell)' and + // technical identification is 'HHD'. Exception: there's one that states itself as 'chipTAN (Manuell)' + // but its DkTanMethod is set to 'HHDOPT1' -> handle ChipTanManuell before ChipTanFlickercode + parameters.dkTanMethod == DkTanMethod.HHD || name.contains("manuell") -> + TanMethodType.ChipTanManuell + + // names are like 'chipTAN optisch/comfort', 'SmartTAN (plus) optic/USB', 'chipTAN (Flicker)' and + // technical identification is 'HHDOPT1' + parameters.dkTanMethod == DkTanMethod.HHDOPT1 || + tanMethodNameContains(name, "optisch", "optic", "comfort", "flicker") -> + TanMethodType.ChipTanFlickercode + + // 'Smart-TAN plus optisch / USB' seems to be a Flickertan method -> test for 'optisch' first + name.contains("usb") -> TanMethodType.ChipTanUsb + + // QRTAN+ from 1822 direct has nothing to do with chipTAN QR. + name.contains("qr") -> { + if (tanMethodNameContains(name, "chipTAN", "Smart")) TanMethodType.ChipTanQrCode + else TanMethodType.QrCode + } + + // photoTAN from Commerzbank (comdirect), Deutsche Bank, norisbank has nothing to do with chipTAN photo + name.contains("photo") -> { + // e.g. 'Smart-TAN photo' / description 'Challenge' + if (tanMethodNameContains(name, "chipTAN", "Smart")) TanMethodType.ChipTanPhotoTanMatrixCode + // e.g. 'photoTAN-Verfahren', description 'Freigabe durch photoTAN' + else TanMethodType.photoTan + } + + tanMethodNameContains(name, "SMS", "mobile", "mTAN") -> TanMethodType.SmsTan + + // 'flateXSecure' identifies itself as 'PPTAN' instead of 'AppTAN' + // 'activeTAN-Verfahren' can actually be used either with an app or a reader; it's like chipTAN QR but without a chip card + parameters.dkTanMethod == DkTanMethod.App + || tanMethodNameContains(name, "push", "app", "BestSign", "SecureGo", "TAN2go", "activeTAN", "easyTAN", "SecurePlus", "TAN+") + || technicalTanMethodIdentificationContains(parameters, "SECURESIGN", "PPTAN") -> + TanMethodType.AppTan + + // we filter out iTAN and Einschritt-Verfahren as they are not permitted anymore according to PSD2 + else -> null + } + } + + protected open fun mapHhdVersion(parameters: TanMethodParameters): HHDVersion? { + return when { + technicalTanMethodIdentificationContains(parameters, "HHD1.4") -> HHDVersion.HHD_1_4 + technicalTanMethodIdentificationContains(parameters, "HHD1.3") -> HHDVersion.HHD_1_3 + parameters.versionDkTanMethod?.contains("1.4") == true -> HHDVersion.HHD_1_4 + parameters.versionDkTanMethod?.contains("1.3") == true -> HHDVersion.HHD_1_4 + else -> null + } + } + + protected open fun tanMethodNameContains(name: String, vararg namesToTest: String): Boolean { + namesToTest.forEach { nameToTest -> + if (name.contains(nameToTest.toLowerCase())) { + return true + } + } + + return false + } + + protected open fun technicalTanMethodIdentificationContains(parameters: TanMethodParameters, vararg valuesToTest: String): Boolean { + valuesToTest.forEach { valueToTest -> + if (parameters.technicalTanMethodIdentification.contains(valueToTest, true)) { + return true + } + } + + return false + } + + protected open fun mapDecoupledTanMethodParameters(parameters: TanMethodParameters): DecoupledTanMethodParameters? { + parameters.manualConfirmationAllowedForDecoupled?.let { manualConfirmationAllowed -> + return DecoupledTanMethodParameters( + manualConfirmationAllowed, + parameters.periodicStateRequestsAllowedForDecoupled ?: false, // this and the following values are all set when manualConfirmationAllowedForDecoupled is set + parameters.maxNumberOfStateRequestsForDecoupled ?: 0, + parameters.initialDelayInSecondsForStateRequestsForDecoupled ?: Int.MAX_VALUE, + parameters.delayInSecondsForNextStateRequestsForDecoupled ?: Int.MAX_VALUE + ) + } + + return null + } + + + open fun isJobSupported(bank: BankData, segmentId: ISegmentId): Boolean { + return bank.supportedJobs.map { it.jobName }.contains(segmentId.id) + } + + open fun isJobSupported(account: AccountData, supportedJob: JobParameters): Boolean { + for (allowedJobName in account.allowedJobNames) { + if (allowedJobName == supportedJob.jobName) { + return true + } + } + + return false + } + + protected open fun findExistingAccount(bank: BankData, accountInfo: AccountInfo): AccountData? { + bank.accounts.forEach { account -> + if (account.accountIdentifier == accountInfo.accountIdentifier + && account.productName == accountInfo.productName) { + + return account + } + } + + return null + } + + protected open fun mapAccountType(accountInfo: AccountInfo): AccountType? { + if (accountInfo.accountType == null || accountInfo.accountType == AccountType.Sonstige) { + accountInfo.productName?.let { name -> + // comdirect doesn't set account type field but names its bank accounts according to them like 'Girokonto', 'Tagesgeldkonto', ... + return when { + name.contains("Girokonto", true) -> AccountType.Girokonto + name.contains("Festgeld", true) -> AccountType.Festgeldkonto + name.contains("Tagesgeld", true) -> AccountType.Sparkonto // learnt something new today: according to Wikipedia some direct banks offer a modern version of saving accounts as 'Tagesgeldkonto' + name.contains("Kreditkarte", true) -> AccountType.Kreditkartenkonto + else -> accountInfo.accountType + } + } + } + + return accountInfo.accountType + } + +} \ No newline at end of file diff --git a/fints4k/src/jvm6Test/kotlin/net/dankito/banking/fints/bankdetails/BanksFinTsDetailsRetriever.kt b/fints4k/src/jvm6Test/kotlin/net/dankito/banking/fints/bankdetails/BanksFinTsDetailsRetriever.kt index f7377c21..57b608b3 100644 --- a/fints4k/src/jvm6Test/kotlin/net/dankito/banking/fints/bankdetails/BanksFinTsDetailsRetriever.kt +++ b/fints4k/src/jvm6Test/kotlin/net/dankito/banking/fints/bankdetails/BanksFinTsDetailsRetriever.kt @@ -14,6 +14,7 @@ import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.SmsAb import net.dankito.banking.fints.model.* import net.dankito.banking.bankfinder.BankInfo import net.dankito.banking.fints.FinTsJobExecutor +import net.dankito.banking.fints.model.mapper.ModelMapper import net.dankito.banking.fints.response.BankResponse import net.dankito.banking.fints.response.segments.SepaAccountInfoParameters import net.dankito.banking.fints.response.segments.TanInfo @@ -50,19 +51,19 @@ class BanksFinTsDetailsRetriever { private val messageBuilder = MessageBuilder() - private val jobExecutor = object : FinTsJobExecutor(NoOpFinTsClientCallback()) { - - fun getAndHandleResponseForMessagePublic(message: MessageBuilderResult, dialogContext: DialogContext, callback: (BankResponse) -> Unit) { - getAndHandleResponseForMessage(message, dialogContext, callback) - } - - fun updateBankDataPublic(bank: BankData, response: BankResponse) { - super.updateBankData(bank, response) - } + private val modelMapper = object : ModelMapper(messageBuilder) { fun mapToTanMethodTypePublic(parameters: TanMethodParameters): TanMethodType? { return super.mapToTanMethodType(parameters) } + + } + + private val jobExecutor = object : FinTsJobExecutor(NoOpFinTsClientCallback(), modelMapper = modelMapper) { + + fun getAndHandleResponseForMessagePublic(message: MessageBuilderResult, dialogContext: DialogContext, callback: (BankResponse) -> Unit) { + getAndHandleResponseForMessage(message, dialogContext, callback) + } } @@ -140,7 +141,7 @@ class BanksFinTsDetailsRetriever { countDownLatch.await(30, TimeUnit.SECONDS) - jobExecutor.updateBankDataPublic(bank, anonymousBankInfoResponse.get()) + modelMapper.updateBankData(bank, anonymousBankInfoResponse.get()) return anonymousBankInfoResponse.get() } @@ -212,7 +213,7 @@ class BanksFinTsDetailsRetriever { tanMethodParameter[methodParameter.methodName]?.add(methodParameter) } - val tanMethodType = jobExecutor.mapToTanMethodTypePublic(methodParameter) + val tanMethodType = modelMapper.mapToTanMethodTypePublic(methodParameter) if (tanMethodTypes.containsKey(tanMethodType) == false) { tanMethodTypes.put(tanMethodType, mutableSetOf(methodParameter)) }