BankingClient/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClient.kt
2024-08-14 13:29:01 +02:00

198 lines
No EOL
11 KiB
Kotlin

package net.dankito.banking.fints
import kotlinx.datetime.LocalDate
import net.dankito.banking.client.model.parameter.FinTsClientParameter
import net.dankito.banking.fints.callback.FinTsClientCallback
import net.dankito.banking.fints.model.*
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.parameter.RetrieveTransactions
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
import net.dankito.banking.client.model.response.ErrorCode
import net.dankito.banking.client.model.response.GetAccountDataResponse
import net.dankito.banking.client.model.response.TransferMoneyResponse
import net.dankito.banking.fints.mapper.FinTsModelMapper
import net.dankito.banking.fints.response.client.FinTsClientResponse
import net.dankito.banking.fints.response.client.GetAccountInfoResponse
import net.dankito.banking.fints.response.client.GetAccountTransactionsResponse
import net.dankito.banking.fints.response.segments.AccountType
import net.dankito.banking.fints.util.BicFinder
import net.dankito.banking.fints.util.FinTsServerAddressFinder
import net.dankito.banking.fints.webclient.IWebClient
import net.dankito.utils.multiplatform.extensions.minusDays
import net.dankito.utils.multiplatform.extensions.todayAtEuropeBerlin
import kotlin.jvm.JvmOverloads
open class FinTsClient @JvmOverloads constructor(
open var callback: FinTsClientCallback,
protected open val jobExecutor: FinTsJobExecutor = FinTsJobExecutor(),
protected open val finTsServerAddressFinder: FinTsServerAddressFinder = FinTsServerAddressFinder(),
protected open val product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically
) {
companion object { // TODO: use the English names
val SupportedAccountTypes = listOf(AccountType.Girokonto, AccountType.Festgeldkonto, AccountType.Kreditkartenkonto, AccountType.Sparkonto)
}
constructor(callback: FinTsClientCallback, webClient: IWebClient) : this(callback, FinTsJobExecutor(RequestExecutor(webClient = webClient))) // Swift does not support default parameter values -> create constructor overloads
protected open val mapper = FinTsModelMapper()
protected open val bicFinder = BicFinder()
open suspend fun getAccountDataAsync(bankCode: String, loginName: String, password: String): GetAccountDataResponse {
return getAccountDataAsync(GetAccountDataParameter(bankCode, loginName, password))
}
open suspend fun getAccountDataAsync(param: GetAccountDataParameter): GetAccountDataResponse {
val finTsServerAddress = finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
if (finTsServerAddress.isNullOrBlank()) {
return GetAccountDataResponse(ErrorCode.BankDoesNotSupportFinTs3, "Either bank does not support FinTS 3.0 or we don't know its FinTS server address", null, listOf())
}
val bank = mapper.mapToBankData(param, finTsServerAddress)
val accounts = param.accounts
if (accounts.isNullOrEmpty() || param.retrieveOnlyAccountInfo) { // then first retrieve customer's bank accounts
val getAccountInfoResponse = getAccountInfo(param, bank)
if (getAccountInfoResponse.successful == false || param.retrieveOnlyAccountInfo) {
return GetAccountDataResponse(mapper.mapErrorCode(getAccountInfoResponse), mapper.mapErrorMessages(getAccountInfoResponse), null,
getAccountInfoResponse.messageLogWithoutSensitiveData, bank)
} else {
return getAccountData(param, getAccountInfoResponse.bank, getAccountInfoResponse.bank.accounts, getAccountInfoResponse)
}
} else {
return getAccountData(param, bank, accounts.map { mapper.mapToAccountData(it, param) }, null)
}
}
protected open suspend fun getAccountData(param: GetAccountDataParameter, bank: BankData, accounts: List<AccountData>, previousJobResponse: FinTsClientResponse?): GetAccountDataResponse {
val retrievedTransactionsResponses = mutableListOf<GetAccountTransactionsResponse>()
val accountsSupportingRetrievingTransactions = accounts.filter { it.supportsRetrievingBalance || it.supportsRetrievingAccountTransactions }
if (accountsSupportingRetrievingTransactions.isEmpty()) {
val errorMessage = "None of the accounts ${accounts.map { it.productName }} supports retrieving balance or transactions" // TODO: translate
return GetAccountDataResponse(ErrorCode.NoneOfTheAccountsSupportsRetrievingData, errorMessage, mapper.map(bank), previousJobResponse?.messageLogWithoutSensitiveData ?: listOf(), bank)
}
accountsSupportingRetrievingTransactions.forEach { account ->
retrievedTransactionsResponses.add(getAccountData(param, bank, account))
}
val unsuccessfulJob = retrievedTransactionsResponses.firstOrNull { it.successful == false }
val errorCode = unsuccessfulJob?.let { mapper.mapErrorCode(it) }
?: if (retrievedTransactionsResponses.size < accountsSupportingRetrievingTransactions.size) ErrorCode.DidNotRetrieveAllAccountData else null
return GetAccountDataResponse(errorCode, mapper.mapErrorMessages(unsuccessfulJob), mapper.map(bank, retrievedTransactionsResponses),
mapper.mergeMessageLog(previousJobResponse, *retrievedTransactionsResponses.toTypedArray()), bank)
}
protected open suspend fun getAccountData(param: GetAccountDataParameter, bank: BankData, account: AccountData): GetAccountTransactionsResponse {
val context = JobContext(JobContextType.GetTransactions, this.callback, product, bank, account)
return jobExecutor.getTransactionsAsync(context, mapper.toGetAccountTransactionsParameter(param, bank, account))
}
open suspend fun transferMoneyAsync(bankCode: String, loginName: String, password: String, recipientName: String, recipientAccountIdentifier: String,
amount: Money, reference: String? = null): TransferMoneyResponse {
return transferMoneyAsync(TransferMoneyParameter(bankCode, loginName, password, null, recipientName, recipientAccountIdentifier, null, amount, reference))
}
open suspend fun transferMoneyAsync(param: TransferMoneyParameter): TransferMoneyResponse {
val finTsServerAddress = finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
if (finTsServerAddress.isNullOrBlank()) {
return TransferMoneyResponse(ErrorCode.BankDoesNotSupportFinTs3, "Either bank does not FinTS 3.0 or we don't know its FinTS server address", listOf(), null)
}
val recipientBankIdentifier = getRecipientBankCode(param)
if (recipientBankIdentifier == null) {
return TransferMoneyResponse(ErrorCode.CanNotDetermineBicForIban, "We can only determine recipient's BIC automatically for German IBANs. If it's a German IBAN, either we " +
"cannot extract the bank code from IBAN ${param.recipientAccountIdentifier} (fourth to twelfth position) or don't know the BIC to this bank code. Please specify recipient's IBAN explicitly.", listOf())
}
val bank = mapper.mapToBankData(param, finTsServerAddress)
val remittanceAccount = param.remittanceAccount
if (remittanceAccount == null) { // then first retrieve customer's bank accounts
val getAccountInfoResponse = getAccountInfo(param, bank)
if (getAccountInfoResponse.successful == false) {
return TransferMoneyResponse(mapper.mapErrorCode(getAccountInfoResponse), mapper.mapErrorMessages(getAccountInfoResponse),
getAccountInfoResponse.messageLogWithoutSensitiveData, bank)
} else {
return transferMoneyAsync(param, recipientBankIdentifier, getAccountInfoResponse.bank, getAccountInfoResponse.bank.accounts, getAccountInfoResponse)
}
} else {
return transferMoneyAsync(param, recipientBankIdentifier, bank, listOf(mapper.mapToAccountData(remittanceAccount, param)), null)
}
}
protected open suspend fun transferMoneyAsync(param: TransferMoneyParameter, recipientBankIdentifier: String, bank: BankData, accounts: List<AccountData>, previousJobResponse: FinTsClientResponse?): TransferMoneyResponse {
val accountsSupportingTransfer = accounts.filter { it.supportsTransferringMoney }
val accountToUse: AccountData
if (accountsSupportingTransfer.isEmpty()) {
return TransferMoneyResponse(ErrorCode.NoAccountSupportsMoneyTransfer, "None of the accounts $accounts supports money transfer", previousJobResponse?.messageLogWithoutSensitiveData ?: listOf(), bank)
} else if (accountsSupportingTransfer.size == 1) {
accountToUse = accountsSupportingTransfer.first()
} else {
val selectedAccount = param.selectAccountToUseForTransfer?.invoke(accountsSupportingTransfer)
if (selectedAccount == null) {
return TransferMoneyResponse(ErrorCode.MoreThanOneAccountSupportsMoneyTransfer, "More than one of the accounts $accountsSupportingTransfer supports money transfer, so we cannot clearly determine which one to use for this transfer", previousJobResponse?.messageLogWithoutSensitiveData ?: listOf(), bank)
}
accountToUse = selectedAccount
}
val context = JobContext(JobContextType.TransferMoney, this.callback, product, bank, accountToUse)
val response = jobExecutor.transferMoneyAsync(context, BankTransferData(param.recipientName, param.recipientAccountIdentifier, recipientBankIdentifier,
param.amount, param.reference, param.instantPayment))
return TransferMoneyResponse(mapper.mapErrorCode(response), mapper.mapErrorMessages(response), mapper.mergeMessageLog(previousJobResponse, response), bank)
}
private fun getRecipientBankCode(param: TransferMoneyParameter): String? {
param.recipientBankIdentifier?.let { return it }
val probablyIban = param.recipientAccountIdentifier.replace(" ", "")
if (probablyIban.length > 12) {
val bankCode = probablyIban.substring(4, 4 + 8) // extract bank code from IBAN. For German IBAN bank code starts at fourth position and has 8 digits
bicFinder.findBic(bankCode)?.let { return it }
}
return null
}
protected open suspend fun getAccountInfo(param: FinTsClientParameter, bank: BankData): GetAccountInfoResponse {
param.finTsModel?.let {
// TODO: implement
// return GetAccountInfoResponse(it)
}
val context = JobContext(JobContextType.AddAccount, this.callback, product, bank) // TODO: add / change JobContextType
/* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */
val newUserInfoResponse = jobExecutor.retrieveBasicDataLikeUsersTanMethods(context, param.preferredTanMethods, param.preferredTanMedium)
if (newUserInfoResponse.successful == false) { // bank parameter (FinTS server address, ...) already seem to be wrong
return GetAccountInfoResponse(context, newUserInfoResponse)
}
/* Second dialog, executed in retrieveBasicDataLikeUsersTanMethods() if required: some banks require that in order to initialize a dialog with
strong customer authorization TAN media is required */
val getAccountsResponse = jobExecutor.getAccounts(context)
return GetAccountInfoResponse(context, getAccountsResponse)
}
}