Implemented new simplified data model in FinTsClient.getAccountData()
This commit is contained in:
parent
b74b165974
commit
52de5a2956
|
@ -48,8 +48,8 @@ class FirstFragment : Fragment() {
|
||||||
|
|
||||||
// TODO: set your credentials here
|
// TODO: set your credentials here
|
||||||
presenter.retrieveAccountData("", "", "", "") { response ->
|
presenter.retrieveAccountData("", "", "", "") { response ->
|
||||||
if (response.successful) {
|
response.customerAccount?.let { customer ->
|
||||||
accountTransactionsAdapter.items = response.retrievedData.flatMap { it.bookedTransactions }
|
accountTransactionsAdapter.items = customer.accounts.flatMap { it.bookedTransactions }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@ import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.datetime.LocalDate
|
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.callback.SimpleFinTsClientCallback
|
||||||
import net.dankito.banking.fints.model.AddAccountParameter
|
|
||||||
import net.dankito.banking.fints.model.TanChallenge
|
import net.dankito.banking.fints.model.TanChallenge
|
||||||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
|
||||||
import net.dankito.utils.multiplatform.extensions.millisSinceEpochAtSystemDefaultTimeZone
|
import net.dankito.utils.multiplatform.extensions.millisSinceEpochAtSystemDefaultTimeZone
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
@ -24,7 +24,7 @@ open class Presenter {
|
||||||
private val log = LoggerFactory.getLogger(Presenter::class.java)
|
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
|
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) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
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) {
|
withContext(Dispatchers.Main) {
|
||||||
retrievedResult(response)
|
retrievedResult(response)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.view.View
|
||||||
import net.codinux.banking.fints4k.android.Presenter
|
import net.codinux.banking.fints4k.android.Presenter
|
||||||
import net.codinux.banking.fints4k.android.R
|
import net.codinux.banking.fints4k.android.R
|
||||||
import net.codinux.banking.fints4k.android.adapter.viewholder.AccountTransactionsViewHolder
|
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.banking.fints.util.toBigDecimal
|
||||||
import net.dankito.utils.android.extensions.setTextColorToColorResource
|
import net.dankito.utils.android.extensions.setTextColorToColorResource
|
||||||
import net.dankito.utils.android.ui.adapter.ListRecyclerAdapter
|
import net.dankito.utils.android.ui.adapter.ListRecyclerAdapter
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
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.RBuilder
|
||||||
import react.RComponent
|
import react.RComponent
|
||||||
import react.Props
|
import react.Props
|
||||||
|
@ -27,10 +28,10 @@ class AccountTransactionsView(props: AccountTransactionsViewProps) : RComponent<
|
||||||
// TODO: set your credentials here
|
// TODO: set your credentials here
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
props.presenter.retrieveAccountData("", "", "", "") { response ->
|
props.presenter.retrieveAccountData("", "", "", "") { response ->
|
||||||
if (response.successful) {
|
response.customerAccount?.let { customer ->
|
||||||
val balance = response.retrievedData.sumOf { it.balance?.amount?.string?.replace(',', '.')?.toDoubleOrNull() ?: 0.0 } // i know, double is not an appropriate data type for amounts
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,13 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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.callback.SimpleFinTsClientCallback
|
||||||
import net.dankito.banking.fints.model.AddAccountParameter
|
|
||||||
import net.dankito.banking.fints.model.TanChallenge
|
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.KtorWebClient
|
||||||
import net.dankito.banking.fints.webclient.ProxyingWebClient
|
import net.dankito.banking.fints.webclient.ProxyingWebClient
|
||||||
import net.dankito.utils.multiplatform.log.LoggerFactory
|
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
|
// 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
|
// 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()))
|
ProxyingWebClient("http://localhost:8082/", KtorWebClient()))
|
||||||
|
|
||||||
open var enterTanCallback: ((TanChallenge) -> Unit)? = null
|
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) {
|
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) {
|
withContext(Dispatchers.Main) {
|
||||||
retrievedResult(response)
|
retrievedResult(response)
|
||||||
|
|
|
@ -51,23 +51,21 @@ struct ContentView: View {
|
||||||
|
|
||||||
private func retrieveTransactions() {
|
private func retrieveTransactions() {
|
||||||
// TODO: set your credentials here
|
// TODO: set your credentials here
|
||||||
self.presenter.retrieveTransactions("", "", "", "", self.handleRetrieveTransactionsResult)
|
self.presenter.getAccountData("", "", "", "", self.handleGetAccountDataResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleRetrieveTransactionsResult(_ result: AddAccountResponse) {
|
private func handleGetAccountDataResponse(_ response: GetAccountDataResponse) {
|
||||||
NSLog("Retrieved response: \(result.retrievedTransactionsResponses)")
|
NSLog("Retrieved response: \(response.retrievedTransactions)")
|
||||||
|
|
||||||
if (result.successful) {
|
if (response.successful) {
|
||||||
var allTransactions: [AccountTransaction] = []
|
var allTransactions: [AccountTransaction] = []
|
||||||
|
|
||||||
for accountResponse in result.retrievedTransactionsResponses {
|
if let transactions = response.retrievedTransactions as? Set<AccountTransaction> { // it's a Set
|
||||||
if let transactions = accountResponse.retrievedData?.bookedTransactions as? Set<AccountTransaction> { // it's a Set
|
|
||||||
allTransactions.append(contentsOf: transactions)
|
allTransactions.append(contentsOf: transactions)
|
||||||
}
|
}
|
||||||
if let transactions = accountResponse.retrievedData?.bookedTransactions as? [AccountTransaction] {
|
if let transactions = response.retrievedTransactions as? [AccountTransaction] {
|
||||||
allTransactions.append(contentsOf: transactions)
|
allTransactions.append(contentsOf: transactions)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.transactions = allTransactions
|
self.transactions = allTransactions
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@ class Presenter : ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func retrieveTransactions(_ bankCode: String, _ customerId: String, _ pin: String, _ finTs3ServerAddress: String, _ callback: @escaping (AddAccountResponse) -> Void) {
|
func getAccountData(_ bankCode: String, _ customerId: String, _ pin: String, _ finTs3ServerAddress: String, _ callback: @escaping (GetAccountDataResponse) -> Void) {
|
||||||
self.fintsClient.addAccountAsync(parameter: AddAccountParameter(bankCode: bankCode, customerId: customerId, pin: pin, finTs3ServerAddress: finTs3ServerAddress), callback: callback)
|
self.fintsClient.getAccountData(parameter: GetAccountDataParameter(bankCode: bankCode, customerId: customerId, pin: pin, finTs3ServerAddress: finTs3ServerAddress), callback: callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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?,
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
package net.dankito.banking.client.model
|
||||||
|
|
||||||
|
|
||||||
|
enum class BankAccountType {
|
||||||
|
|
||||||
|
CheckingAccount,
|
||||||
|
|
||||||
|
SavingsAccount,
|
||||||
|
|
||||||
|
FixedTermDepositAccount,
|
||||||
|
|
||||||
|
SecuritiesAccount,
|
||||||
|
|
||||||
|
LoanAccount,
|
||||||
|
|
||||||
|
CreditCardAccount,
|
||||||
|
|
||||||
|
FundDeposit,
|
||||||
|
|
||||||
|
BuildingLoanContract,
|
||||||
|
|
||||||
|
InsuranceContract,
|
||||||
|
|
||||||
|
Other
|
||||||
|
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package net.dankito.banking.client.model.response
|
||||||
|
|
||||||
|
|
||||||
|
enum class ErrorCode {
|
||||||
|
|
||||||
|
InternalError,
|
||||||
|
|
||||||
|
BankReturnedError,
|
||||||
|
|
||||||
|
WrongCredentials,
|
||||||
|
|
||||||
|
AccountLocked,
|
||||||
|
|
||||||
|
JobNotSupported,
|
||||||
|
|
||||||
|
UserCancelledAction,
|
||||||
|
|
||||||
|
TanRequiredButShouldAbortIfRequiresTan,
|
||||||
|
|
||||||
|
NoneOfTheAccountsSupportsRetrievingData,
|
||||||
|
|
||||||
|
DidNotRetrieveAllAccountData
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,11 +25,6 @@ open class FinTsClientDeprecated(
|
||||||
protected open val product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically
|
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) : this(callback, FinTsJobExecutor()) // Swift does not support default parameter values -> create constructor overloads
|
||||||
|
|
||||||
constructor(callback: FinTsClientCallback, webClient: IWebClient) : this(callback, FinTsJobExecutor(RequestExecutor(webClient = webClient)))
|
constructor(callback: FinTsClientCallback, webClient: IWebClient) : this(callback, FinTsJobExecutor(RequestExecutor(webClient = webClient)))
|
||||||
|
|
|
@ -27,7 +27,7 @@ import net.dankito.utils.multiplatform.extensions.todayAtSystemDefaultTimeZone
|
||||||
/**
|
/**
|
||||||
* Low level class that executes concrete business transactions (= FinTS Geschäftsvorfälle).
|
* 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(
|
open class FinTsJobExecutor(
|
||||||
protected open val requestExecutor: RequestExecutor = RequestExecutor(),
|
protected open val requestExecutor: RequestExecutor = RequestExecutor(),
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package net.dankito.banking.fints.model
|
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.datenelemente.abgeleiteteformate.Laenderkennzeichen
|
||||||
import net.dankito.banking.fints.messages.segmente.id.CustomerSegmentId
|
import net.dankito.banking.fints.messages.segmente.id.CustomerSegmentId
|
||||||
import net.dankito.banking.fints.response.segments.AccountType
|
import net.dankito.banking.fints.response.segments.AccountType
|
||||||
|
@ -27,7 +27,7 @@ open class AccountData(
|
||||||
|
|
||||||
|
|
||||||
open val isAccountTypeSupportedByApplication: Boolean
|
open val isAccountTypeSupportedByApplication: Boolean
|
||||||
get() = FinTsClientDeprecated.SupportedAccountTypes.contains(accountType)
|
get() = FinTsClient.SupportedAccountTypes.contains(accountType)
|
||||||
|| allowedJobNames.contains(CustomerSegmentId.Balance.id)
|
|| allowedJobNames.contains(CustomerSegmentId.Balance.id)
|
||||||
|| allowedJobNames.contains(CustomerSegmentId.AccountTransactionsMt940.id)
|
|| allowedJobNames.contains(CustomerSegmentId.AccountTransactionsMt940.id)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,11 @@ open class Money(
|
||||||
val currency: Currency
|
val currency: Currency
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val Zero = Money(Amount.Zero, "EUR")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
constructor(amount: Amount, currencyCode: String) : this(amount, Currency(currencyCode))
|
constructor(amount: Amount, currencyCode: String) : this(amount, Currency(currencyCode))
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,10 @@ open class AddAccountResponse(
|
||||||
context: JobContext,
|
context: JobContext,
|
||||||
getAccountsResponse: BankResponse,
|
getAccountsResponse: BankResponse,
|
||||||
open val retrievedTransactionsResponses: List<GetAccountTransactionsResponse> = listOf()
|
open val retrievedTransactionsResponses: List<GetAccountTransactionsResponse> = listOf()
|
||||||
) : FinTsClientResponse(context, getAccountsResponse) {
|
) : GetAccountInfoResponse(context, getAccountsResponse) {
|
||||||
|
|
||||||
open val bank: BankData = context.bank
|
|
||||||
|
|
||||||
override val successful: Boolean
|
override val successful: Boolean
|
||||||
get() = super.successful && bank.accounts.isNotEmpty()
|
get() = super.successful
|
||||||
&& bank.accounts.size == retrievedTransactionsResponses.size
|
&& bank.accounts.size == retrievedTransactionsResponses.size
|
||||||
&& retrievedTransactionsResponses.none { it.noTanMethodSelected }
|
&& retrievedTransactionsResponses.none { it.noTanMethodSelected }
|
||||||
&& retrievedTransactionsResponses.none { it.isPinLocked }
|
&& retrievedTransactionsResponses.none { it.isPinLocked }
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
package net.dankito.banking.fints
|
package net.dankito.banking.fints
|
||||||
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
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.callback.FinTsClientCallback
|
||||||
import net.dankito.banking.fints.model.AddAccountParameter
|
|
||||||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
|
||||||
import net.dankito.banking.fints.webclient.IWebClient
|
import net.dankito.banking.fints.webclient.IWebClient
|
||||||
|
|
||||||
open class iOSFinTsClient(
|
open class iOSFinTsClient(
|
||||||
|
@ -12,7 +11,7 @@ open class iOSFinTsClient(
|
||||||
webClient: IWebClient
|
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
|
open var callback: FinTsClientCallback
|
||||||
get() = fintsClient.callback
|
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
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.datetime.LocalDate
|
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.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.model.TanChallenge
|
||||||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
|
||||||
import net.dankito.utils.multiplatform.extensions.*
|
import net.dankito.utils.multiplatform.extensions.*
|
||||||
import platform.posix.exit
|
import platform.posix.exit
|
||||||
|
|
||||||
|
@ -23,13 +23,19 @@ class Application {
|
||||||
|
|
||||||
fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String) {
|
fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String) {
|
||||||
runBlocking {
|
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) {
|
private fun displayRetrievedAccountData(customer: CustomerAccount) {
|
||||||
if (response.retrievedData.isEmpty()) {
|
if (customer.accounts.isEmpty()) {
|
||||||
|
println()
|
||||||
|
println("No account data retrieved")
|
||||||
|
} else if (customer.accounts.flatMap { it.bookedTransactions }.isEmpty()) {
|
||||||
println()
|
println()
|
||||||
|
|
||||||
if (response.bank.accounts.isEmpty()) {
|
|
||||||
println("No data retrieved")
|
|
||||||
} else {
|
|
||||||
println("No transactions retrieved for accounts:")
|
println("No transactions retrieved for accounts:")
|
||||||
response.bank.accounts.forEach { account -> println("- $account") }
|
customer.accounts.forEach { println("- $it") }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response.retrievedData.forEach { data ->
|
customer.accounts.forEach { account ->
|
||||||
println()
|
println()
|
||||||
println("${data.account}:")
|
println("${account}:")
|
||||||
println()
|
println()
|
||||||
|
|
||||||
if (data.bookedTransactions.isEmpty()) {
|
if (account.bookedTransactions.isEmpty()) {
|
||||||
println("No transactions retrieved for this account")
|
println("No transactions retrieved for this account")
|
||||||
} else {
|
} else {
|
||||||
displayTransactions(data)
|
displayTransactions(account.bookedTransactions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayTransactions(data: RetrievedAccountData) {
|
private fun displayTransactions(bookedTransactions: List<AccountTransaction>) {
|
||||||
val countTransactionsDigits = data.bookedTransactions.size.numberOfDigits
|
val countTransactionsDigits = bookedTransactions.size.numberOfDigits
|
||||||
val largestAmountDigits = data.bookedTransactions.maxByOrNull { it.amount.displayString.length }?.amount?.displayString?.length ?: 0
|
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)} " +
|
println("${(transactionIndex + 1).toStringWithMinDigits(countTransactionsDigits, " ")}. ${formatDate(transaction.valueDate)} " +
|
||||||
"${transaction.amount.displayString.ensureMinStringLength(largestAmountDigits, " ")} ${transaction.otherPartyName ?: ""} - ${transaction.reference}")
|
"${transaction.amount.displayString.ensureMinStringLength(largestAmountDigits, " ")} ${transaction.otherPartyName ?: ""} - ${transaction.reference}")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue