Added BankingClient

This commit is contained in:
dankito 2024-08-17 03:21:50 +02:00
parent d89a55e8d7
commit 0c9d1ab91c
17 changed files with 355 additions and 3 deletions

View File

@ -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")

View File

@ -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<GetAccountDataResponse>
}

View File

@ -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<GetAccountDataResponse>
}

View File

@ -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)
}

View File

@ -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<GetAccountDataResponse>
}

View File

@ -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<GetAccountDataResponse>
}

View File

@ -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)
}

View File

@ -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
)

View File

@ -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
}

View File

@ -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<String> = emptyList(),
) {
override fun toString() = "$type: ${internalError ?: errorMessagesFromBank.joinToString()}"
}

View File

@ -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
}

View File

@ -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<AccountTransaction>
get() = customer.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
override fun toString() = customer.toString()
}

View File

@ -0,0 +1,30 @@
package net.codinux.banking.client.model.response
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
class Response<T> (
val type: ResponseType,
val data: T? = null,
val error: Error? = null,
val tanRequired: TanRequired? = null
) {
companion object {
fun <T> success(data: T): Response<T> =
Response(ResponseType.Success, data)
fun <T> error(errorType: ErrorType, internalError: String? = null, errorMessagesFromBank: List<String> = emptyList()): Response<T> =
Response(ResponseType.Error, null, Error(errorType, internalError, errorMessagesFromBank))
fun <T> bankReturnedError(errorMessagesFromBank: List<String>): Response<T> =
Response.error(ErrorType.BankReturnedError, null, errorMessagesFromBank)
}
override fun toString() = when (type) {
ResponseType.Success -> "Success: $data"
ResponseType.Error -> "Error: $error"
ResponseType.TanRequired -> "TanRequired: $tanRequired"
}
}

View File

@ -0,0 +1,9 @@
package net.codinux.banking.client.model.response
enum class ResponseType {
Success,
Error,
TanRequired
}

View File

@ -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
) {
}

View File

@ -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"
)
}

View File

@ -21,6 +21,8 @@ plugins {
}
rootProject.name = "BankingClient"
rootProject.name = "BankingClientProject"
include("BankingClientModel")
include("BankingClient")