diff --git a/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/FirstFragment.kt b/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/FirstFragment.kt index c8232c2e..b9c6484a 100644 --- a/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/FirstFragment.kt +++ b/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/FirstFragment.kt @@ -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 } } } } diff --git a/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/Presenter.kt b/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/Presenter.kt index 3b39b8fa..37266ea8 100644 --- a/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/Presenter.kt +++ b/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/Presenter.kt @@ -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) diff --git a/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/adapter/AccountTransactionsListRecyclerAdapter.kt b/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/adapter/AccountTransactionsListRecyclerAdapter.kt index 70145290..b50e47ce 100644 --- a/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/adapter/AccountTransactionsListRecyclerAdapter.kt +++ b/SampleApplications/AndroidApp/src/main/java/net/codinux/banking/fints4k/android/adapter/AccountTransactionsListRecyclerAdapter.kt @@ -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 diff --git a/SampleApplications/WebApp/src/main/kotlin/AccountTransactionsView.kt b/SampleApplications/WebApp/src/main/kotlin/AccountTransactionsView.kt index 1a6807ca..66b53642 100644 --- a/SampleApplications/WebApp/src/main/kotlin/AccountTransactionsView.kt +++ b/SampleApplications/WebApp/src/main/kotlin/AccountTransactionsView.kt @@ -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)) } } } diff --git a/SampleApplications/WebApp/src/main/kotlin/Presenter.kt b/SampleApplications/WebApp/src/main/kotlin/Presenter.kt index c2ca5974..2d949077 100644 --- a/SampleApplications/WebApp/src/main/kotlin/Presenter.kt +++ b/SampleApplications/WebApp/src/main/kotlin/Presenter.kt @@ -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) diff --git a/SampleApplications/iOSApp/fints4k iOS/fints4k iOS/ContentView.swift b/SampleApplications/iOSApp/fints4k iOS/fints4k iOS/ContentView.swift index 243c89a2..7c174546 100644 --- a/SampleApplications/iOSApp/fints4k iOS/fints4k iOS/ContentView.swift +++ b/SampleApplications/iOSApp/fints4k iOS/fints4k iOS/ContentView.swift @@ -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 { // 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 { // it's a Set + allTransactions.append(contentsOf: transactions) + } + if let transactions = response.retrievedTransactions as? [AccountTransaction] { + allTransactions.append(contentsOf: transactions) } self.transactions = allTransactions diff --git a/SampleApplications/iOSApp/fints4k iOS/fints4k iOS/Presenter.swift b/SampleApplications/iOSApp/fints4k iOS/fints4k iOS/Presenter.swift index 6948c644..327819c5 100644 --- a/SampleApplications/iOSApp/fints4k iOS/fints4k iOS/Presenter.swift +++ b/SampleApplications/iOSApp/fints4k iOS/fints4k iOS/Presenter.swift @@ -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) } diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/AccountTransaction.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/AccountTransaction.kt new file mode 100644 index 00000000..29b2d044 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/AccountTransaction.kt @@ -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" + } + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccount.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccount.kt new file mode 100644 index 00000000..15ad541f --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccount.kt @@ -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 = listOf() + + + override fun toString(): String { + return "$productName ($identifier)" + } + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccountIdentifier.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccountIdentifier.kt new file mode 100644 index 00000000..7dc214c1 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccountIdentifier.kt @@ -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?, +) \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccountType.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccountType.kt new file mode 100644 index 00000000..a9766c20 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/BankAccountType.kt @@ -0,0 +1,26 @@ +package net.dankito.banking.client.model + + +enum class BankAccountType { + + CheckingAccount, + + SavingsAccount, + + FixedTermDepositAccount, + + SecuritiesAccount, + + LoanAccount, + + CreditCardAccount, + + FundDeposit, + + BuildingLoanContract, + + InsuranceContract, + + Other + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/CustomerAccount.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/CustomerAccount.kt new file mode 100644 index 00000000..823c140d --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/CustomerAccount.kt @@ -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 = listOf(), + + // TODO: use that ones from .tan sub package + open var tanMethods: List = listOf(), + open var selectedTanMethod: TanMethod? = null, + open var tanMedia: List = listOf(), + open var selectedTanMedium: TanMedium? = null, +) : CustomerCredentials(bankCode, loginName, password, finTsServerAddress) { + + + override fun toString(): String { + return "$bankName $loginName" + } + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/CustomerCredentials.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/CustomerCredentials.kt new file mode 100644 index 00000000..2286ffe6 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/CustomerCredentials.kt @@ -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 + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/parameter/FinTsClientParameter.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/parameter/FinTsClientParameter.kt new file mode 100644 index 00000000..0f8eb317 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/parameter/FinTsClientParameter.kt @@ -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? = 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) \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/parameter/GetAccountDataParameter.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/parameter/GetAccountDataParameter.kt new file mode 100644 index 00000000..1208c175 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/parameter/GetAccountDataParameter.kt @@ -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? = null, + open val retrieveBalance: Boolean = true, + open val retrieveTransactions: RetrieveTransactions = RetrieveTransactions.OfLast90Days, + open val retrieveTransactionsFrom: LocalDate? = null, + open val retrieveTransactionsTo: LocalDate? = null, + + preferredTanMethods: List? = 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 + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/parameter/RetrieveTransactions.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/parameter/RetrieveTransactions.kt new file mode 100644 index 00000000..887cb2c3 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/parameter/RetrieveTransactions.kt @@ -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 + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/ErrorCode.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/ErrorCode.kt new file mode 100644 index 00000000..fea087ec --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/ErrorCode.kt @@ -0,0 +1,24 @@ +package net.dankito.banking.client.model.response + + +enum class ErrorCode { + + InternalError, + + BankReturnedError, + + WrongCredentials, + + AccountLocked, + + JobNotSupported, + + UserCancelledAction, + + TanRequiredButShouldAbortIfRequiresTan, + + NoneOfTheAccountsSupportsRetrievingData, + + DidNotRetrieveAllAccountData + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/FinTsClientResponse.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/FinTsClientResponse.kt new file mode 100644 index 00000000..9c12ee6e --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/FinTsClientResponse.kt @@ -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, + open val finTsModel: BankData? = null +) { + + open val successful: Boolean + get() = error == null + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/GetAccountDataResponse.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/GetAccountDataResponse.kt new file mode 100644 index 00000000..dfe9284d --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/GetAccountDataResponse.kt @@ -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, + finTsModel: BankData? = null +) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel) { + + open val retrievedTransactions: List + get() = customerAccount?.accounts?.flatMap { it.bookedTransactions } ?: listOf() + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClient.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClient.kt new file mode 100644 index 00000000..44573483 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClient.kt @@ -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, previousJobResponse: FinTsClientResponse?): GetAccountDataResponse { + val retrievedTransactionsResponses = mutableListOf() + + 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) + } + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientDeprecated.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientDeprecated.kt index bf1c5a62..e5902cbf 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientDeprecated.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientDeprecated.kt @@ -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))) 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 5f9b5742..20946297 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt @@ -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(), diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/mapper/FinTsModelMapper.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/mapper/FinTsModelMapper.kt new file mode 100644 index 00000000..c79fd6f3 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/mapper/FinTsModelMapper.kt @@ -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): List { + 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): 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 { + 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 { + return responses.filterNotNull().flatMap { it.messageLogWithoutSensitiveData } + } + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/AccountData.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/AccountData.kt index d73135ad..3836b43e 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/AccountData.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/AccountData.kt @@ -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) diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/Money.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/Money.kt index 4bad06e2..0e87db7b 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/Money.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/model/Money.kt @@ -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)) diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/response/client/AddAccountResponse.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/response/client/AddAccountResponse.kt index 8fac0505..457a64ea 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/response/client/AddAccountResponse.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/response/client/AddAccountResponse.kt @@ -8,12 +8,10 @@ open class AddAccountResponse( context: JobContext, getAccountsResponse: BankResponse, open val retrievedTransactionsResponses: List = 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 } diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/response/client/GetAccountInfoResponse.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/response/client/GetAccountInfoResponse.kt new file mode 100644 index 00000000..af06266d --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/response/client/GetAccountInfoResponse.kt @@ -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() + +} \ No newline at end of file diff --git a/fints4k/src/iosMain/kotlin/net/dankito/banking/fints/iOSFinTsClient.kt b/fints4k/src/iosMain/kotlin/net/dankito/banking/fints/iOSFinTsClient.kt index 89f44213..0723dfd8 100644 --- a/fints4k/src/iosMain/kotlin/net/dankito/banking/fints/iOSFinTsClient.kt +++ b/fints4k/src/iosMain/kotlin/net/dankito/banking/fints/iOSFinTsClient.kt @@ -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)) } } diff --git a/fints4k/src/nativeMain/kotlin/Application.kt b/fints4k/src/nativeMain/kotlin/Application.kt index 0ae8058d..6893e410 100644 --- a/fints4k/src/nativeMain/kotlin/Application.kt +++ b/fints4k/src/nativeMain/kotlin/Application.kt @@ -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) { + 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}") }