Implemented new simplified data model in FinTsClient.getAccountData()

This commit is contained in:
dankito 2022-02-20 23:18:40 +01:00
parent b74b165974
commit 52de5a2956
29 changed files with 696 additions and 77 deletions

View File

@ -48,8 +48,8 @@ class FirstFragment : Fragment() {
// TODO: set your credentials here
presenter.retrieveAccountData("", "", "", "") { response ->
if (response.successful) {
accountTransactionsAdapter.items = response.retrievedData.flatMap { it.bookedTransactions }
response.customerAccount?.let { customer ->
accountTransactionsAdapter.items = customer.accounts.flatMap { it.bookedTransactions }
}
}
}

View File

@ -5,11 +5,11 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDate
import net.dankito.banking.fints.FinTsClientDeprecated
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.response.GetAccountDataResponse
import net.dankito.banking.fints.FinTsClient
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
import net.dankito.banking.fints.model.AddAccountParameter
import net.dankito.banking.fints.model.TanChallenge
import net.dankito.banking.fints.response.client.AddAccountResponse
import net.dankito.utils.multiplatform.extensions.millisSinceEpochAtSystemDefaultTimeZone
import org.slf4j.LoggerFactory
import java.math.BigDecimal
@ -24,7 +24,7 @@ open class Presenter {
private val log = LoggerFactory.getLogger(Presenter::class.java)
}
private val fintsClient = FinTsClientDeprecated(SimpleFinTsClientCallback { challenge -> enterTan(challenge) })
private val fintsClient = FinTsClient(SimpleFinTsClientCallback { challenge -> enterTan(challenge) })
open var enterTanCallback: ((TanChallenge) -> Unit)? = null
@ -34,10 +34,10 @@ open class Presenter {
open fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String, retrievedResult: (AddAccountResponse) -> Unit) {
open fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String, retrievedResult: (GetAccountDataResponse) -> Unit) {
GlobalScope.launch(Dispatchers.IO) {
val response = fintsClient.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress))
log.info("Retrieved response from ${response.bank.bankName} for ${response.bank.customerName}")
val response = fintsClient.getAccountData(GetAccountDataParameter(bankCode, customerId, pin, finTs3ServerAddress))
log.info("Retrieved response from ${response.customerAccount?.bankName} for ${response.customerAccount?.customerName}")
withContext(Dispatchers.Main) {
retrievedResult(response)

View File

@ -4,7 +4,7 @@ import android.view.View
import net.codinux.banking.fints4k.android.Presenter
import net.codinux.banking.fints4k.android.R
import net.codinux.banking.fints4k.android.adapter.viewholder.AccountTransactionsViewHolder
import net.dankito.banking.fints.model.AccountTransaction
import net.dankito.banking.client.model.AccountTransaction
import net.dankito.banking.fints.util.toBigDecimal
import net.dankito.utils.android.extensions.setTextColorToColorResource
import net.dankito.utils.android.ui.adapter.ListRecyclerAdapter

View File

@ -1,6 +1,7 @@
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.dankito.banking.fints.model.*
import net.dankito.banking.client.model.AccountTransaction
import net.dankito.banking.fints.model.TanChallenge
import react.RBuilder
import react.RComponent
import react.Props
@ -27,10 +28,10 @@ class AccountTransactionsView(props: AccountTransactionsViewProps) : RComponent<
// TODO: set your credentials here
GlobalScope.launch {
props.presenter.retrieveAccountData("", "", "", "") { response ->
if (response.successful) {
val balance = response.retrievedData.sumOf { it.balance?.amount?.string?.replace(',', '.')?.toDoubleOrNull() ?: 0.0 } // i know, double is not an appropriate data type for amounts
response.customerAccount?.let { customer ->
val balance = customer.accounts.sumOf { it.balance?.amount?.string?.replace(',', '.')?.toDoubleOrNull() ?: 0.0 } // i know, double is not an appropriate data type for amounts
setState(AccountTransactionsViewState(balance.toString() + " " + (response.retrievedData.firstOrNull()?.balance?.currency ?: ""), response.retrievedData.flatMap { it.bookedTransactions }, state.enterTanChallenge))
setState(AccountTransactionsViewState(balance.toString() + " " + (customer.accounts.firstOrNull()?.balance?.currency ?: ""), customer.accounts.flatMap { it.bookedTransactions }, state.enterTanChallenge))
}
}
}

View File

@ -2,11 +2,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.dankito.banking.fints.FinTsClientDeprecated
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.response.GetAccountDataResponse
import net.dankito.banking.fints.FinTsClient
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
import net.dankito.banking.fints.model.AddAccountParameter
import net.dankito.banking.fints.model.TanChallenge
import net.dankito.banking.fints.response.client.AddAccountResponse
import net.dankito.banking.fints.model.TanMethod
import net.dankito.banking.fints.model.TanMethodType
import net.dankito.banking.fints.webclient.KtorWebClient
import net.dankito.banking.fints.webclient.ProxyingWebClient
import net.dankito.utils.multiplatform.log.LoggerFactory
@ -20,7 +22,7 @@ open class Presenter {
// to circumvent CORS we have to use a CORS proxy like the SampleApplications.CorsProxy Application.kt or
// https://github.com/Rob--W/cors-anywhere. Set CORS proxy's URL here
protected open val fintsClient = FinTsClientDeprecated(SimpleFinTsClientCallback { challenge -> enterTan(challenge) },
protected open val fintsClient = FinTsClient(SimpleFinTsClientCallback { challenge -> enterTan(challenge) },
ProxyingWebClient("http://localhost:8082/", KtorWebClient()))
open var enterTanCallback: ((TanChallenge) -> Unit)? = null
@ -30,11 +32,11 @@ open class Presenter {
}
open fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String, retrievedResult: (AddAccountResponse) -> Unit) {
open fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String, retrievedResult: (GetAccountDataResponse) -> Unit) {
GlobalScope.launch(Dispatchers.Unconfined) {
val response = fintsClient.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress))
val response = fintsClient.getAccountData(GetAccountDataParameter(bankCode, customerId, pin, finTs3ServerAddress))
log.info("Retrieved response from ${response.bank.bankName} for ${response.bank.customerName}")
log.info("Retrieved response from ${response.customerAccount?.bankName} for ${response.customerAccount?.customerName}")
withContext(Dispatchers.Main) {
retrievedResult(response)

View File

@ -51,22 +51,20 @@ struct ContentView: View {
private func retrieveTransactions() {
// TODO: set your credentials here
self.presenter.retrieveTransactions("", "", "", "", self.handleRetrieveTransactionsResult)
self.presenter.getAccountData("", "", "", "", self.handleGetAccountDataResponse)
}
private func handleRetrieveTransactionsResult(_ result: AddAccountResponse) {
NSLog("Retrieved response: \(result.retrievedTransactionsResponses)")
private func handleGetAccountDataResponse(_ response: GetAccountDataResponse) {
NSLog("Retrieved response: \(response.retrievedTransactions)")
if (result.successful) {
if (response.successful) {
var allTransactions: [AccountTransaction] = []
for accountResponse in result.retrievedTransactionsResponses {
if let transactions = accountResponse.retrievedData?.bookedTransactions as? Set<AccountTransaction> { // it's a Set
allTransactions.append(contentsOf: transactions)
}
if let transactions = accountResponse.retrievedData?.bookedTransactions as? [AccountTransaction] {
allTransactions.append(contentsOf: transactions)
}
if let transactions = response.retrievedTransactions as? Set<AccountTransaction> { // it's a Set
allTransactions.append(contentsOf: transactions)
}
if let transactions = response.retrievedTransactions as? [AccountTransaction] {
allTransactions.append(contentsOf: transactions)
}
self.transactions = allTransactions

View File

@ -18,8 +18,8 @@ class Presenter : ObservableObject {
}
func retrieveTransactions(_ bankCode: String, _ customerId: String, _ pin: String, _ finTs3ServerAddress: String, _ callback: @escaping (AddAccountResponse) -> Void) {
self.fintsClient.addAccountAsync(parameter: AddAccountParameter(bankCode: bankCode, customerId: customerId, pin: pin, finTs3ServerAddress: finTs3ServerAddress), callback: callback)
func getAccountData(_ bankCode: String, _ customerId: String, _ pin: String, _ finTs3ServerAddress: String, _ callback: @escaping (GetAccountDataResponse) -> Void) {
self.fintsClient.getAccountData(parameter: GetAccountDataParameter(bankCode: bankCode, customerId: customerId, pin: pin, finTs3ServerAddress: finTs3ServerAddress), callback: callback)
}

View File

@ -0,0 +1,97 @@
package net.dankito.banking.client.model
import kotlinx.datetime.LocalDate
import net.dankito.banking.fints.model.Amount
import net.dankito.banking.fints.model.Money
import net.dankito.utils.multiplatform.extensions.atUnixEpochStart
open class AccountTransaction(
val amount: Money, // TODO: if we decide to stick with Money, create own type, don't use that one from fints.model (or move over from)
val unparsedReference: String,
val bookingDate: LocalDate,
val otherPartyName: String?,
val otherPartyBankCode: String?,
val otherPartyAccountId: String?,
val bookingText: String?,
val valueDate: LocalDate,
val statementNumber: Int,
val sequenceNumber: Int?,
val openingBalance: Money?,
val closingBalance: Money?,
val endToEndReference: String?,
val customerReference: String?,
val mandateReference: String?,
val creditorIdentifier: String?,
val originatorsIdentificationCode: String?,
val compensationAmount: String?,
val originalAmount: String?,
val sepaReference: String?,
val deviantOriginator: String?,
val deviantRecipient: String?,
val referenceWithNoSpecialType: String?,
val primaNotaNumber: String?,
val textKeySupplement: String?,
val currencyType: String?,
val bookingKey: String,
val referenceForTheAccountOwner: String,
val referenceOfTheAccountServicingInstitution: String?,
val supplementaryDetails: String?,
val transactionReferenceNumber: String,
val relatedReferenceNumber: String?
) {
// for object deserializers
internal constructor() : this(Money(Amount.Zero, ""), "", LocalDate.atUnixEpochStart, null, null, null, null, LocalDate.atUnixEpochStart)
constructor(amount: Money, unparsedReference: String, bookingDate: LocalDate, otherPartyName: String?, otherPartyBankCode: String?, otherPartyAccountId: String?, bookingText: String?, valueDate: LocalDate)
: this(amount, unparsedReference, bookingDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate,
0, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null,
null, "", "", null, null, "", null)
open val showOtherPartyName: Boolean
get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO
val reference: String
get() = sepaReference ?: unparsedReference
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AccountTransaction) return false
if (amount != other.amount) return false
if (unparsedReference != other.unparsedReference) return false
if (bookingDate != other.bookingDate) return false
if (otherPartyName != other.otherPartyName) return false
if (otherPartyBankCode != other.otherPartyBankCode) return false
if (otherPartyAccountId != other.otherPartyAccountId) return false
if (bookingText != other.bookingText) return false
if (valueDate != other.valueDate) return false
return true
}
override fun hashCode(): Int {
var result = amount.hashCode()
result = 31 * result + unparsedReference.hashCode()
result = 31 * result + bookingDate.hashCode()
result = 31 * result + (otherPartyName?.hashCode() ?: 0)
result = 31 * result + (otherPartyBankCode?.hashCode() ?: 0)
result = 31 * result + (otherPartyAccountId?.hashCode() ?: 0)
result = 31 * result + (bookingText?.hashCode() ?: 0)
result = 31 * result + valueDate.hashCode()
return result
}
override fun toString(): String {
return "$valueDate $amount $otherPartyName: $unparsedReference"
}
}

View File

@ -0,0 +1,41 @@
package net.dankito.banking.client.model
import kotlinx.datetime.LocalDate
import net.dankito.banking.fints.model.Money
open class BankAccount(
identifier: String,
subAccountNumber: String?,
iban: String?,
val accountHolderName: String,
val type: BankAccountType = BankAccountType.CheckingAccount,
val productName: String? = null,
val currency: String = "EUR", // TODO: may parse to a value object
val accountLimit: String? = null,
// TODO: create an enum AccountCapabilities [ RetrieveBalance, RetrieveTransactions, TransferMoney / MoneyTransfer(?), InstantPayment ]
val supportsRetrievingTransactions: Boolean = false,
val supportsRetrievingBalance: Boolean = false,
val supportsTransferringMoney: Boolean = false,
val supportsInstantPayment: Boolean = false
) : BankAccountIdentifier(identifier, subAccountNumber, iban) {
internal constructor() : this("", null, null, "") // for object deserializers
constructor(identifier: BankAccountIdentifier) : this(identifier.identifier, identifier.subAccountNumber, identifier.iban, "")
open var balance: Money = Money.Zero
open var retrievedTransactionsFrom: LocalDate? = null
open var retrievedTransactionsTo: LocalDate? = null
open var bookedTransactions: List<AccountTransaction> = listOf()
override fun toString(): String {
return "$productName ($identifier)"
}
}

View File

@ -0,0 +1,8 @@
package net.dankito.banking.client.model
open class BankAccountIdentifier(
open val identifier: String,
open val subAccountNumber: String?,
open val iban: String?,
)

View File

@ -0,0 +1,26 @@
package net.dankito.banking.client.model
enum class BankAccountType {
CheckingAccount,
SavingsAccount,
FixedTermDepositAccount,
SecuritiesAccount,
LoanAccount,
CreditCardAccount,
FundDeposit,
BuildingLoanContract,
InsuranceContract,
Other
}

View File

@ -0,0 +1,35 @@
package net.dankito.banking.client.model
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
import net.dankito.banking.fints.model.TanMethod
//import net.dankito.banking.client.model.tan.TanMedium
//import net.dankito.banking.client.model.tan.TanMethod
open class CustomerAccount(
override var bankCode: String,
override var loginName: String,
override var password: String,
override var finTsServerAddress: String,
open var bankName: String,
open var bic: String,
open var customerName: String = "",
open var userId: String = loginName,
open var accounts: List<BankAccount> = listOf(),
// TODO: use that ones from .tan sub package
open var tanMethods: List<TanMethod> = listOf(),
open var selectedTanMethod: TanMethod? = null,
open var tanMedia: List<TanMedium> = listOf(),
open var selectedTanMedium: TanMedium? = null,
) : CustomerCredentials(bankCode, loginName, password, finTsServerAddress) {
override fun toString(): String {
return "$bankName $loginName"
}
}

View File

@ -0,0 +1,13 @@
package net.dankito.banking.client.model
open class CustomerCredentials(
open val bankCode: String,
open val loginName: String,
open val password: String,
open val finTsServerAddress: String // TODO: get rid of this
) {
internal constructor() : this("", "", "", "") // for object deserializers
}

View File

@ -0,0 +1,19 @@
package net.dankito.banking.client.model.parameter
import net.dankito.banking.fints.model.BankData
import net.dankito.banking.fints.model.TanMethodType
import net.dankito.banking.client.model.CustomerCredentials
// TODO: Rename to BankingClientRequest(Base)?
open class FinTsClientParameter(
bankCode: String,
loginName: String,
password: String,
finTsServerAddress: String, // TODO: get rid of this
open val preferredTanMethods: List<TanMethodType>? = null,
open val preferredTanMedium: String? = null, // the ID of the medium
open val abortIfTanIsRequired: Boolean = false,
open val finTsModel: BankData? = null
) : CustomerCredentials(bankCode, loginName, password, finTsServerAddress)

View File

@ -0,0 +1,33 @@
package net.dankito.banking.client.model.parameter
import kotlinx.datetime.LocalDate
import net.dankito.banking.fints.model.BankData
import net.dankito.banking.fints.model.TanMethodType
import net.dankito.banking.client.model.BankAccountIdentifier
open class GetAccountDataParameter(
bankCode: String,
loginName: String,
password: String,
finTsServerAddress: String, // TODO: get rid of this
/**
* Optionally specify for which bank account to retrieve the account data.
* If not set the data for all bank accounts of this account will be retrieved.
*/
open val accounts: List<BankAccountIdentifier>? = null,
open val retrieveBalance: Boolean = true,
open val retrieveTransactions: RetrieveTransactions = RetrieveTransactions.OfLast90Days,
open val retrieveTransactionsFrom: LocalDate? = null,
open val retrieveTransactionsTo: LocalDate? = null,
preferredTanMethods: List<TanMethodType>? = null,
preferredTanMedium: String? = null,
abortIfTanIsRequired: Boolean = false,
finTsModel: BankData? = null
) : FinTsClientParameter(bankCode, loginName, password, finTsServerAddress, preferredTanMethods, preferredTanMedium, abortIfTanIsRequired, finTsModel) {
open val retrieveOnlyAccountInfo: Boolean
get() = retrieveBalance == false && retrieveTransactions == RetrieveTransactions.No
}

View File

@ -0,0 +1,21 @@
package net.dankito.banking.client.model.parameter
enum class RetrieveTransactions {
No,
All,
/**
* Some banks support that according to PSD2 account transactions of last 90 days may be retrieved without
* a TAN (= no strong customer authorization needed). So try this options if you don't want to enter a TAN.
*/
OfLast90Days,
/**
* Retrieves account transactions in the boundaries of [GetAccountDataParameter.retrieveTransactionsFrom] to [GetAccountDataParameter.retrieveTransactionsTo].
*/
AccordingToRetrieveFromAndTo
}

View File

@ -0,0 +1,24 @@
package net.dankito.banking.client.model.response
enum class ErrorCode {
InternalError,
BankReturnedError,
WrongCredentials,
AccountLocked,
JobNotSupported,
UserCancelledAction,
TanRequiredButShouldAbortIfRequiresTan,
NoneOfTheAccountsSupportsRetrievingData,
DidNotRetrieveAllAccountData
}

View File

@ -0,0 +1,18 @@
package net.dankito.banking.client.model.response
import net.dankito.banking.fints.model.BankData
import net.dankito.banking.fints.model.MessageLogEntry
// TODO: rename to BankingClientResponse?
open class FinTsClientResponse(
open val error: ErrorCode?,
open val errorMessage: String?,
open val messageLogWithoutSensitiveData: List<MessageLogEntry>,
open val finTsModel: BankData? = null
) {
open val successful: Boolean
get() = error == null
}

View File

@ -0,0 +1,19 @@
package net.dankito.banking.client.model.response
import net.dankito.banking.client.model.AccountTransaction
import net.dankito.banking.client.model.CustomerAccount
import net.dankito.banking.fints.model.*
open class GetAccountDataResponse(
error: ErrorCode?,
errorMessage: String?,
open val customerAccount: CustomerAccount?,
messageLogWithoutSensitiveData: List<MessageLogEntry>,
finTsModel: BankData? = null
) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel) {
open val retrievedTransactions: List<AccountTransaction>
get() = customerAccount?.accounts?.flatMap { it.bookedTransactions } ?: listOf()
}

View File

@ -0,0 +1,122 @@
package net.dankito.banking.fints
import kotlinx.datetime.LocalDate
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.response.ErrorCode
import net.dankito.banking.client.model.response.GetAccountDataResponse
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.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 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) : this(callback, FinTsJobExecutor()) // Swift does not support default parameter values -> create constructor overloads
constructor(callback: FinTsClientCallback, webClient: IWebClient) : this(callback, FinTsJobExecutor(RequestExecutor(webClient = webClient)))
protected open val mapper = FinTsModelMapper()
open suspend fun getAccountData(param: GetAccountDataParameter): GetAccountDataResponse {
val bank = BankData(param.bankCode, param.loginName, param.password, 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)
val retrieveTransactionsFrom = when (param.retrieveTransactions) {
RetrieveTransactions.No -> LocalDate.todayAtEuropeBerlin() // TODO: implement RetrieveTransactions.No
RetrieveTransactions.OfLast90Days -> calculate90DaysAgo()
RetrieveTransactions.AccordingToRetrieveFromAndTo -> param.retrieveTransactionsFrom
else -> null
}
val retrieveTransactionsTo = when (param.retrieveTransactions) {
RetrieveTransactions.AccordingToRetrieveFromAndTo -> param.retrieveTransactionsTo
else -> null
}
return jobExecutor.getTransactionsAsync(context, GetAccountTransactionsParameter(bank, account, param.retrieveBalance, retrieveTransactionsFrom,
retrieveTransactionsTo, abortIfTanIsRequired = param.abortIfTanIsRequired))
}
private fun calculate90DaysAgo(): LocalDate? {
// Europe/Berlin: we're communicating with German bank servers, so we have to use their time zone
return LocalDate.todayAtEuropeBerlin().minusDays(90)
}
protected open suspend fun getAccountInfo(param: GetAccountDataParameter, bank: BankData): GetAccountInfoResponse {
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)
}
}

View File

@ -25,11 +25,6 @@ open class FinTsClientDeprecated(
protected open val product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically
) {
companion object {
val SupportedAccountTypes = listOf(AccountType.Girokonto, AccountType.Festgeldkonto, AccountType.Kreditkartenkonto)
}
constructor(callback: FinTsClientCallback) : this(callback, FinTsJobExecutor()) // Swift does not support default parameter values -> create constructor overloads
constructor(callback: FinTsClientCallback, webClient: IWebClient) : this(callback, FinTsJobExecutor(RequestExecutor(webClient = webClient)))

View File

@ -27,7 +27,7 @@ import net.dankito.utils.multiplatform.extensions.todayAtSystemDefaultTimeZone
/**
* Low level class that executes concrete business transactions (= FinTS Geschäftsvorfälle).
*
* In almost all cases you want to use [FinTsClientDeprecated] which wraps these business transactions to a higher level API.
* In almost all cases you want to use [FinTsClient] which wraps these business transactions to a higher level API.
*/
open class FinTsJobExecutor(
protected open val requestExecutor: RequestExecutor = RequestExecutor(),

View File

@ -0,0 +1,124 @@
package net.dankito.banking.fints.mapper
import net.dankito.banking.client.model.*
import net.dankito.banking.client.model.AccountTransaction
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.response.ErrorCode
import net.dankito.banking.fints.messages.datenelemente.abgeleiteteformate.Laenderkennzeichen
import net.dankito.banking.fints.model.*
import net.dankito.banking.fints.response.client.FinTsClientResponse
import net.dankito.banking.fints.response.client.GetAccountTransactionsResponse
import net.dankito.banking.fints.response.segments.AccountType
open class FinTsModelMapper {
open fun mapToAccountData(credentials: BankAccountIdentifier, param: GetAccountDataParameter): AccountData {
val accountData = AccountData(credentials.identifier, credentials.subAccountNumber, Laenderkennzeichen.Germany, param.bankCode,
credentials.iban, param.loginName, null, null, "", null, null, listOf(), listOf())
// TODO: where to know from if account supports retrieving balance and transactions?
accountData.setSupportsFeature(AccountFeature.RetrieveBalance, true)
accountData.setSupportsFeature(AccountFeature.RetrieveAccountTransactions, true)
return accountData
}
open fun map(bank: BankData): CustomerAccount {
return CustomerAccount(bank.bankCode, bank.customerId, bank.pin, bank.finTs3ServerAddress, bank.bankName, bank.bic, bank.customerName, bank.userId,
map(bank.accounts), bank.tanMethodsAvailableForUser, bank.selectedTanMethod, bank.tanMedia, bank.selectedTanMedium)
}
open fun map(accounts: List<AccountData>): List<BankAccount> {
return accounts.map { map(it) }
}
open fun map(account: AccountData): BankAccount {
return BankAccount(account.accountIdentifier, account.subAccountAttribute, account.iban, account.accountHolderName, map(account.accountType), account.productName,
account.currency ?: "EUR", account.accountLimit, account.supportsRetrievingAccountTransactions, account.supportsRetrievingBalance, account.supportsTransferringMoney, account.supportsRealTimeTransfer)
}
open fun map(accountType: AccountType?): BankAccountType {
return when (accountType) {
AccountType.Girokonto -> BankAccountType.CheckingAccount
AccountType.Sparkonto -> BankAccountType.SavingsAccount
AccountType.Festgeldkonto -> BankAccountType.FixedTermDepositAccount
AccountType.Wertpapierdepot -> BankAccountType.SecuritiesAccount
AccountType.Darlehenskonto -> BankAccountType.LoanAccount
AccountType.Kreditkartenkonto -> BankAccountType.CreditCardAccount
AccountType.FondsDepot -> BankAccountType.FundDeposit
AccountType.Bausparvertrag -> BankAccountType.BuildingLoanContract
AccountType.Versicherungsvertrag -> BankAccountType.InsuranceContract
else -> BankAccountType.Other
}
}
open fun map(bank: BankData, retrievedTransactionsResponses: List<GetAccountTransactionsResponse>): CustomerAccount {
val customerAccount = map(bank)
val retrievedData = retrievedTransactionsResponses.mapNotNull { it.retrievedData }
customerAccount.accounts.forEach { bankAccount ->
retrievedData.firstOrNull { it.account.accountIdentifier == bankAccount.identifier }?.let { accountTransactionsResponse ->
bankAccount.balance = accountTransactionsResponse.balance ?: Money.Zero
bankAccount.retrievedTransactionsFrom = accountTransactionsResponse.retrievedTransactionsFrom
bankAccount.retrievedTransactionsTo = accountTransactionsResponse.retrievedTransactionsTo
bankAccount.bookedTransactions = map(accountTransactionsResponse)
}
}
return customerAccount
}
open fun map(data: RetrievedAccountData): List<AccountTransaction> {
return data.bookedTransactions.map { map(it) }
}
open fun map(transaction: net.dankito.banking.fints.model.AccountTransaction): AccountTransaction {
return AccountTransaction(transaction.amount, transaction.unparsedReference, transaction.bookingDate,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText, transaction.valueDate,
transaction.statementNumber, transaction.sequenceNumber, transaction.openingBalance, transaction.closingBalance,
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount, transaction.sepaReference, transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement,
transaction.currencyType, transaction.bookingKey, transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution, transaction.supplementaryDetails,
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber)
}
open fun mapErrorCode(response: FinTsClientResponse): ErrorCode? {
return when {
response.internalError != null -> ErrorCode.InternalError
response.errorMessagesFromBank.isNotEmpty() -> ErrorCode.BankReturnedError
response.isPinLocked -> ErrorCode.AccountLocked
response.wrongCredentialsEntered -> ErrorCode.WrongCredentials
response.isJobAllowed == false || response.isJobVersionSupported == false -> ErrorCode.JobNotSupported
response.tanRequiredButWeWereToldToAbortIfSo -> ErrorCode.TanRequiredButShouldAbortIfRequiresTan
response.userCancelledAction || response.noTanMethodSelected || // either the user really has the choice to select one, then the errorCode would be UserCancelledAction,
// or if it gets selected automatically, that means there aren't any TanMethods which should only be the case if before another error occurred
// if isStrongAuthenticationRequired is set but tanRequiredButWeWereToldToAbortIfSo then user cancelled entering TAN
response.isStrongAuthenticationRequired -> ErrorCode.UserCancelledAction
else -> null
}
}
open fun mapErrorMessages(response: FinTsClientResponse?): String? {
if (response == null) {
return null
}
val errorMessages = response.errorMessagesFromBank.toMutableList()
response.internalError?.let {
errorMessages.add(it)
}
return if (errorMessages.isEmpty()) null
else errorMessages.joinToString("\r\n")
}
open fun mergeMessageLog(vararg responses: FinTsClientResponse?): List<MessageLogEntry> {
return responses.filterNotNull().flatMap { it.messageLogWithoutSensitiveData }
}
}

View File

@ -1,6 +1,6 @@
package net.dankito.banking.fints.model
import net.dankito.banking.fints.FinTsClientDeprecated
import net.dankito.banking.fints.FinTsClient
import net.dankito.banking.fints.messages.datenelemente.abgeleiteteformate.Laenderkennzeichen
import net.dankito.banking.fints.messages.segmente.id.CustomerSegmentId
import net.dankito.banking.fints.response.segments.AccountType
@ -27,7 +27,7 @@ open class AccountData(
open val isAccountTypeSupportedByApplication: Boolean
get() = FinTsClientDeprecated.SupportedAccountTypes.contains(accountType)
get() = FinTsClient.SupportedAccountTypes.contains(accountType)
|| allowedJobNames.contains(CustomerSegmentId.Balance.id)
|| allowedJobNames.contains(CustomerSegmentId.AccountTransactionsMt940.id)

View File

@ -6,6 +6,11 @@ open class Money(
val currency: Currency
) {
companion object {
val Zero = Money(Amount.Zero, "EUR")
}
constructor(amount: Amount, currencyCode: String) : this(amount, Currency(currencyCode))

View File

@ -8,12 +8,10 @@ open class AddAccountResponse(
context: JobContext,
getAccountsResponse: BankResponse,
open val retrievedTransactionsResponses: List<GetAccountTransactionsResponse> = listOf()
) : FinTsClientResponse(context, getAccountsResponse) {
open val bank: BankData = context.bank
) : GetAccountInfoResponse(context, getAccountsResponse) {
override val successful: Boolean
get() = super.successful && bank.accounts.isNotEmpty()
get() = super.successful
&& bank.accounts.size == retrievedTransactionsResponses.size
&& retrievedTransactionsResponses.none { it.noTanMethodSelected }
&& retrievedTransactionsResponses.none { it.isPinLocked }

View File

@ -0,0 +1,17 @@
package net.dankito.banking.fints.response.client
import net.dankito.banking.fints.model.*
import net.dankito.banking.fints.response.BankResponse
open class GetAccountInfoResponse(
context: JobContext,
getAccountsResponse: BankResponse,
) : FinTsClientResponse(context, getAccountsResponse) {
open val bank: BankData = context.bank
override val successful: Boolean
get() = super.successful && bank.accounts.isNotEmpty()
}

View File

@ -1,10 +1,9 @@
package net.dankito.banking.fints
import kotlinx.coroutines.*
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.response.GetAccountDataResponse
import net.dankito.banking.fints.callback.FinTsClientCallback
import net.dankito.banking.fints.model.AddAccountParameter
import net.dankito.banking.fints.response.client.AddAccountResponse
import net.dankito.banking.fints.webclient.IWebClient
open class iOSFinTsClient(
@ -12,7 +11,7 @@ open class iOSFinTsClient(
webClient: IWebClient
) {
protected open val fintsClient = FinTsClientDeprecated(callback, FinTsJobExecutor(RequestExecutor(webClient = webClient)))
protected open val fintsClient = FinTsClient(callback, FinTsJobExecutor(RequestExecutor(webClient = webClient)))
open var callback: FinTsClientCallback
get() = fintsClient.callback
@ -21,9 +20,9 @@ open class iOSFinTsClient(
}
open fun addAccountAsync(parameter: AddAccountParameter, callback: (AddAccountResponse) -> Unit) {
open fun getAccountData(parameter: GetAccountDataParameter, callback: (GetAccountDataResponse) -> Unit) {
GlobalScope.launch(Dispatchers.Main) { // do not block UI thread as with runBlocking { } but stay on UI thread as passing mutable state between threads currently doesn't work in Kotlin/Native
callback(fintsClient.addAccountAsync(parameter))
callback(fintsClient.getAccountData(parameter))
}
}

View File

@ -1,11 +1,11 @@
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.LocalDate
import net.dankito.banking.fints.FinTsClientDeprecated
import net.dankito.banking.client.model.AccountTransaction
import net.dankito.banking.client.model.CustomerAccount
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.fints.FinTsClient
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
import net.dankito.banking.fints.model.AddAccountParameter
import net.dankito.banking.fints.model.RetrievedAccountData
import net.dankito.banking.fints.model.TanChallenge
import net.dankito.banking.fints.response.client.AddAccountResponse
import net.dankito.utils.multiplatform.extensions.*
import platform.posix.exit
@ -23,13 +23,19 @@ class Application {
fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String) {
runBlocking {
val client = FinTsClientDeprecated(SimpleFinTsClientCallback { tanChallenge -> enterTan(tanChallenge) })
val client = FinTsClient(SimpleFinTsClientCallback { tanChallenge -> enterTan(tanChallenge) })
val response = client.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress))
val response = client.getAccountData(GetAccountDataParameter(bankCode, customerId, pin, finTs3ServerAddress))
println("Retrieved response from ${response.bank.bankName} for ${response.bank.customerName}")
if (response.error != null) {
println("An error occurred: ${response.error}${response.errorMessage?.let { " $it" }}")
}
displayRetrievedAccountData(response)
response.customerAccount?.let { account ->
println("Retrieved response from ${account.bankName} for ${account.customerName}")
displayRetrievedAccountData(account)
}
}
}
@ -50,36 +56,34 @@ class Application {
}
private fun displayRetrievedAccountData(response: AddAccountResponse) {
if (response.retrievedData.isEmpty()) {
private fun displayRetrievedAccountData(customer: CustomerAccount) {
if (customer.accounts.isEmpty()) {
println()
if (response.bank.accounts.isEmpty()) {
println("No data retrieved")
} else {
println("No transactions retrieved for accounts:")
response.bank.accounts.forEach { account -> println("- $account") }
}
println("No account data retrieved")
} else if (customer.accounts.flatMap { it.bookedTransactions }.isEmpty()) {
println()
println("No transactions retrieved for accounts:")
customer.accounts.forEach { println("- $it") }
}
response.retrievedData.forEach { data ->
customer.accounts.forEach { account ->
println()
println("${data.account}:")
println("${account}:")
println()
if (data.bookedTransactions.isEmpty()) {
if (account.bookedTransactions.isEmpty()) {
println("No transactions retrieved for this account")
} else {
displayTransactions(data)
displayTransactions(account.bookedTransactions)
}
}
}
private fun displayTransactions(data: RetrievedAccountData) {
val countTransactionsDigits = data.bookedTransactions.size.numberOfDigits
val largestAmountDigits = data.bookedTransactions.maxByOrNull { it.amount.displayString.length }?.amount?.displayString?.length ?: 0
private fun displayTransactions(bookedTransactions: List<AccountTransaction>) {
val countTransactionsDigits = bookedTransactions.size.numberOfDigits
val largestAmountDigits = bookedTransactions.maxByOrNull { it.amount.displayString.length }?.amount?.displayString?.length ?: 0
data.bookedTransactions.sortedByDescending { it.valueDate }.forEachIndexed { transactionIndex, transaction ->
bookedTransactions.sortedByDescending { it.valueDate }.forEachIndexed { transactionIndex, transaction ->
println("${(transactionIndex + 1).toStringWithMinDigits(countTransactionsDigits, " ")}. ${formatDate(transaction.valueDate)} " +
"${transaction.amount.displayString.ensureMinStringLength(largestAmountDigits, " ")} ${transaction.otherPartyName ?: ""} - ${transaction.reference}")
}