From 000a169a007ec6090e6fd996b328528e07d5ce18 Mon Sep 17 00:00:00 2001 From: dankito Date: Tue, 3 Sep 2024 02:17:22 +0200 Subject: [PATCH] Added updateAccountTransactions() to BankingClient --- .../codinux/banking/client/BankingClient.kt | 9 ++++ .../banking/client/BankingClientForUser.kt | 9 ++++ .../client/BankingClientForUserBase.kt | 14 +++++- .../banking/client/BankingClientExtensions.kt | 11 +++++ .../banking/client/model/BankAccount.kt | 3 ++ .../model/extensions/LocalDateExtensions.kt | 25 +++++++++++ .../extensions/LocalDateTimeExtensions.kt | 16 +++++++ .../model/extensions/TimeZoneExtensions.kt | 7 +++ .../model/response/GetTransactionsResponse.kt | 22 ++++++++++ FinTs4jBankingClient/build.gradle.kts | 2 +- .../client/fints4k/FinTs4kBankingClient.kt | 31 ++++++++++++- .../banking/client/fints4k/FinTs4kMapper.kt | 43 ++++++++++++++++++- 12 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/LocalDateExtensions.kt create mode 100644 BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/LocalDateTimeExtensions.kt create mode 100644 BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/TimeZoneExtensions.kt create mode 100644 BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetTransactionsResponse.kt diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClient.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClient.kt index 8f9a5861..655f73f7 100644 --- a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClient.kt +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClient.kt @@ -1,8 +1,10 @@ package net.codinux.banking.client +import net.codinux.banking.client.model.* import net.codinux.banking.client.model.options.RetrieveTransactions import net.codinux.banking.client.model.request.GetAccountDataRequest import net.codinux.banking.client.model.response.GetAccountDataResponse +import net.codinux.banking.client.model.response.GetTransactionsResponse import net.codinux.banking.client.model.response.Response interface BankingClient { @@ -30,4 +32,11 @@ interface BankingClient { */ suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response + /** + * Convenience wrapper around [getAccountDataAsync]. + * Updates account's transactions beginning from [BankAccount.lastTransactionRetrievalTime]. + * This may requires TAN if [BankAccount.lastTransactionRetrievalTime] is older than 90 days. + */ + suspend fun updateAccountTransactionsAsync(user: UserAccount, accounts: List? = null): Response> + } \ No newline at end of file diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForUser.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForUser.kt index ee3f9f5d..f7a6a6a2 100644 --- a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForUser.kt +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForUser.kt @@ -1,8 +1,10 @@ package net.codinux.banking.client +import net.codinux.banking.client.model.BankAccount import net.codinux.banking.client.model.options.GetAccountDataOptions import net.codinux.banking.client.model.options.RetrieveTransactions import net.codinux.banking.client.model.response.GetAccountDataResponse +import net.codinux.banking.client.model.response.GetTransactionsResponse import net.codinux.banking.client.model.response.Response interface BankingClientForUser { @@ -30,4 +32,11 @@ interface BankingClientForUser { */ suspend fun getAccountDataAsync(options: GetAccountDataOptions): Response + /** + * Convenience wrapper around [getAccountDataAsync]. + * Updates account's transactions beginning from [BankAccount.lastTransactionRetrievalTime]. + * This may requires TAN if [BankAccount.lastTransactionRetrievalTime] is older than 90 days. + */ + suspend fun updateAccountTransactionsAsync(): Response> + } \ No newline at end of file diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForUserBase.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForUserBase.kt index cf9ac42c..893fba9e 100644 --- a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForUserBase.kt +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForUserBase.kt @@ -1,15 +1,27 @@ package net.codinux.banking.client import net.codinux.banking.client.model.AccountCredentials +import net.codinux.banking.client.model.UserAccount import net.codinux.banking.client.model.options.GetAccountDataOptions import net.codinux.banking.client.model.request.GetAccountDataRequest +import net.codinux.banking.client.model.response.GetTransactionsResponse +import net.codinux.banking.client.model.response.Response abstract class BankingClientForUserBase( protected val credentials: AccountCredentials, protected val client: BankingClient ) : BankingClientForUser { + private lateinit var user: UserAccount + override suspend fun getAccountDataAsync(options: GetAccountDataOptions) = - client.getAccountDataAsync(GetAccountDataRequest(credentials, options)) + client.getAccountDataAsync(GetAccountDataRequest(credentials, options)).also { + it.data?.user?.let { retrievedUser -> + this.user = retrievedUser + } + } + + override suspend fun updateAccountTransactionsAsync(): Response> = + client.updateAccountTransactionsAsync(user) } \ No newline at end of file diff --git a/BankingClient/src/jvmMain/kotlin/net/codinux/banking/client/BankingClientExtensions.kt b/BankingClient/src/jvmMain/kotlin/net/codinux/banking/client/BankingClientExtensions.kt index 1a48dd09..f7c4852c 100644 --- a/BankingClient/src/jvmMain/kotlin/net/codinux/banking/client/BankingClientExtensions.kt +++ b/BankingClient/src/jvmMain/kotlin/net/codinux/banking/client/BankingClientExtensions.kt @@ -1,6 +1,8 @@ package net.codinux.banking.client import kotlinx.coroutines.runBlocking +import net.codinux.banking.client.model.BankAccount +import net.codinux.banking.client.model.UserAccount import net.codinux.banking.client.model.options.GetAccountDataOptions import net.codinux.banking.client.model.request.GetAccountDataRequest @@ -12,10 +14,19 @@ fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking { this@getAccountData.getAccountDataAsync(request) } +fun BankingClient.updateAccountTransactions(user: UserAccount, accounts: List? = null) = runBlocking { + this@updateAccountTransactions.updateAccountTransactionsAsync(user, accounts) +} + + fun BankingClientForUser.getAccountData() = runBlocking { this@getAccountData.getAccountDataAsync() } fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlocking { this@getAccountData.getAccountDataAsync(options) +} + +fun BankingClientForUser.updateAccountTransactions() = runBlocking { + this@updateAccountTransactions.updateAccountTransactionsAsync() } \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/BankAccount.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/BankAccount.kt index e585c0be..f9d06c5c 100644 --- a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/BankAccount.kt +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/BankAccount.kt @@ -40,5 +40,8 @@ open class BankAccount( open val displayName: String get() = userSetDisplayName ?: productName ?: identifier + fun supportsAnyFeature(vararg features: BankAccountFeatures): Boolean = + features.any { this.features.contains(it) } + override fun toString() = "$type $identifier $productName (IBAN: $iban)" } \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/LocalDateExtensions.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/LocalDateExtensions.kt new file mode 100644 index 00000000..d547cbc7 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/LocalDateExtensions.kt @@ -0,0 +1,25 @@ +package net.codinux.banking.client.model.extensions + +import kotlinx.datetime.* +import kotlin.js.JsName + +val UnixEpochStart: LocalDate = LocalDate.parse("1970-01-01") + + +fun LocalDate.Companion.todayAtSystemDefaultTimeZone(): LocalDate { + return nowAt(TimeZone.currentSystemDefault()) +} + +fun LocalDate.Companion.todayAtEuropeBerlin(): LocalDate { + return nowAt(TimeZone.europeBerlin) +} + +@JsName("nowAtForDate") +fun LocalDate.Companion.nowAt(timeZone: TimeZone): LocalDate { + return Clock.System.todayIn(timeZone) +} + + +fun LocalDate.minusDays(days: Int): LocalDate { + return this.minus(days, DateTimeUnit.DAY) +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/LocalDateTimeExtensions.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/LocalDateTimeExtensions.kt new file mode 100644 index 00000000..b5d0d1a2 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/LocalDateTimeExtensions.kt @@ -0,0 +1,16 @@ +package net.codinux.banking.client.model.extensions + +import kotlinx.datetime.* + + +fun LocalDateTime.Companion.nowAtUtc(): LocalDateTime { + return nowAt(TimeZone.UTC) +} + +fun LocalDateTime.Companion.nowAtEuropeBerlin(): LocalDateTime { + return nowAt(TimeZone.europeBerlin) +} + +fun LocalDateTime.Companion.nowAt(timeZone: TimeZone): LocalDateTime { + return Clock.System.now().toLocalDateTime(timeZone) +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/TimeZoneExtensions.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/TimeZoneExtensions.kt new file mode 100644 index 00000000..1da04e33 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/extensions/TimeZoneExtensions.kt @@ -0,0 +1,7 @@ +package net.codinux.banking.client.model.extensions + +import kotlinx.datetime.TimeZone + + +val TimeZone.Companion.europeBerlin: TimeZone + get() = TimeZone.of("Europe/Berlin") \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetTransactionsResponse.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetTransactionsResponse.kt new file mode 100644 index 00000000..5f054c55 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetTransactionsResponse.kt @@ -0,0 +1,22 @@ +package net.codinux.banking.client.model.response + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import net.codinux.banking.client.model.AccountTransaction +import net.codinux.banking.client.model.Amount +import net.codinux.banking.client.model.BankAccount +import net.codinux.banking.client.model.UnbookedAccountTransaction +import net.codinux.banking.client.model.config.NoArgConstructor + +@NoArgConstructor +open class GetTransactionsResponse( + val account: BankAccount, + val balance: Amount? = null, + val bookedTransactions: List, + val unbookedTransactions: List, + val transactionRetrievalTime: LocalDateTime, + val retrievedTransactionsFrom: LocalDate? = null, + val retrievedTransactionsTo: LocalDate? = null +) { + override fun toString() = "${account.productName} $balance, ${bookedTransactions.size} booked transactions from $retrievedTransactionsFrom" +} \ No newline at end of file diff --git a/FinTs4jBankingClient/build.gradle.kts b/FinTs4jBankingClient/build.gradle.kts index 96cb0125..c95008ed 100644 --- a/FinTs4jBankingClient/build.gradle.kts +++ b/FinTs4jBankingClient/build.gradle.kts @@ -77,7 +77,7 @@ kotlin { dependencies { api(project(":BankingClient")) - api("net.codinux.banking:fints4k:1.0.0-Alpha-12") + api("net.codinux.banking:fints4k:1.0.0-Alpha-13-SNAPSHOT") api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion") } diff --git a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt index bedb8f91..12cd81f2 100644 --- a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt @@ -2,12 +2,15 @@ package net.codinux.banking.client.fints4k import net.codinux.banking.client.BankingClient import net.codinux.banking.client.BankingClientCallback +import net.codinux.banking.client.model.BankAccount +import net.codinux.banking.client.model.BankAccountFeatures +import net.codinux.banking.client.model.UserAccount import net.codinux.banking.client.model.options.GetAccountDataOptions import net.codinux.banking.client.model.request.GetAccountDataRequest -import net.codinux.banking.client.model.response.GetAccountDataResponse -import net.codinux.banking.client.model.response.Response +import net.codinux.banking.client.model.response.* import net.codinux.banking.fints.FinTsClient import net.codinux.banking.fints.config.FinTsClientConfiguration +import net.codinux.banking.fints.model.BankData open class FinTs4kBankingClient( config: FinTsClientConfiguration = FinTsClientConfiguration(), @@ -28,4 +31,28 @@ open class FinTs4kBankingClient( return mapper.map(response) } + override suspend fun updateAccountTransactionsAsync(user: UserAccount, accounts: List?): Response> { + val accountsToRequest = (accounts ?: user.accounts).filter { it.supportsAnyFeature(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.RetrieveBalance) } + + if (accountsToRequest.isNotEmpty()) { + var finTsModel: BankData? = null + + val responses = accountsToRequest.map { account -> + val parameter = mapper.mapToUpdateAccountTransactionsParameter(user, account, finTsModel) + + val response = client.getAccountDataAsync(parameter) + + if (response.finTsModel != null) { + finTsModel = response.finTsModel // so that basic account data doesn't have to be retrieved another time if user has multiple accounts + } + + Triple(account, parameter, response) + } + + return mapper.map(responses) + } + + return Response.error(ErrorType.NoneOfTheAccountsSupportsRetrievingData, "Keiner der Konten unterstützt das Abholen der Umsätze oder des Kontostands") // TODO: translate + } + } \ No newline at end of file diff --git a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt index 071f628a..c7806d8c 100644 --- a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt @@ -1,5 +1,6 @@ package net.codinux.banking.client.fints4k +import kotlinx.datetime.LocalDateTime import net.codinux.banking.client.model.* import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.Amount @@ -11,6 +12,7 @@ import net.codinux.banking.client.model.tan.TanChallenge import net.codinux.banking.client.model.tan.TanImage import net.codinux.banking.client.model.tan.TanMethod import net.codinux.banking.client.model.tan.TanMethodType +import net.codinux.banking.fints.extensions.nowAtEuropeBerlin import net.dankito.banking.client.model.BankAccountIdentifierImpl import net.dankito.banking.client.model.parameter.GetAccountDataParameter import net.dankito.banking.client.model.parameter.RetrieveTransactions @@ -41,6 +43,21 @@ open class FinTs4kMapper { abortIfTanIsRequired = options.abortIfTanIsRequired ) + open fun mapToUpdateAccountTransactionsParameter(user: UserAccount, account: BankAccount, finTsModel: BankData?): GetAccountDataParameter { + val accountIdentifier = BankAccountIdentifierImpl(account.identifier, account.subAccountNumber, account.iban) + val from = account.lastTransactionRetrievalTime?.date + val retrieveTransactions = if (from != null) RetrieveTransactions.AccordingToRetrieveFromAndTo else RetrieveTransactions.OfLast90Days +// val preferredTanMethods = listOf(mapTanMethodType(user.selectedTanMethod.type)) // TODO: currently we aren't saving TanMethods in database, re-enable as soon as TanMethods get saved + val preferredTanMethods = emptyList() + + return GetAccountDataParameter(user.bankCode, user.loginName, user.password!!, listOf(accountIdentifier), true, + retrieveTransactions, from, + preferredTanMethods = preferredTanMethods, + preferredTanMedium = user.selectedTanMediumName, + finTsModel = finTsModel + ) + } + protected open fun mapTanMethodType(type: TanMethodType): net.codinux.banking.fints.model.TanMethodType = net.codinux.banking.fints.model.TanMethodType.valueOf(type.name) @@ -53,6 +70,27 @@ open class FinTs4kMapper { } } + open fun map(responses: List>): Response> { + val type = if (responses.all { it.third.successful }) ResponseType.Success else ResponseType.Error + + // TODO: update UserAccount and BankAccount objects according to retrieved data + val mappedResponses = responses.map { (account, param, getAccountDataResponse) -> + val user = getAccountDataResponse.customerAccount + val finTsBankAccount = user?.accounts?.firstOrNull { it.identifier == account.identifier && it.subAccountNumber == account.subAccountNumber } + + if (getAccountDataResponse.successful && user != null && finTsBankAccount != null) { + Response.success(GetTransactionsResponse(account, mapAmount(finTsBankAccount.balance), mapBookedTransactions(finTsBankAccount), emptyList(), + finTsBankAccount.lastTransactionRetrievalTime ?: LocalDateTime.nowAtEuropeBerlin(), param.retrieveTransactionsFrom, param.retrieveTransactionsTo)) + } else { + mapError(getAccountDataResponse) + } + } + + val data = mappedResponses.filter { it.type == ResponseType.Success }.mapNotNull { it.data } + + return (object : Response>(type, data, mappedResponses.firstNotNullOfOrNull { it.error }) { }) + } + open fun mapToUserAccountViewInfo(bank: BankData): UserAccountViewInfo = UserAccountViewInfo( bank.bankCode, bank.customerId, bank.bankName, getBankingGroup(bank.bankName, bank.bic) @@ -87,7 +125,7 @@ open class FinTs4kMapper { mapAmount(account.balance), account.retrievedTransactionsFrom, account.lastTransactionRetrievalTime, // TODO: map haveAllTransactionsBeenRetrieved countDaysForWhichTransactionsAreKept = account.countDaysForWhichTransactionsAreKept, - bookedTransactions = account.bookedTransactions.map { mapTransaction(it) }.toMutableList() + bookedTransactions = mapBookedTransactions(account).toMutableList() ) protected open fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType = @@ -109,6 +147,9 @@ open class FinTs4kMapper { } + protected open fun mapBookedTransactions(account: net.dankito.banking.client.model.BankAccount): List = + account.bookedTransactions.map { mapTransaction(it) } + protected open fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction( mapAmount(transaction.amount), transaction.amount.currency.code, transaction.unparsedReference, transaction.bookingDate, transaction.valueDate,