From 3e4dfee615559f7cdd7e192492762b032f5d2914 Mon Sep 17 00:00:00 2001 From: dankito Date: Sat, 17 Aug 2024 03:29:17 +0200 Subject: [PATCH] Added FinTs4kBankingClient --- FinTs4jBankingClient/build.gradle.kts | 125 ++++++++++++++++++ .../client/fints4k/FinTs4kBankingClient.kt | 26 ++++ .../FinTs4kBankingClientForCustomer.kt | 12 ++ .../banking/client/fints4k/FinTs4kMapper.kt | 106 +++++++++++++++ .../fints4k/FinTs4kBankingClientTest.kt | 33 +++++ build.gradle.kts | 8 +- settings.gradle.kts | 2 + 7 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 FinTs4jBankingClient/build.gradle.kts create mode 100644 FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt create mode 100644 FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientForCustomer.kt create mode 100644 FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt create mode 100644 FinTs4jBankingClient/src/commonTest/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientTest.kt diff --git a/FinTs4jBankingClient/build.gradle.kts b/FinTs4jBankingClient/build.gradle.kts new file mode 100644 index 00000000..b887b157 --- /dev/null +++ b/FinTs4jBankingClient/build.gradle.kts @@ -0,0 +1,125 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + +plugins { + kotlin("multiplatform") +} + + +repositories { + mavenLocal() + // TODO: remove again as soon as kmp-web-client is released + maven { + setUrl("https://oss.sonatype.org/content/repositories/snapshots/") + } +} + + +kotlin { + jvmToolchain(8) + + jvm { + withJava() + + testRuns["test"].executionTask.configure { + useJUnitPlatform() + + testLogging { + showExceptions = true + showStandardStreams = true + events("passed", "skipped", "failed") + // exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } + } + } + +// js { +// moduleName = "fints4k-banking-client" +// binaries.executable() +// +// browser { +// testTask { +// useKarma { +// useChromeHeadless() +// useFirefoxHeadless() +// } +// } +// } +// +// nodejs { +// testTask { +// useMocha { +// timeout = "20s" // Mocha times out after 2 s, which is too short for bufferExceeded() test +// } +// } +// } +// } +// +// wasmJs() +// +// +// linuxX64() +// mingwX64() +// +// iosArm64() +// iosSimulatorArm64() +// macosX64() +// macosArm64() +// watchosArm64() +// watchosSimulatorArm64() +// tvosArm64() +// tvosSimulatorArm64() + + applyDefaultHierarchyTemplate() + + + + val coroutinesVersion: String by project + + sourceSets { + commonMain { + dependencies { + api(project(":BankingClient")) + + api("net.codinux.banking:fints4k:1.0.0-Alpha-10") + +// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + } + } + commonTest { + dependencies { + implementation(kotlin("test")) + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") + } + } + + jvmMain { + dependencies { +// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:$coroutinesVersion") + } + } + jvmTest { + dependencies { + implementation(kotlin("test")) + } + } + +// jsMain { +// dependencies { +//// implementation(npm("@js-joda/timezone", "2.3.0")) +// } +// } +// jsTest { } +// +// nativeMain { } +// nativeTest { } + } +} + + + +ext["customArtifactId"] = "fints4k-banking-client" + +apply(from = "../gradle/scripts/publish-codinux.gradle.kts") \ No newline at end of file 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 new file mode 100644 index 00000000..b80fc8d6 --- /dev/null +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClient.kt @@ -0,0 +1,26 @@ +package net.codinux.banking.client.fints4k + +import net.codinux.banking.client.BankingClient +import net.codinux.banking.client.model.AccountCredentials +import net.codinux.banking.client.model.options.GetAccountDataOptions +import net.codinux.banking.client.model.response.GetAccountDataResponse +import net.codinux.banking.client.model.response.Response +import net.dankito.banking.fints.FinTsClient +import net.dankito.banking.fints.callback.SimpleFinTsClientCallback + +class FinTs4kBankingClient : BankingClient { + + private val mapper = FinTs4kMapper() + + private val client = FinTsClient(SimpleFinTsClientCallback { tanChallenge -> +// callback.enterTan() + }) + + + override suspend fun getAccountDataAsync(credentials: AccountCredentials, options: GetAccountDataOptions): Response { + val response = client.getAccountDataAsync(mapper.mapToGetAccountDataParameter(credentials, options)) + + return mapper.map(response) + } + +} \ No newline at end of file diff --git a/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientForCustomer.kt b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientForCustomer.kt new file mode 100644 index 00000000..73c928f7 --- /dev/null +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientForCustomer.kt @@ -0,0 +1,12 @@ +package net.codinux.banking.client.fints4k + +import net.codinux.banking.client.BankingClientForCustomerBase +import net.codinux.banking.client.model.AccountCredentials + +class FinTs4kBankingClientForCustomer(credentials: AccountCredentials) + : BankingClientForCustomerBase(credentials, FinTs4kBankingClient()) { + + constructor(bankCode: String, loginName: String, password: String) + : this(AccountCredentials(bankCode, loginName, password)) + +} \ 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 new file mode 100644 index 00000000..694c89b6 --- /dev/null +++ b/FinTs4jBankingClient/src/commonMain/kotlin/net/codinux/banking/client/fints4k/FinTs4kMapper.kt @@ -0,0 +1,106 @@ +package net.codinux.banking.client.fints4k + +import net.codinux.banking.client.model.* +import net.codinux.banking.client.model.options.GetAccountDataOptions +import net.codinux.banking.client.model.response.* +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.fints.model.Money + +class FinTs4kMapper { + + fun mapToGetAccountDataParameter(credentials: AccountCredentials, options: GetAccountDataOptions) = GetAccountDataParameter( + credentials.bankCode, credentials.loginName, credentials.password, + null, + options.retrieveBalance, + RetrieveTransactions.valueOf(options.retrieveTransactions.name), options.retrieveTransactionsFrom, options.retrieveTransactionsTo, + abortIfTanIsRequired = options.abortIfTanIsRequired + ) + + + fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response { + return if (response.successful && response.customerAccount != null) { + Response(ResponseType.Success, mapCustomer(response.customerAccount!!)) + } else { + mapError(response) + } + } + + + private fun mapCustomer(customer: net.dankito.banking.client.model.CustomerAccount): GetAccountDataResponse { + val mapped = CustomerAccount( + customer.bankCode, customer.loginName, customer.password, + customer.bankName, customer.bic, customer.customerName, customer.userId, + customer.accounts.map { mapAccount(it) } + ) + + return GetAccountDataResponse(mapped) + } + + + private fun mapAccount(account: net.dankito.banking.client.model.BankAccount): BankAccount = BankAccount( + account.identifier, account.accountHolderName, mapAccountType(account.type), account.iban, account.subAccountNumber, + account.productName, account.currency, account.accountLimit, isAccountTypeSupported(account), + mapFeatures(account), + mapAmount(account.balance), account.retrievedTransactionsFrom, account.retrievedTransactionsTo, + // TODO: map haveAllTransactionsBeenRetrieved and countDaysForWhichTransactionsAreKept + bookedTransactions = account.bookedTransactions.map { mapTransaction(it) }.toMutableList() + ) + + private fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType = + BankAccountType.valueOf(type.name) + + private fun mapFeatures(account: net.dankito.banking.client.model.BankAccount): Set = buildSet { + if (account.supportsRetrievingBalance) { + add(BankAccountFeatures.RetrieveBalance) + } + if (account.supportsRetrievingTransactions) { + add(BankAccountFeatures.RetrieveTransactions) + } + if (account.supportsTransferringMoney) { + add(BankAccountFeatures.TransferMoney) + } + if (account.supportsInstantPayment) { + add(BankAccountFeatures.InstantPayment) + } + } + + private fun isAccountTypeSupported(account: net.dankito.banking.client.model.BankAccount): Boolean { + // TODO: +// open val isAccountTypeSupportedByApplication: Boolean +// get() = FinTsClient.SupportedAccountTypes.contains(accountType) +// || allowedJobNames.contains(CustomerSegmentId.Balance.id) +// || allowedJobNames.contains(CustomerSegmentId.AccountTransactionsMt940.id) + + return true + } + + + private fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction( + mapAmount(transaction.amount), transaction.amount.currency.code, transaction.unparsedReference, + transaction.bookingDate, transaction.valueDate, + transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, + transaction.bookingText, null, + transaction.statementNumber, transaction.sequenceNumber, + mapNullableAmount(transaction.openingBalance), mapNullableAmount(transaction.closingBalance), + // TODO: map other properties + ) + + private fun mapNullableAmount(amount: Money?) = amount?.let { mapAmount(it) } + + private fun mapAmount(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.')) + + + private fun mapError(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response { + val error = if (response.error != null) { + Error(ErrorType.valueOf(response.error!!.name), if (response.error == ErrorCode.BankReturnedError) null else response.errorMessage, + if (response.error == ErrorCode.BankReturnedError && response.errorMessage !== null) listOf(response.errorMessage!!) else emptyList()) + } else { + Error(ErrorType.UnknownError, response.errorMessage) + } + + return Response(ResponseType.Error, null, error) + } + +} \ No newline at end of file diff --git a/FinTs4jBankingClient/src/commonTest/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientTest.kt b/FinTs4jBankingClient/src/commonTest/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientTest.kt new file mode 100644 index 00000000..4a14fd0e --- /dev/null +++ b/FinTs4jBankingClient/src/commonTest/kotlin/net/codinux/banking/client/fints4k/FinTs4kBankingClientTest.kt @@ -0,0 +1,33 @@ +package net.codinux.banking.client.fints4k + +import kotlinx.coroutines.test.runTest +import net.codinux.banking.client.model.response.ResponseType +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class FinTs4kBankingClientTest { + + companion object { + + // set your credentials here: + private const val bankCode = "" + private const val loginName = "" + private const val password = "" + } + + + private val underTest = FinTs4kBankingClientForCustomer(bankCode, loginName, password) + + + @Test + fun getAccountDataAsync() = runTest { + val result = underTest.getAccountDataAsync() + + assertEquals(ResponseType.Success, result.type) + assertNotNull(result.data) + assertTrue(result.data!!.bookedTransactions.isNotEmpty()) + } + +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index fcfb170a..55e0bea2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,13 +28,17 @@ allprojects { tasks.register("publishAllToMavenLocal") { dependsOn( ":BankingClientModel:publishToMavenLocal", - ":BankingClient:publishToMavenLocal" + ":BankingClient:publishToMavenLocal", + + ":FinTs4jBankingClient:publishToMavenLocal" ) } tasks.register("publishAll") { dependsOn( ":BankingClientModel:publish", - ":BankingClient:publish" + ":BankingClient:publish", + + ":FinTs4jBankingClient:publish" ) } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 79c93aee..777137d1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,3 +26,5 @@ rootProject.name = "BankingClientProject" include("BankingClientModel") include("BankingClient") + +include("FinTs4jBankingClient")