BankingClient/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/fints4kBankingClient.kt

283 lines
12 KiB
Kotlin
Raw Normal View History

package net.dankito.banking
import net.dankito.banking.ui.BankingClientCallback
import net.dankito.banking.ui.IBankingClient
import net.dankito.banking.ui.model.parameters.GetTransactionsParameter
import net.dankito.banking.ui.model.parameters.TransferMoneyData
import net.dankito.banking.ui.model.responses.AddAccountResponse
import net.dankito.banking.ui.model.responses.BankingClientResponse
import net.dankito.banking.ui.model.responses.GetTransactionsResponse
import net.dankito.banking.fints.FinTsClientForCustomer
import net.dankito.banking.fints.callback.FinTsClientCallback
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
import net.dankito.banking.fints.model.*
import net.dankito.banking.fints.util.IBase64Service
import net.dankito.banking.fints.util.PureKotlinBase64Service
import net.dankito.banking.fints.webclient.IWebClient
import net.dankito.banking.fints.webclient.KtorWebClient
import net.dankito.banking.extensions.toMoney
2020-09-22 04:06:11 +00:00
import net.dankito.banking.fints.model.BankData
import net.dankito.banking.fints.model.MessageLogEntryType
import net.dankito.banking.fints.response.client.FinTsClientResponse
import net.dankito.banking.ui.model.*
import net.dankito.banking.ui.model.MessageLogEntry
import net.dankito.banking.ui.model.mapper.IModelCreator
import net.dankito.banking.util.ISerializer
import net.dankito.utils.multiplatform.File
import net.dankito.utils.multiplatform.log.LoggerFactory
open class fints4kBankingClient(
2020-09-22 04:06:11 +00:00
protected val bank: TypedBankData,
protected val modelCreator: IModelCreator,
protected val dataFolder: File,
protected val serializer: ISerializer,
webClient: IWebClient = KtorWebClient(),
base64Service: IBase64Service = PureKotlinBase64Service(),
callback: BankingClientCallback
) : IBankingClient {
companion object {
val fints4kClientDataFilename = "fints4kClientData.json"
private val log = LoggerFactory.getLogger(fints4kBankingClient::class)
}
protected val mapper = net.dankito.banking.mapper.fints4kModelMapper(modelCreator)
2020-09-22 04:06:11 +00:00
protected val fintsBank = restoreDataOrMapFromUiModel(bank)
2020-09-22 04:06:11 +00:00
protected open val client = FinTsClientForCustomer(fintsBank, createFinTsClientCallback(callback), webClient, base64Service)
2020-05-16 20:51:51 +00:00
override val messageLogWithoutSensitiveData: List<MessageLogEntry>
get() = client.messageLogWithoutSensitiveData.map { MessageLogEntry(it.message, map(it.type), it.time, bank) }
protected open fun map(type: MessageLogEntryType): net.dankito.banking.ui.model.MessageLogEntryType {
return when (type) {
MessageLogEntryType.Sent -> net.dankito.banking.ui.model.MessageLogEntryType.Sent
MessageLogEntryType.Received -> net.dankito.banking.ui.model.MessageLogEntryType.Received
}
}
2020-05-16 20:51:51 +00:00
override fun addAccountAsync(callback: (AddAccountResponse) -> Unit) {
addAccountAsync(AddAccountParameter(fintsBank), callback)
}
protected open fun addAccountAsync(parameter: AddAccountParameter, callback: (AddAccountResponse) -> Unit) {
client.addAccountAsync(parameter) { response ->
handleAddAccountResponse(response, callback)
}
}
protected open fun handleAddAccountResponse(response: net.dankito.banking.fints.response.client.AddAccountResponse,
callback: (AddAccountResponse) -> Unit) {
if (response.successful) { // if fintsBank couldn't be restored and then an error occurs, e.g. no network connection, then fintsBank contains almost no data which then gets mapped to bank -> accounts, TAN methods, TAN procedures, ... are lost
mapper.mapBank(bank, fintsBank)
}
2020-09-22 04:06:11 +00:00
val mappedResponse = mapper.mapResponse(bank, response)
saveData()
callback(mappedResponse)
}
override fun getTransactionsAsync(parameter: GetTransactionsParameter, callback: (GetTransactionsResponse) -> Unit) {
2020-09-22 04:06:11 +00:00
val account = parameter.account
findAccountForAccount(account) { accountData, response ->
2020-09-22 04:06:11 +00:00
if (accountData == null) {
if (response != null) {
callback(GetTransactionsResponse(account, response))
}
else { // should never be the case
callback(GetTransactionsResponse(account, ""))
}
}
else {
2020-09-22 04:06:11 +00:00
val mappedParameter = GetTransactionsParameter(accountData, parameter.alsoRetrieveBalance, parameter.fromDate,
parameter.toDate, null, parameter.abortIfTanIsRequired) {
2020-09-22 04:06:11 +00:00
parameter.retrievedChunkListener?.invoke(mapper.mapTransactions(account, it))
}
2020-09-22 04:06:11 +00:00
doGetTransactionsAsync(mappedParameter, account, callback)
}
}
}
protected open fun doGetTransactionsAsync(parameter: net.dankito.banking.fints.model.GetTransactionsParameter,
2020-09-22 04:06:11 +00:00
account: TypedBankAccount, callback: (GetTransactionsResponse) -> Unit) {
client.getTransactionsAsync(parameter) { response ->
2020-09-22 04:06:11 +00:00
handleGetTransactionsResponse(account, response, callback)
}
}
2020-09-22 04:06:11 +00:00
protected open fun handleGetTransactionsResponse(account: TypedBankAccount, response: net.dankito.banking.fints.response.client.GetTransactionsResponse,
callback: (GetTransactionsResponse) -> Unit) {
2020-09-22 04:06:11 +00:00
val mappedResponse = mapper.mapResponse(account, response)
saveData()
callback(mappedResponse)
}
2020-09-02 14:54:33 +00:00
override fun transferMoneyAsync(data: TransferMoneyData, callback: (BankingClientResponse) -> Unit) {
findAccountForAccount(data.account) { account, response ->
if (account == null) {
if (response != null) {
callback(response)
}
else { // should never be the case
callback(BankingClientResponse(false, "Konnte Kontodaten nicht vom Bankserver abrufen für ${data.account.identifier}. " +
"Besteht eine Netzwerkverbindung und sind der eingegebenen Benutzername und Passwort korrekt?")) // TODO: translate
}
}
else {
val mappedData = BankTransferData(data.recipientName, data.recipientAccountId, data.recipientBankCode, data.amount.toMoney(), data.reference, data.realTimeTransfer)
doBankTransferAsync(mappedData, account, callback)
}
}
}
protected open fun doBankTransferAsync(data: BankTransferData, account: AccountData, callback: (BankingClientResponse) -> Unit) {
client.doBankTransferAsync(data, account) { response ->
handleBankTransferResponse(callback, response)
}
}
protected open fun handleBankTransferResponse(callback: (BankingClientResponse) -> Unit, response: FinTsClientResponse) {
saveData()
callback(mapper.mapResponse(response))
}
2020-09-22 04:06:11 +00:00
override fun dataChanged(bank: TypedBankData) {
mapper.mapChangesFromUiToClientModel(bank, this.fintsBank)
}
2020-09-22 04:06:11 +00:00
override fun deletedBank(bank: TypedBankData, wasLastAccountWithThisCredentials: Boolean) {
if (wasLastAccountWithThisCredentials) {
2020-09-22 04:06:11 +00:00
getFints4kClientDataFile(bank).delete()
}
}
protected open fun findAccountForAccount(account: TypedBankAccount, findAccountResult: (AccountData?, BankingClientResponse?) -> Unit) {
2020-09-22 04:06:11 +00:00
val mappedAccount = mapper.findMatchingAccount(fintsBank, account)
if (mappedAccount != null) {
findAccountResult(mappedAccount, null)
}
else { // then try to get account data by fetching data from bank
addAccountAsync(AddAccountParameter(fintsBank, false)) { response ->
if (response.successful) {
findAccountResult(mapper.findMatchingAccount(fintsBank, account), response)
}
else {
findAccountResult(null, response)
}
}
}
}
2020-09-22 04:06:11 +00:00
protected open fun restoreDataOrMapFromUiModel(bank: TypedBankData): BankData {
if (isNewAccount(bank)) {
return mapToBankData(bank)
}
2020-09-22 04:06:11 +00:00
return restoreData(bank) ?: mapToBankData(bank)
}
2020-09-22 04:06:11 +00:00
protected open fun isNewAccount(bank: TypedBankData): Boolean {
return bank.accounts.isEmpty()
}
2020-09-22 04:06:11 +00:00
protected open fun mapToBankData(bank: TypedBankData): BankData {
return BankData(bank.bankCode, bank.userName, bank.password, bank.finTsServerAddress, bank.bic, bank.bankName)
}
2020-09-22 04:06:11 +00:00
protected open fun restoreData(bank: TypedBankData): BankData? {
try {
2020-09-22 04:06:11 +00:00
return serializer.deserializeObject(getFints4kClientDataFile(bank), BankData::class)
} catch (e: Exception) {
2020-09-22 04:06:11 +00:00
log.warn(e) { "Could not deserialize bank data of $bank" }
}
return null
}
protected open fun saveData() {
try {
// TODO: fix that real (child) class get serialized and re-enable again
// val clientDataFile = getFints4kClientDataFile(fintsBank.bankCode, fintsBank.customerId)
//
// serializer.serializeObject(fintsBank, clientDataFile)
} catch (e: Exception) {
log.error(e) { "Could not save bank data for $fintsBank" }
}
}
2020-09-22 04:06:11 +00:00
protected open fun getFints4kClientDataFile(bank: TypedBankData): File {
return getFints4kClientDataFile(bank.bankCode, bank.userName)
}
protected open fun getFints4kClientDataFile(bankCode: String, customerId: String): File {
val folder = File(dataFolder, "fints4k-client")
folder.mkdirs()
return File(folder, "${bankCode}_${customerId}_$fints4kClientDataFilename")
}
protected open fun createFinTsClientCallback(clientCallback: BankingClientCallback): FinTsClientCallback {
return object : FinTsClientCallback {
override fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?, callback: (TanMethod?) -> Unit) {
handleAskUserForTanMethod(supportedTanMethods, suggestedTanMethod, callback)
}
2020-09-08 12:07:17 +00:00
override fun enterTan(bank: BankData, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) {
handleEnterTan(bank, tanChallenge, callback, clientCallback)
}
2020-09-08 12:07:17 +00:00
override fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium, callback: (EnterTanGeneratorAtcResult) -> Unit) {
handleEnterTanGeneratorAtc(bank, tanMedium, callback, clientCallback)
}
}
}
protected open fun handleAskUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?, callback: (TanMethod?) -> Unit) {
// we simply return suggestedTanProcedure as even so it's not user's preferred TAN procedure she still can select it in EnterTanDialog
callback(suggestedTanMethod)
}
2020-09-08 12:07:17 +00:00
protected open fun handleEnterTan(bank: BankData, tanChallenge: TanChallenge, enterTanCallback: (EnterTanResult) -> Unit, clientCallback: BankingClientCallback) {
2020-09-22 04:06:11 +00:00
mapper.updateTanMediaAndMethods(this@fints4kBankingClient.bank, bank)
2020-09-22 04:06:11 +00:00
clientCallback.enterTan(this@fints4kBankingClient.bank, mapper.mapTanChallenge(tanChallenge)) { result ->
2020-09-08 12:07:17 +00:00
enterTanCallback(mapper.mapEnterTanResult(result, bank))
}
}
2020-09-08 12:07:17 +00:00
protected open fun handleEnterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium, enterAtcCallback: (EnterTanGeneratorAtcResult) -> Unit, clientCallback: BankingClientCallback) {
2020-09-22 04:06:11 +00:00
mapper.updateTanMediaAndMethods(this@fints4kBankingClient.bank, bank)
clientCallback.enterTanGeneratorAtc(mapper.mapTanMedium(tanMedium)) { result ->
enterAtcCallback(mapper.mapEnterTanGeneratorAtcResult(result))
}
}
}