diff --git a/BankingClient/build.gradle.kts b/BankingClient/build.gradle.kts new file mode 100644 index 00000000..bf453982 --- /dev/null +++ b/BankingClient/build.gradle.kts @@ -0,0 +1,106 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + +plugins { + kotlin("multiplatform") +} + + +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 = "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(":BankingClientModel")) + } + } + commonTest { + dependencies { + implementation(kotlin("test")) + } + } + + jvmMain { + dependencies { + + } + } + jvmTest { } + + jsMain { + dependencies { + + } + } + jsTest { } + + nativeMain { } + nativeTest { } + } +} + + + +ext["customArtifactId"] = "banking-client" + +apply(from = "../gradle/scripts/publish-codinux.gradle.kts") \ No newline at end of file diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClient.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClient.kt new file mode 100644 index 00000000..20bd14a7 --- /dev/null +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClient.kt @@ -0,0 +1,19 @@ +package net.codinux.banking.client + +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 + +interface BankingClient { + + suspend fun getAccountDataAsync(bankCode: String, loginName: String, password: String) = + getAccountDataAsync(AccountCredentials(bankCode, loginName, password)) + + // for languages not supporting default parameters (Java, Swift, JS, ...) + suspend fun getAccountDataAsync(credentials: AccountCredentials) = + getAccountDataAsync(credentials, GetAccountDataOptions()) + + suspend fun getAccountDataAsync(credentials: AccountCredentials, options: GetAccountDataOptions): Response + +} \ No newline at end of file diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForCustomer.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForCustomer.kt new file mode 100644 index 00000000..e73228f8 --- /dev/null +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForCustomer.kt @@ -0,0 +1,14 @@ +package net.codinux.banking.client + +import net.codinux.banking.client.model.options.GetAccountDataOptions +import net.codinux.banking.client.model.response.GetAccountDataResponse +import net.codinux.banking.client.model.response.Response + +interface BankingClientForCustomer { + + // for languages not supporting default parameters (Java, Swift, JS, ...) + suspend fun getAccountDataAsync() = getAccountDataAsync(GetAccountDataOptions()) + + suspend fun getAccountDataAsync(options: GetAccountDataOptions): Response + +} \ No newline at end of file diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForCustomerBase.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForCustomerBase.kt new file mode 100644 index 00000000..395cef53 --- /dev/null +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BankingClientForCustomerBase.kt @@ -0,0 +1,14 @@ +package net.codinux.banking.client + +import net.codinux.banking.client.model.AccountCredentials +import net.codinux.banking.client.model.options.GetAccountDataOptions + +abstract class BankingClientForCustomerBase( + protected val credentials: AccountCredentials, + protected val client: BankingClient +) : BankingClientForCustomer { + + override suspend fun getAccountDataAsync(options: GetAccountDataOptions) = + client.getAccountDataAsync(credentials, options) + +} \ No newline at end of file diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BlockingBankingClient.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BlockingBankingClient.kt new file mode 100644 index 00000000..74c6e19b --- /dev/null +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BlockingBankingClient.kt @@ -0,0 +1,19 @@ +package net.codinux.banking.client + +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 + +interface BlockingBankingClient { + + suspend fun getAccountData(bankCode: String, loginName: String, password: String) = + getAccountData(AccountCredentials(bankCode, loginName, password)) + + // for languages not supporting default parameters (Java, Swift, JS, ...) + fun getAccountData(credentials: AccountCredentials) = + getAccountData(credentials, GetAccountDataOptions()) + + fun getAccountData(credentials: AccountCredentials, options: GetAccountDataOptions): Response + +} \ No newline at end of file diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BlockingBankingClientForCustomer.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BlockingBankingClientForCustomer.kt new file mode 100644 index 00000000..f8312267 --- /dev/null +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BlockingBankingClientForCustomer.kt @@ -0,0 +1,14 @@ +package net.codinux.banking.client + +import net.codinux.banking.client.model.options.GetAccountDataOptions +import net.codinux.banking.client.model.response.GetAccountDataResponse +import net.codinux.banking.client.model.response.Response + +interface BlockingBankingClientForCustomer { + + // for languages not supporting default parameters (Java, Swift, JS, ...) + fun getAccountData() = getAccountData(GetAccountDataOptions()) + + fun getAccountData(options: GetAccountDataOptions): Response + +} \ No newline at end of file diff --git a/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BlockingBankingClientForCustomerBase.kt b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BlockingBankingClientForCustomerBase.kt new file mode 100644 index 00000000..484fe19b --- /dev/null +++ b/BankingClient/src/commonMain/kotlin/net/codinux/banking/client/BlockingBankingClientForCustomerBase.kt @@ -0,0 +1,14 @@ +package net.codinux.banking.client + +import net.codinux.banking.client.model.AccountCredentials +import net.codinux.banking.client.model.options.GetAccountDataOptions + +abstract class BlockingBankingClientForCustomerBase( + protected val credentials: AccountCredentials, + protected val client: BlockingBankingClient +) : BlockingBankingClientForCustomer { + + override fun getAccountData(options: GetAccountDataOptions) = + client.getAccountData(credentials, options) + +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/options/GetAccountDataOptions.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/options/GetAccountDataOptions.kt new file mode 100644 index 00000000..cb1b5da6 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/options/GetAccountDataOptions.kt @@ -0,0 +1,11 @@ +package net.codinux.banking.client.model.options + +import kotlinx.datetime.LocalDate + +data class GetAccountDataOptions( + val retrieveBalance: Boolean = true, + val retrieveTransactions: RetrieveTransactions = RetrieveTransactions.OfLast90Days, + val retrieveTransactionsFrom: LocalDate? = null, + val retrieveTransactionsTo: LocalDate? = null, + val abortIfTanIsRequired: Boolean = false +) \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/options/RetrieveTransactions.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/options/RetrieveTransactions.kt new file mode 100644 index 00000000..e20b5246 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/options/RetrieveTransactions.kt @@ -0,0 +1,18 @@ +package net.codinux.banking.client.model.options + +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 [GetAccountDataOptions.retrieveTransactionsFrom] to [GetAccountDataOptions.retrieveTransactionsTo]. + */ + AccordingToRetrieveFromAndTo +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/Error.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/Error.kt new file mode 100644 index 00000000..efb9964c --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/Error.kt @@ -0,0 +1,19 @@ +package net.codinux.banking.client.model.response + +import net.codinux.banking.client.model.config.NoArgConstructor + +@NoArgConstructor +class Error( + val type: ErrorType, + + /** + * A banking client internal error like an error occurred during response parsing. + */ + val internalError: String? = null, + /** + * Error messages as received from bank + */ + val errorMessagesFromBank: List = emptyList(), +) { + override fun toString() = "$type: ${internalError ?: errorMessagesFromBank.joinToString()}" +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/ErrorType.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/ErrorType.kt new file mode 100644 index 00000000..0ab62f47 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/ErrorType.kt @@ -0,0 +1,33 @@ +package net.codinux.banking.client.model.response + +enum class ErrorType { + BankDoesNotSupportFinTs3, + + NetworkError, + + InternalError, + + BankReturnedError, + + WrongCredentials, + + AccountLocked, + + JobNotSupported, + + UserCancelledAction, + + TanRequiredButShouldAbortIfRequiresTan, + + NoneOfTheAccountsSupportsRetrievingData, + + DidNotRetrieveAllAccountData, + + CanNotDetermineBicForIban, + + NoAccountSupportsMoneyTransfer, + + MoreThanOneAccountSupportsMoneyTransfer, + + UnknownError +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetAccountDataResponse.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetAccountDataResponse.kt new file mode 100644 index 00000000..a0a9d7f0 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/GetAccountDataResponse.kt @@ -0,0 +1,16 @@ +package net.codinux.banking.client.model.response + +import net.codinux.banking.client.model.AccountTransaction +import net.codinux.banking.client.model.CustomerAccount +import net.codinux.banking.client.model.config.NoArgConstructor + +@NoArgConstructor +class GetAccountDataResponse( + val customer: CustomerAccount +) { + + val bookedTransactions: List + get() = customer.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate } + + override fun toString() = customer.toString() +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/Response.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/Response.kt new file mode 100644 index 00000000..88f7335c --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/Response.kt @@ -0,0 +1,30 @@ +package net.codinux.banking.client.model.response + +import net.codinux.banking.client.model.config.NoArgConstructor + +@NoArgConstructor +class Response ( + val type: ResponseType, + val data: T? = null, + val error: Error? = null, + val tanRequired: TanRequired? = null +) { + + companion object { + fun success(data: T): Response = + Response(ResponseType.Success, data) + + fun error(errorType: ErrorType, internalError: String? = null, errorMessagesFromBank: List = emptyList()): Response = + Response(ResponseType.Error, null, Error(errorType, internalError, errorMessagesFromBank)) + + fun bankReturnedError(errorMessagesFromBank: List): Response = + Response.error(ErrorType.BankReturnedError, null, errorMessagesFromBank) + } + + + override fun toString() = when (type) { + ResponseType.Success -> "Success: $data" + ResponseType.Error -> "Error: $error" + ResponseType.TanRequired -> "TanRequired: $tanRequired" + } +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/ResponseType.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/ResponseType.kt new file mode 100644 index 00000000..13fb0990 --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/ResponseType.kt @@ -0,0 +1,9 @@ +package net.codinux.banking.client.model.response + +enum class ResponseType { + Success, + + Error, + + TanRequired +} \ No newline at end of file diff --git a/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/TanRequired.kt b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/TanRequired.kt new file mode 100644 index 00000000..9c13f24a --- /dev/null +++ b/BankingClientModel/src/commonMain/kotlin/net/codinux/banking/client/model/response/TanRequired.kt @@ -0,0 +1,12 @@ +package net.codinux.banking.client.model.response + +import net.codinux.banking.client.model.config.NoArgConstructor + +@NoArgConstructor +class TanRequired ( + val tanRequestId: String, + // TODO: add TAN model +// val tanChallenge: TanChallenge +) { + +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 03f614b0..fcfb170a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,12 +27,14 @@ allprojects { tasks.register("publishAllToMavenLocal") { dependsOn( - ":BankingClientModel:publishToMavenLocal" + ":BankingClientModel:publishToMavenLocal", + ":BankingClient:publishToMavenLocal" ) } tasks.register("publishAll") { dependsOn( - ":BankingClientModel:publish" + ":BankingClientModel:publish", + ":BankingClient:publish" ) } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 232c9fe4..79c93aee 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,8 @@ plugins { } -rootProject.name = "BankingClient" +rootProject.name = "BankingClientProject" + include("BankingClientModel") +include("BankingClient")