Compare commits
No commits in common. "main" and "v1.0.0-Alpha-11" have entirely different histories.
main
...
v1.0.0-Alp
419 changed files with 3002 additions and 6414 deletions
24
README.md
24
README.md
|
@ -4,49 +4,35 @@ fints4k is an implementation of the FinTS 3.0 online banking protocol used by mo
|
|||
|
||||
It's fast, easy extendable and running on multiple platforms: JVM, Android, (iOS, JavaScript, Windows, MacOS, Linux).
|
||||
|
||||
However it's not a full implementation of FinTS standard but implements all common use cases:
|
||||
|
||||
## Features
|
||||
- Retrieving account information, balances and turnovers (Kontoumsätze und -saldo).
|
||||
- Transfer money and real-time transfers (SEPA Überweisungen und Echtzeitüberweisung).
|
||||
- Supports TAN methods chipTAN manual, Flickercode, QrCode and Photo (Matrix code), pushTAN, smsTAN and appTAN.
|
||||
|
||||
However, this is quite a low level implementation and in most cases not what you want to use.
|
||||
In most cases you want to use a higher level abstraction like [FinTs4kBankingClient](https://git.dankito.net/codinux/BankingClient).
|
||||
|
||||
## Setup
|
||||
Not uploaded to Maven Central yet, will do this the next few days!
|
||||
|
||||
Gradle:
|
||||
```
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation("net.codinux.banking:fints4k:1.0.0-Alpha-13")
|
||||
compile 'net.dankito.banking:fints4k:0.1.0'
|
||||
}
|
||||
```
|
||||
|
||||
Maven:
|
||||
```
|
||||
|
||||
// add Repository https://maven.dankito.net/api/packages/codinux/maven
|
||||
|
||||
<dependency>
|
||||
<groupId>net.dankito.banking</groupId>
|
||||
<artifactId>fints4k-jvm</artifactId>
|
||||
<version>1.0.0-Alpha-11</version>
|
||||
<artifactId>fints4k</artifactId>
|
||||
<version>0.1.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Quite outdated, have to update it. In most cases use [FinTs4kBankingClient](https://git.dankito.net/codinux/BankingClient).
|
||||
|
||||
See e.g. [JavaShowcase](fints4k/src/test/java/net/dankito/banking/fints/JavaShowcase.java) or [FinTsClientTest](fints4k/src/test/kotlin/net/dankito/banking/fints/FinTsClientTest.kt).
|
||||
|
||||
```java
|
||||
|
|
|
@ -7,9 +7,9 @@ import kotlinx.coroutines.withContext
|
|||
import kotlinx.datetime.LocalDate
|
||||
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
||||
import net.dankito.banking.client.model.response.GetAccountDataResponse
|
||||
import net.codinux.banking.fints.FinTsClient
|
||||
import net.codinux.banking.fints.callback.SimpleFinTsClientCallback
|
||||
import net.codinux.banking.fints.model.TanChallenge
|
||||
import net.dankito.banking.fints.FinTsClient
|
||||
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
|
||||
import net.dankito.banking.fints.model.TanChallenge
|
||||
import net.dankito.utils.multiplatform.extensions.millisSinceEpochAtSystemDefaultTimeZone
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.math.BigDecimal
|
||||
|
|
|
@ -5,7 +5,7 @@ import net.codinux.banking.fints4k.android.Presenter
|
|||
import net.codinux.banking.fints4k.android.R
|
||||
import net.codinux.banking.fints4k.android.adapter.viewholder.AccountTransactionsViewHolder
|
||||
import net.dankito.banking.client.model.AccountTransaction
|
||||
import net.codinux.banking.fints.util.toBigDecimal
|
||||
import net.dankito.banking.fints.util.toBigDecimal
|
||||
import net.dankito.utils.android.extensions.setTextColorToColorResource
|
||||
import net.dankito.utils.android.ui.adapter.ListRecyclerAdapter
|
||||
import org.slf4j.LoggerFactory
|
||||
|
|
|
@ -15,9 +15,9 @@ import androidx.fragment.app.DialogFragment
|
|||
import androidx.fragment.app.FragmentActivity
|
||||
import net.codinux.banking.fints4k.android.Presenter
|
||||
import net.codinux.banking.fints4k.android.R
|
||||
import net.codinux.banking.fints.model.FlickerCodeTanChallenge
|
||||
import net.codinux.banking.fints.model.ImageTanChallenge
|
||||
import net.codinux.banking.fints.model.TanChallenge
|
||||
import net.dankito.banking.fints.model.FlickerCodeTanChallenge
|
||||
import net.dankito.banking.fints.model.ImageTanChallenge
|
||||
import net.dankito.banking.fints.model.TanChallenge
|
||||
import net.dankito.utils.android.extensions.getSpannedFromHtml
|
||||
import net.dankito.utils.android.extensions.show
|
||||
|
||||
|
|
|
@ -13,12 +13,12 @@ import net.dankito.banking.client.model.CustomerAccount
|
|||
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
||||
import net.dankito.banking.client.model.parameter.RetrieveTransactions
|
||||
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
|
||||
import net.codinux.banking.fints.FinTsClient
|
||||
import net.codinux.banking.fints.callback.SimpleFinTsClientCallback
|
||||
import net.codinux.banking.fints.extensions.toStringWithMinDigits
|
||||
import net.codinux.banking.fints.getAccountData
|
||||
import net.codinux.banking.fints.model.TanChallenge
|
||||
import net.codinux.banking.fints.transferMoney
|
||||
import net.dankito.banking.fints.FinTsClient
|
||||
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
|
||||
import net.dankito.banking.fints.extensions.toStringWithMinDigits
|
||||
import net.dankito.banking.fints.getAccountData
|
||||
import net.dankito.banking.fints.model.TanChallenge
|
||||
import net.dankito.banking.fints.transferMoney
|
||||
import util.CsvWriter
|
||||
import util.OutputFormat
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package commands
|
||||
|
||||
import NativeApp
|
||||
import net.codinux.banking.fints.model.TanMethodType
|
||||
import net.dankito.banking.fints.model.TanMethodType
|
||||
|
||||
|
||||
data class CommonConfig(
|
||||
|
|
|
@ -5,10 +5,10 @@ import com.github.ajalt.clikt.core.requireObject
|
|||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
|
||||
import net.codinux.banking.fints.model.AccountData
|
||||
import net.codinux.banking.fints.model.Amount
|
||||
import net.codinux.banking.fints.model.Currency
|
||||
import net.codinux.banking.fints.model.Money
|
||||
import net.dankito.banking.fints.model.AccountData
|
||||
import net.dankito.banking.fints.model.Amount
|
||||
import net.dankito.banking.fints.model.Currency
|
||||
import net.dankito.banking.fints.model.Money
|
||||
|
||||
|
||||
class TransferMoneyCommand : CliktCommand("Transfers money from your account to a recipient", name = "transfer", printHelpOnEmptyArgs = true) {
|
||||
|
|
|
@ -12,8 +12,8 @@ import kotlinx.datetime.LocalDate
|
|||
import kotlinx.datetime.minus
|
||||
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
||||
import net.dankito.banking.client.model.parameter.RetrieveTransactions
|
||||
import net.codinux.banking.fints.model.TanMethodType
|
||||
import net.codinux.banking.fints.extensions.todayAtEuropeBerlin
|
||||
import net.dankito.banking.fints.model.TanMethodType
|
||||
import net.dankito.banking.fints.extensions.todayAtEuropeBerlin
|
||||
import util.OutputFormat
|
||||
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package net.codinux.banking.fints
|
||||
package net.dankito.banking.fints
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
||||
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
|
||||
import net.dankito.banking.client.model.response.GetAccountDataResponse
|
||||
import net.dankito.banking.client.model.response.TransferMoneyResponse
|
||||
import net.codinux.banking.fints.model.Money
|
||||
import net.dankito.banking.fints.model.Money
|
||||
|
||||
|
||||
fun FinTsClient.getAccountData(bankCode: String, loginName: String, password: String): GetAccountDataResponse {
|
|
@ -41,8 +41,8 @@ open class CsvWriter {
|
|||
protected open suspend fun writeToFile(stream: AsyncStream, valueSeparator: String, customer: CustomerAccount, account: BankAccount, transaction: AccountTransaction) {
|
||||
val amount = if (valueSeparator == ";") transaction.amount.amount.string.replace('.', ',') else transaction.amount.amount.string.replace(',', '.')
|
||||
|
||||
stream.writeString(listOf(customer.bankName, account.identifier, transaction.valueDate, amount, transaction.amount.currency, ensureNotNull(transaction.postingText), wrap(transaction.reference ?: ""),
|
||||
ensureNotNull(transaction.otherPartyName), ensureNotNull(transaction.otherPartyBankId), ensureNotNull(transaction.otherPartyAccountId)).joinToString(valueSeparator))
|
||||
stream.writeString(listOf(customer.bankName, account.identifier, transaction.valueDate, amount, transaction.amount.currency, ensureNotNull(transaction.bookingText), wrap(transaction.reference),
|
||||
ensureNotNull(transaction.otherPartyName), ensureNotNull(transaction.otherPartyBankCode), ensureNotNull(transaction.otherPartyAccountId)).joinToString(valueSeparator))
|
||||
|
||||
stream.writeString(NewLine)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.dankito.banking.client.model.AccountTransaction
|
||||
import net.codinux.banking.fints.model.TanChallenge
|
||||
import net.dankito.banking.fints.model.TanChallenge
|
||||
import react.*
|
||||
import react.dom.*
|
||||
import styled.styledDiv
|
||||
|
|
|
@ -2,8 +2,8 @@ import io.ktor.util.encodeBase64
|
|||
import kotlinx.html.InputType
|
||||
import kotlinx.html.js.onChangeFunction
|
||||
import kotlinx.html.style
|
||||
import net.codinux.banking.fints.model.ImageTanChallenge
|
||||
import net.codinux.banking.fints.model.TanChallenge
|
||||
import net.dankito.banking.fints.model.ImageTanChallenge
|
||||
import net.dankito.banking.fints.model.TanChallenge
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import react.Props
|
||||
import react.RBuilder
|
||||
|
|
|
@ -6,11 +6,11 @@ import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
|||
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
|
||||
import net.dankito.banking.client.model.response.GetAccountDataResponse
|
||||
import net.dankito.banking.client.model.response.TransferMoneyResponse
|
||||
import net.codinux.banking.fints.FinTsClient
|
||||
import net.codinux.banking.fints.callback.SimpleFinTsClientCallback
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.codinux.banking.fints.webclient.KtorWebClient
|
||||
import net.codinux.banking.fints.webclient.ProxyingWebClient
|
||||
import net.dankito.banking.fints.FinTsClient
|
||||
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
|
||||
import net.dankito.banking.fints.model.*
|
||||
import net.dankito.banking.fints.webclient.KtorWebClient
|
||||
import net.dankito.banking.fints.webclient.ProxyingWebClient
|
||||
import net.dankito.utils.multiplatform.log.LoggerFactory
|
||||
|
||||
open class Presenter {
|
||||
|
|
16
build.gradle
16
build.gradle
|
@ -1,16 +1,16 @@
|
|||
// TODO: move to versions.gradle
|
||||
ext {
|
||||
appVersionName = "1.0.0-Alpha-16-SNAPSHOT"
|
||||
appVersionName = '1.0.0-Alpha-11'
|
||||
|
||||
|
||||
/* Test */
|
||||
|
||||
assertJVersion = "3.12.2"
|
||||
assertJVersion = '3.12.2'
|
||||
|
||||
mockitoVersion = "2.22.0"
|
||||
mockitoKotlinVersion = "1.6.0"
|
||||
mockitoVersion = '2.22.0'
|
||||
mockitoKotlinVersion = '1.6.0'
|
||||
|
||||
logbackVersion = "1.2.3"
|
||||
logbackVersion = '1.2.3'
|
||||
}
|
||||
|
||||
buildscript {
|
||||
|
@ -48,10 +48,4 @@ task publishAllToMavenLocal {
|
|||
dependsOn = [
|
||||
"fints4k:publishToMavenLocal",
|
||||
]
|
||||
}
|
||||
|
||||
task publishAll {
|
||||
dependsOn = [
|
||||
"fints4k:publish",
|
||||
]
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
| | |
|
||||
|--------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
|
||||
| Geschäftsvorfall | Business Transaction / Job |
|
||||
| Verwendungszweck | Remittance information, reference, (payment) purpose |
|
||||
| Überweisung | Remittance (techn.), money transfer, bank transfer, wire transfer (Amerik.), credit transfer |
|
||||
| Buchungsschlüssel | posting key |
|
||||
| Buchungstext | posting text |
|
||||
| | |
|
||||
| Ende-zu-Ende Referenz | End to End Reference |
|
||||
| Kundenreferenz | Reference of the submitting customer |
|
||||
| Mandatsreferenz | mandate reference |
|
||||
| Creditor Identifier | Creditor Identifier |
|
||||
| Originators Identification Code | Originators Identification Code |
|
||||
| Compensation Amount | Compensation Amount |
|
||||
| Original Amount | Original Amount |
|
||||
| Abweichender Überweisender (CT-AT08) / Abweichender Zahlungsempfänger (DD-AT38) | payer’s/debtor’s reference party (for credit transfer / payee’s / creditor’s reference party (for a direct debit) |
|
||||
| Abweichender Zahlungsempfänger (CT-AT28) / Abweichender Zahlungspflichtiger (DDAT15) | payee’s/creditor’s reference party / payer’s/debtor’s reference party |
|
||||
| | |
|
||||
| Überweisender | Payer, debtor |
|
||||
| Zahlungsempfänger | Payee, creditor |
|
||||
| Zahlungseingang | Payment receipt |
|
||||
| Lastschrift | direct debit |
|
||||
| | |
|
||||
| | |
|
||||
| Primanoten-Nr. | Journal no. |
|
||||
| | |
|
|
@ -8,15 +8,11 @@ plugins {
|
|||
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(8)
|
||||
|
||||
compilerOptions {
|
||||
// suppresses compiler warning: [EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING] 'expect'/'actual' classes (including interfaces, objects, annotations, enums, and 'actual' typealiases) are in Beta.
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
|
||||
if (System.getProperty("idea.debugger.dispatch.addr") != null) {
|
||||
freeCompilerArgs.add("-Xdebug")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -38,7 +34,7 @@ kotlin {
|
|||
browser {
|
||||
testTask {
|
||||
useKarma {
|
||||
useChromeHeadless()
|
||||
// useChromeHeadless()
|
||||
useFirefoxHeadless()
|
||||
}
|
||||
}
|
||||
|
@ -47,13 +43,10 @@ kotlin {
|
|||
nodejs()
|
||||
}
|
||||
|
||||
// wasmJs() // ktor is not available for wasmJs yet
|
||||
|
||||
|
||||
linuxX64()
|
||||
mingwX64()
|
||||
|
||||
iosX64()
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
macosX64()
|
||||
|
@ -76,9 +69,8 @@ kotlin {
|
|||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinxSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
|
||||
|
||||
implementation("net.codinux.log:klf:$klfVersion")
|
||||
implementation("net.codinux.log:kmp-log:$klfVersion")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
package net.codinux.banking.fints.config
|
||||
|
||||
import net.codinux.banking.fints.model.ProductData
|
||||
|
||||
data class FinTsClientOptions(
|
||||
|
||||
/**
|
||||
* If FinTS messages sent to and received from bank servers and errors should be collected. They are then accessible
|
||||
* via [net.codinux.banking.fints.response.client.FinTsClientResponse.messageLog].
|
||||
*
|
||||
* Set to false by default.
|
||||
*/
|
||||
val collectMessageLog: Boolean = false,
|
||||
|
||||
/**
|
||||
* If set to true then [net.codinux.banking.fints.callback.FinTsClientCallback.messageLogAdded] get fired when a
|
||||
* FinTS message get sent to bank server, a FinTS message is received from bank server or an error occurred.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
val fireCallbackOnMessageLogs: Boolean = false,
|
||||
|
||||
/**
|
||||
* If sensitive data like user name, password, login name should be removed from FinTS messages before being logged.
|
||||
*
|
||||
* Defaults to true.
|
||||
*/
|
||||
val removeSensitiveDataFromMessageLog: Boolean = true,
|
||||
|
||||
val appendFinTsMessagesToLog: Boolean = false,
|
||||
|
||||
val closeDialogs: Boolean = true,
|
||||
|
||||
val version: String = "1.0.0", // TODO: get version dynamically
|
||||
val productName: String = "15E53C26816138699C7B6A3E8" // TODO: extract constant // TODO: get product number for fints4k and Bankmeister (if we stick with that name)
|
||||
) {
|
||||
|
||||
val product: ProductData by lazy { ProductData(productName, version) }
|
||||
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package net.codinux.banking.fints.extensions
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.plus
|
||||
|
||||
|
||||
// should actually be named `now()`, but that name is already shadowed by deprecated Instant.Companion.now() method
|
||||
fun Instant.Companion.nowExt(): Instant = Clock.System.now()
|
||||
|
||||
fun Instant.plusMinutes(minutes: Int) = this.plus(minutes, DateTimeUnit.MINUTE)
|
|
@ -1,11 +0,0 @@
|
|||
package net.codinux.banking.fints.extensions
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
fun randomWithSeed(): Random = Random(randomSeed())
|
||||
|
||||
fun randomSeed(): Long {
|
||||
return Instant.nowExt().nanosecondsOfSecond.toLong() + Instant.nowExt().toEpochMilliseconds()
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package net.codinux.banking.fints.extensions
|
||||
|
||||
import kotlinx.datetime.TimeZone
|
||||
|
||||
|
||||
val TimeZone.Companion.EuropeBerlin: TimeZone
|
||||
get() = TimeZone.of("Europe/Berlin")
|
|
@ -1,10 +0,0 @@
|
|||
package net.codinux.banking.fints.log
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
interface IMessageLogAppender {
|
||||
|
||||
fun logError(loggingClass: KClass<*>, message: String, e: Throwable? = null)
|
||||
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package net.codinux.banking.fints.log
|
||||
|
||||
import net.codinux.banking.fints.model.AccountData
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.MessageType
|
||||
import net.codinux.banking.fints.model.JobContextType
|
||||
|
||||
|
||||
class MessageContext(
|
||||
val jobType: JobContextType,
|
||||
val messageType: MessageType,
|
||||
val jobNumber: Int,
|
||||
val dialogNumber: Int,
|
||||
val messageNumber: Int,
|
||||
val bank: BankData,
|
||||
val account: AccountData?
|
||||
) {
|
||||
override fun toString() = "${jobNumber}_${dialogNumber}_$messageNumber ${bank.bankCode} $jobType $messageType"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.datenelemente
|
||||
|
||||
import net.codinux.banking.fints.messages.Existenzstatus
|
||||
import net.codinux.banking.fints.messages.Nachrichtenteil
|
||||
|
||||
|
||||
abstract class DatenelementBase(val existenzstatus: Existenzstatus) : Nachrichtenteil()
|
|
@ -1,21 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.datenelemente.implementierte
|
||||
|
||||
|
||||
enum class Dialogsprache(override val code: String) : ICodeEnum {
|
||||
|
||||
/**
|
||||
* Der Kunde darf lediglich ein Sprachkennzeichen einstellen, das im Rahmen
|
||||
* der BPD vom Kreditinstitut an das Kundensystem übermittelt wurde.
|
||||
* Wenn noch keine BPD vorliegen, sollte das Kundenprodukt mit Hilfe eines
|
||||
* anonymen Dialogs die aktuelle BPD des Instituts ermitteln und die Standardsprache des Instituts einstellen, die in den Bankparameterdaten mitgeteilt
|
||||
* wird. Falls die BPD nicht abgerufen werden kann, ist der Wert „0“ einzustellen. Das Kreditinstitut antwortet in diesem Fall in seiner Standardsprache.
|
||||
*/
|
||||
Default("0"),
|
||||
|
||||
German("1"),
|
||||
|
||||
English("2"),
|
||||
|
||||
French("3")
|
||||
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.datenelemente.implementierte
|
||||
|
||||
/**
|
||||
* Information darüber, ob die Kundensystem-ID erforderlich ist.
|
||||
*/
|
||||
enum class KundensystemStatusWerte(override val code: String) : ICodeEnum {
|
||||
|
||||
/**
|
||||
* Kundensystem-ID wird nicht benötigt (HBCI DDV-Verfahren und
|
||||
* chipkartenbasierte Verfahren ab Sicherheitsprofil-Version 3)
|
||||
* und PinTan bis über HKSYN eine Kundensystem-ID vom Bankserver abgerufen wurde.
|
||||
*/
|
||||
NichtBenoetigt("0"),
|
||||
|
||||
/**
|
||||
* Kundensystem-ID wird benötigt (sonstige HBCI RAH- /
|
||||
* RDH- und PIN/TAN-Verfahren)
|
||||
*/
|
||||
Benoetigt("1")
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.datenelemente.implementierte
|
||||
|
||||
import net.codinux.banking.fints.messages.Existenzstatus
|
||||
import net.codinux.banking.fints.messages.datenelemente.basisformate.TextDatenelement
|
||||
|
||||
|
||||
open class NotAllowedDatenelement : TextDatenelement("", Existenzstatus.NotAllowed)
|
|
@ -1,7 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.datenelemente.implementierte.account
|
||||
|
||||
import net.codinux.banking.fints.messages.Existenzstatus
|
||||
import net.codinux.banking.fints.messages.datenelemente.basisformate.AlphanumerischesDatenelement
|
||||
|
||||
|
||||
open class BIC(bic: String, existenzstatus: Existenzstatus) : AlphanumerischesDatenelement(bic, existenzstatus, 11)
|
|
@ -1,7 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.datenelemente.implementierte.account
|
||||
|
||||
import net.codinux.banking.fints.messages.Existenzstatus
|
||||
import net.codinux.banking.fints.messages.datenelemente.basisformate.AlphanumerischesDatenelement
|
||||
|
||||
|
||||
open class IBAN(iban: String, existenzstatus: Existenzstatus) : AlphanumerischesDatenelement(iban, existenzstatus, 34)
|
|
@ -1,36 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.datenelemente.implementierte.tan
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.dankito.banking.client.model.BankAccountIdentifier
|
||||
|
||||
@Serializable
|
||||
open class MobilePhoneTanMedium(
|
||||
val concealedPhoneNumber: String?,
|
||||
val phoneNumber: String?,
|
||||
val smsDebitAccount: BankAccountIdentifier? = null
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is MobilePhoneTanMedium) return false
|
||||
|
||||
if (concealedPhoneNumber != other.concealedPhoneNumber) return false
|
||||
if (phoneNumber != other.phoneNumber) return false
|
||||
if (smsDebitAccount != other.smsDebitAccount) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = concealedPhoneNumber.hashCode()
|
||||
result = 31 * result + phoneNumber.hashCode()
|
||||
result = 31 * result + smsDebitAccount.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return phoneNumber ?: concealedPhoneNumber ?: ""
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.segmente.id
|
||||
|
||||
|
||||
interface ISegmentId {
|
||||
|
||||
val id: String
|
||||
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.segmente.implementierte
|
||||
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.DialogId
|
||||
import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.Segmentkopf
|
||||
import net.codinux.banking.fints.messages.segmente.Segment
|
||||
import net.codinux.banking.fints.messages.segmente.id.CustomerSegmentId
|
||||
import net.codinux.banking.fints.model.DialogContext
|
||||
|
||||
|
||||
class Dialogende(
|
||||
segmentNumber: Int,
|
||||
dialogContext: DialogContext
|
||||
|
||||
) : Segment(listOf(
|
||||
Segmentkopf(CustomerSegmentId.DialogEnd, 1, segmentNumber),
|
||||
DialogId(dialogContext.dialogId)
|
||||
))
|
|
@ -1,28 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.segmente.implementierte
|
||||
|
||||
import net.codinux.banking.fints.messages.Existenzstatus
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.KundenID
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.KundensystemID
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.KundensystemStatus
|
||||
import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.Kreditinstitutskennung
|
||||
import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.Segmentkopf
|
||||
import net.codinux.banking.fints.messages.segmente.Segment
|
||||
import net.codinux.banking.fints.messages.segmente.id.CustomerSegmentId
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.MessageBaseData
|
||||
|
||||
|
||||
open class IdentifikationsSegment(
|
||||
segmentNumber: Int,
|
||||
bank: BankData
|
||||
|
||||
) : Segment(listOf(
|
||||
Segmentkopf(CustomerSegmentId.Identification, 2, segmentNumber),
|
||||
Kreditinstitutskennung(bank.countryCode, bank.bankCodeForOnlineBanking),
|
||||
KundenID(bank.customerId),
|
||||
KundensystemID(bank.customerSystemId),
|
||||
KundensystemStatus(bank.customerSystemStatus, Existenzstatus.Mandatory)
|
||||
)) {
|
||||
|
||||
constructor(segmentNumber: Int, baseData: MessageBaseData) : this(segmentNumber, baseData.bank)
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package net.codinux.banking.fints.messages.segmente.implementierte.depot
|
||||
|
||||
import net.codinux.banking.fints.messages.Existenzstatus
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.Aufsetzpunkt
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.account.MaximaleAnzahlEintraege
|
||||
import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.Segmentkopf
|
||||
import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.account.Kontoverbindung
|
||||
import net.codinux.banking.fints.messages.segmente.Segment
|
||||
import net.codinux.banking.fints.messages.segmente.id.CustomerSegmentId
|
||||
import net.codinux.banking.fints.model.AccountData
|
||||
|
||||
/**
|
||||
* Nr. Name Version Typ Format Länge Status Anzahl Restriktionen
|
||||
1 Segmentkopf 1 DEG M 1
|
||||
2 Depot 3 DEG ktv # M 1
|
||||
3 Währung der Depotaufstellung 1 DE cur # C 1 O: „Währung der Depotaufstellung wählbar“ (BPD) = „J“; N: sonst
|
||||
4 Kursqualität 2 DE code 1 C 1 1,2 O: „Kursqualität wählbar“ (BPD) = „J“; N: sonst
|
||||
5 Maximale Anzahl Einträge 1 DE num ..4 C 1 >0 O: „Eingabe Anzahl Einträge erlaubt“ (BPD) = „J“; N: sonst
|
||||
6 Aufsetzpunkt 1 DE an ..35 C 1 M: vom Institut wurde ein Aufsetzpunkt rückgemeldet N: sonst
|
||||
*/
|
||||
class Depotaufstellung(
|
||||
segmentNumber: Int,
|
||||
account: AccountData,
|
||||
// parameter: GetAccountTransactionsParameter
|
||||
): Segment(listOf(
|
||||
Segmentkopf(CustomerSegmentId.SecuritiesAccountBalance, 6, segmentNumber),
|
||||
Kontoverbindung(account),
|
||||
// TODO:
|
||||
// 3. Währung der Depotaufstellung
|
||||
// 4. Kursqualität
|
||||
// 5. Maximale Anzahl Einträge
|
||||
// 6. Aufsetzpunkt
|
||||
|
||||
// MaximaleAnzahlEintraege(parameter), // TODO: this is wrong, it only works for HKKAZ
|
||||
MaximaleAnzahlEintraege(null, Existenzstatus.Optional),
|
||||
Aufsetzpunkt(null, Existenzstatus.Optional) // will be set dynamically, see MessageBuilder.rebuildMessageWithContinuationId(); M: vom Institut wurde ein Aufsetzpunkt rückgemeldet. N: sonst
|
||||
))
|
|
@ -1,164 +0,0 @@
|
|||
package net.codinux.banking.fints.model
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.codinux.banking.fints.extensions.UnixEpochStart
|
||||
|
||||
|
||||
open class AccountTransaction(
|
||||
val account: AccountData,
|
||||
val amount: Money,
|
||||
val reference: String?, // that was also new to me that reference may is null
|
||||
|
||||
val bookingDate: LocalDate,
|
||||
val valueDate: LocalDate,
|
||||
|
||||
/**
|
||||
* Name des Überweisenden oder Zahlungsempfängers
|
||||
*/
|
||||
val otherPartyName: String?,
|
||||
/**
|
||||
* BIC des Überweisenden / Zahlungsempfängers
|
||||
*/
|
||||
val otherPartyBankId: String?,
|
||||
/**
|
||||
* IBAN des Überweisenden oder Zahlungsempfängers
|
||||
*/
|
||||
val otherPartyAccountId: String?,
|
||||
|
||||
/**
|
||||
* Buchungstext, z. B. DAUERAUFTRAG, BARGELDAUSZAHLUNG, ONLINE-UEBERWEISUNG, FOLGELASTSCHRIFT, ...
|
||||
*/
|
||||
val postingText: String?,
|
||||
|
||||
val openingBalance: Money?,
|
||||
val closingBalance: Money?,
|
||||
|
||||
/**
|
||||
* Auszugsnummer
|
||||
*/
|
||||
val statementNumber: Int,
|
||||
/**
|
||||
* Blattnummer
|
||||
*/
|
||||
val sheetNumber: Int?,
|
||||
|
||||
/**
|
||||
* Kundenreferenz.
|
||||
*/
|
||||
val customerReference: String?,
|
||||
/**
|
||||
* Bankreferenz
|
||||
*/
|
||||
val bankReference: String?,
|
||||
/**
|
||||
* Währungsart und Umsatzbetrag in Ursprungswährung
|
||||
*/
|
||||
val furtherInformation: String?,
|
||||
|
||||
|
||||
/* Remittance information */
|
||||
|
||||
val endToEndReference: String?,
|
||||
val mandateReference: String?,
|
||||
val creditorIdentifier: String?,
|
||||
val originatorsIdentificationCode: String?,
|
||||
|
||||
/**
|
||||
* Summe aus Auslagenersatz und Bearbeitungsprovision bei einer nationalen Rücklastschrift
|
||||
* sowie optionalem Zinsausgleich.
|
||||
*/
|
||||
val compensationAmount: String?,
|
||||
/**
|
||||
* Betrag der ursprünglichen Lastschrift
|
||||
*/
|
||||
val originalAmount: String?,
|
||||
/**
|
||||
* Abweichender Überweisender oder Zahlungsempfänger
|
||||
*/
|
||||
val deviantOriginator: String?,
|
||||
/**
|
||||
* Abweichender Zahlungsempfänger oder Zahlungspflichtiger
|
||||
*/
|
||||
val deviantRecipient: String?,
|
||||
val referenceWithNoSpecialType: String?,
|
||||
|
||||
/**
|
||||
* Primanoten-Nr.
|
||||
*/
|
||||
val journalNumber: String?,
|
||||
/**
|
||||
* Bei R-Transaktionen siehe Tabelle der
|
||||
* SEPA-Rückgabecodes, bei SEPALastschriften siehe optionale Belegung
|
||||
* bei GVC 104 und GVC 105 (GVC = Geschäftsvorfallcode)
|
||||
*/
|
||||
val textKeyAddition: String?,
|
||||
|
||||
/**
|
||||
* Referenznummer, die vom Sender als eindeutige Kennung für die Nachricht vergeben wurde
|
||||
* (z.B. als Referenz auf stornierte Nachrichten).
|
||||
*/
|
||||
val orderReferenceNumber: String?,
|
||||
/**
|
||||
* Bezugsreferenz
|
||||
*/
|
||||
val referenceNumber: String?,
|
||||
|
||||
/**
|
||||
* Storno, ob die Buchung storniert wurde(?).
|
||||
* Aus:
|
||||
* „RC“ = Storno Haben
|
||||
* „RD“ = Storno Soll
|
||||
*/
|
||||
val isReversal: Boolean
|
||||
) {
|
||||
|
||||
// for object deserializers
|
||||
internal constructor() : this(AccountData(), Money(Amount.Zero, ""), "", UnixEpochStart, UnixEpochStart, null, null, null, null)
|
||||
|
||||
constructor(account: AccountData, amount: Money, unparsedReference: String, bookingDate: LocalDate, valueDate: LocalDate, otherPartyName: String?, otherPartyBankId: String?, otherPartyAccountId: String?, postingText: String? = null)
|
||||
: this(account, amount, unparsedReference, bookingDate, valueDate, otherPartyName, otherPartyBankId, otherPartyAccountId, postingText,
|
||||
null, null, 0, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null,
|
||||
"", null, null, "", null, false)
|
||||
|
||||
|
||||
open val showOtherPartyName: Boolean
|
||||
get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is AccountTransaction) return false
|
||||
|
||||
if (account != other.account) return false
|
||||
if (amount != other.amount) return false
|
||||
if (reference != other.reference) return false
|
||||
if (bookingDate != other.bookingDate) return false
|
||||
if (otherPartyName != other.otherPartyName) return false
|
||||
if (otherPartyBankId != other.otherPartyBankId) return false
|
||||
if (otherPartyAccountId != other.otherPartyAccountId) return false
|
||||
if (postingText != other.postingText) return false
|
||||
if (valueDate != other.valueDate) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = account.hashCode()
|
||||
result = 31 * result + amount.hashCode()
|
||||
result = 31 * result + reference.hashCode()
|
||||
result = 31 * result + bookingDate.hashCode()
|
||||
result = 31 * result + otherPartyName.hashCode()
|
||||
result = 31 * result + otherPartyBankId.hashCode()
|
||||
result = 31 * result + otherPartyAccountId.hashCode()
|
||||
result = 31 * result + postingText.hashCode()
|
||||
result = 31 * result + valueDate.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$valueDate $amount $otherPartyName: $reference"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package net.codinux.banking.fints.model
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import net.codinux.banking.fints.extensions.nowExt
|
||||
import net.codinux.banking.fints.log.MessageContext
|
||||
import net.codinux.banking.fints.response.segments.ReceivedSegment
|
||||
|
||||
|
||||
open class MessageLogEntry(
|
||||
open val type: MessageLogEntryType,
|
||||
open val context: MessageContext,
|
||||
open val messageTrace: String,
|
||||
open val message: String,
|
||||
open val messageWithoutSensitiveData: String? = null,
|
||||
open val error: Throwable? = null,
|
||||
/**
|
||||
* Parsed received segments.
|
||||
*
|
||||
* Is only set if [type] is set to [MessageLogEntryType.Received] and response parsing was successful.
|
||||
*/
|
||||
open val parsedSegments: List<ReceivedSegment> = emptyList(),
|
||||
open val time: Instant = Instant.nowExt()
|
||||
) {
|
||||
|
||||
val messageIncludingMessageTrace: String
|
||||
get() = messageTrace + "\n" + message
|
||||
|
||||
override fun toString(): String {
|
||||
return "$context $type $message"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package net.codinux.banking.fints.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
open class PinInfo(
|
||||
val minPinLength: Int?,
|
||||
val maxPinLength: Int?,
|
||||
val minTanLength: Int?,
|
||||
val userIdHint: String?,
|
||||
val customerIdHint: String?
|
||||
)
|
|
@ -1,116 +0,0 @@
|
|||
package net.codinux.banking.fints.model
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import net.codinux.banking.fints.extensions.nowExt
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.response.BankResponse
|
||||
import net.codinux.banking.fints.response.client.FinTsClientResponse
|
||||
import net.codinux.log.Log
|
||||
|
||||
|
||||
open class TanChallenge(
|
||||
val forAction: ActionRequiringTan,
|
||||
val messageToShowToUser: String,
|
||||
val challenge: String,
|
||||
val tanMethod: TanMethod,
|
||||
val tanMediaIdentifier: String?,
|
||||
val bank: BankData,
|
||||
val account: AccountData? = null,
|
||||
/**
|
||||
* Datum und Uhrzeit, bis zu welchem Zeitpunkt eine TAN auf Basis der gesendeten Challenge gültig ist. Nach Ablauf der Gültigkeitsdauer wird die entsprechende TAN entwertet.
|
||||
*
|
||||
* In server's time zone, that is Europe/Berlin.
|
||||
*/
|
||||
val tanExpirationTime: Instant? = null,
|
||||
val challengeCreationTimestamp: Instant = Instant.nowExt()
|
||||
) {
|
||||
|
||||
var enterTanResult: EnterTanResult? = null
|
||||
private set
|
||||
|
||||
open val isEnteringTanDone: Boolean
|
||||
get() = enterTanResult != null
|
||||
|
||||
private val tanExpiredCallbacks = mutableListOf<() -> Unit>()
|
||||
|
||||
private val userApprovedDecoupledTanCallbacks = mutableListOf<() -> Unit>()
|
||||
|
||||
|
||||
fun userEnteredTan(enteredTan: String) {
|
||||
this.enterTanResult = EnterTanResult(enteredTan.replace(" ", ""))
|
||||
}
|
||||
|
||||
internal fun userApprovedDecoupledTan(responseAfterApprovingDecoupledTan: BankResponse) {
|
||||
this.enterTanResult = EnterTanResult(null, true, responseAfterApprovingDecoupledTan)
|
||||
|
||||
userApprovedDecoupledTanCallbacks.toTypedArray().forEach { // copy to avoid ConcurrentModificationException
|
||||
try {
|
||||
it.invoke()
|
||||
} catch (e: Throwable) {
|
||||
Log.error(e) { "Could not call userApprovedDecoupledTanCallback" }
|
||||
}
|
||||
}
|
||||
clearUserApprovedDecoupledTanCallbacks()
|
||||
}
|
||||
|
||||
fun userDidNotEnterTan() {
|
||||
clearUserApprovedDecoupledTanCallbacks()
|
||||
|
||||
this.enterTanResult = EnterTanResult(null)
|
||||
}
|
||||
|
||||
internal fun tanExpired() {
|
||||
tanExpiredCallbacks.toTypedArray().forEach {
|
||||
try {
|
||||
it.invoke()
|
||||
} catch (e: Throwable) {
|
||||
Log.error(e) { "Could not call tanExpiredCallback" }
|
||||
}
|
||||
}
|
||||
|
||||
clearTanExpiredCallbacks()
|
||||
|
||||
userDidNotEnterTan()
|
||||
}
|
||||
|
||||
fun userAsksToChangeTanMethod(changeTanMethodTo: TanMethod) {
|
||||
clearUserApprovedDecoupledTanCallbacks()
|
||||
|
||||
this.enterTanResult = EnterTanResult(null, changeTanMethodTo = changeTanMethodTo)
|
||||
}
|
||||
|
||||
fun userAsksToChangeTanMedium(changeTanMediumTo: TanMedium, changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?) {
|
||||
clearUserApprovedDecoupledTanCallbacks()
|
||||
|
||||
this.enterTanResult = EnterTanResult(null, changeTanMediumTo = changeTanMediumTo, changeTanMediumResultCallback = changeTanMediumResultCallback)
|
||||
}
|
||||
|
||||
|
||||
fun addTanExpiredCallback(callback: () -> Unit) {
|
||||
if (isEnteringTanDone == false) {
|
||||
this.tanExpiredCallbacks.add(callback)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun clearTanExpiredCallbacks() {
|
||||
tanExpiredCallbacks.clear()
|
||||
}
|
||||
|
||||
fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) {
|
||||
if (isEnteringTanDone == false) {
|
||||
this.userApprovedDecoupledTanCallbacks.add(callback)
|
||||
} else if (enterTanResult != null && enterTanResult!!.userApprovedDecoupledTan == true) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun clearUserApprovedDecoupledTanCallbacks() {
|
||||
userApprovedDecoupledTanCallbacks.clear()
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$tanMethod (medium: $tanMediaIdentifier): $messageToShowToUser"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package net.codinux.banking.fints.response.client
|
||||
|
||||
import net.codinux.banking.fints.model.JobContext
|
||||
import net.codinux.banking.fints.response.BankResponse
|
||||
import net.codinux.banking.fints.response.segments.TanMediaList
|
||||
|
||||
|
||||
open class GetTanMediaListResponse(
|
||||
context: JobContext,
|
||||
response: BankResponse,
|
||||
val tanMediaList: TanMediaList?
|
||||
)
|
||||
: FinTsClientResponse(context, response)
|
|
@ -1,44 +0,0 @@
|
|||
package net.codinux.banking.fints.response.segments
|
||||
|
||||
|
||||
open class JobParameters(
|
||||
open val jobName: String,
|
||||
/**
|
||||
* Höchstens zulässige Anzahl an Segmenten der jeweiligen Auftragsart je
|
||||
* Kundennachricht. Übersteigt die Anzahl der vom Kunden übermittelten Segmente pro Auftragsart die zugelassene Maximalanzahl, so wird die gesamte
|
||||
* Nachricht abgelehnt.
|
||||
*/
|
||||
open val maxCountJobs: Int,
|
||||
|
||||
/**
|
||||
* Anzahl der Signaturen, die zur Ausführung eines Geschäftsvorfalls als erforderlich definiert ist.
|
||||
* Falls 0 angegeben ist, handelt es sich um einen nicht signierungspflichtigen
|
||||
* Geschäftsvorfall, der auch über einen anonymen Zugang ohne Signierungsmöglichkeit ausgeführt werden kann.
|
||||
* Falls die Anzahl der benötigten Signaturen größer als 1 ist, bedeutet dies,
|
||||
* dass dieser Geschäftsvorfall zusätzlich von mindestens einem anderen berechtigten Benutzer signiert werden muss, über dessen Identität in den UPD
|
||||
* jedoch nichts ausgesagt wird.
|
||||
* In bestimmten Fällen ist die Anzahl der Signaturen durch die Art des Geschäftsvorfalls vorgegeben (z. B. sind bei Keymanagement-Aufträgen nicht
|
||||
* mehrere Signaturen möglich).
|
||||
*
|
||||
* (Ist meistens 1, da PinTan Nachrichten außer im Anonymen Dialog immer eigene Signatur brauchen.)
|
||||
*/
|
||||
open val minimumCountSignatures: Int,
|
||||
open val securityClass: Int?,
|
||||
segmentString: String
|
||||
)
|
||||
: ReceivedSegment(segmentString) {
|
||||
|
||||
|
||||
internal constructor() : this("", 0, 0, null, "0:0:0") // for object deserializers
|
||||
|
||||
|
||||
constructor(parameters: JobParameters)
|
||||
: this(parameters.jobName, parameters.maxCountJobs, parameters.minimumCountSignatures,
|
||||
parameters.securityClass, parameters.segmentString)
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$jobName $segmentVersion"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package net.codinux.banking.fints.response.segments
|
||||
|
||||
|
||||
open class RetrieveAccountTransactionsParameters(
|
||||
parameters: JobParameters,
|
||||
open val serverTransactionsRetentionDays: Int,
|
||||
open val settingCountEntriesAllowed: Boolean,
|
||||
open val settingAllAccountAllowed: Boolean,
|
||||
open val supportedCamtDataFormats: List<String> = emptyList()
|
||||
) : JobParameters(parameters) {
|
||||
|
||||
internal constructor() : this(JobParameters(), -1, false, false) // for object deserializers
|
||||
|
||||
// for languages not supporting default parameters
|
||||
constructor(parameters: JobParameters, serverTransactionsRetentionDays: Int, settingCountEntriesAllowed: Boolean, settingAllAccountAllowed: Boolean) :
|
||||
this(parameters, serverTransactionsRetentionDays, settingCountEntriesAllowed, settingAllAccountAllowed, emptyList())
|
||||
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package net.codinux.banking.fints.response.segments
|
||||
|
||||
import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings
|
||||
|
||||
class SecuritiesAccountBalanceSegment(
|
||||
val statementOfHoldings: List<StatementOfHoldings>,
|
||||
segmentString: String
|
||||
)
|
||||
: ReceivedSegment(segmentString)
|
|
@ -1,30 +0,0 @@
|
|||
package net.codinux.banking.fints.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
|
||||
object BankDataSerializer : KSerializer<BankData> {
|
||||
|
||||
private val serializer = SerializedFinTsData.serializer()
|
||||
|
||||
private val mapper = SerializedFinTsDataMapper()
|
||||
|
||||
|
||||
override val descriptor: SerialDescriptor = serializer.descriptor
|
||||
|
||||
override fun serialize(encoder: Encoder, value: BankData) {
|
||||
val surrogate = mapper.map(value)
|
||||
|
||||
encoder.encodeSerializableValue(serializer, surrogate)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): BankData {
|
||||
val surrogate = decoder.decodeSerializableValue(serializer)
|
||||
|
||||
return mapper.map(surrogate)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package net.codinux.banking.fints.serialization
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.log.logger
|
||||
|
||||
object FinTsModelSerializer {
|
||||
|
||||
private val json: Json by lazy {
|
||||
Json { this.ignoreUnknownKeys = true }
|
||||
}
|
||||
|
||||
private val prettyPrintJson by lazy {
|
||||
Json {
|
||||
this.ignoreUnknownKeys = true
|
||||
this.prettyPrint = true
|
||||
}
|
||||
}
|
||||
|
||||
private val mapper = SerializedFinTsDataMapper()
|
||||
|
||||
private val log by logger()
|
||||
|
||||
|
||||
fun serializeToJson(bank: BankData, prettyPrint: Boolean = false): String? {
|
||||
return try {
|
||||
val serializableData = mapper.map(bank)
|
||||
|
||||
val json = if (prettyPrint) prettyPrintJson else json
|
||||
|
||||
json.encodeToString(serializableData)
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not map fints4k model to JSON" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun deserializeFromJson(serializedFinTsData: String): BankData? = try {
|
||||
val serializedData = json.decodeFromString<SerializedFinTsData>(serializedFinTsData)
|
||||
|
||||
mapper.map(serializedData)
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not deserialize BankData from JSON:\n$serializedFinTsData"}
|
||||
null
|
||||
}
|
||||
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package net.codinux.banking.fints.serialization
|
||||
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.*
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.model.AccountData
|
||||
import net.codinux.banking.fints.model.PinInfo
|
||||
import net.codinux.banking.fints.model.TanMethod
|
||||
import net.codinux.banking.fints.serialization.jobparameter.DetailedSerializableJobParameters
|
||||
import net.codinux.banking.fints.serialization.jobparameter.SerializableJobParameters
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
class SerializedFinTsData(
|
||||
val bankCode: String,
|
||||
val customerId: String,
|
||||
val pin: String,
|
||||
val finTs3ServerAddress: String,
|
||||
val bic: String,
|
||||
|
||||
val bankName: String,
|
||||
val countryCode: Int,
|
||||
val bpdVersion: Int,
|
||||
|
||||
val userId: String,
|
||||
val customerName: String,
|
||||
val updVersion: Int,
|
||||
|
||||
val tanMethodsSupportedByBank: List<TanMethod>,
|
||||
val identifierOfTanMethodsAvailableForUser: List<String> = listOf(),
|
||||
val selectedTanMethodIdentifier: String,
|
||||
val tanMedia: List<TanMedium> = listOf(),
|
||||
val selectedTanMediumIdentifier: String? = null,
|
||||
|
||||
val supportedLanguages: List<Dialogsprache> = listOf(),
|
||||
val selectedLanguage: Dialogsprache = Dialogsprache.Default,
|
||||
val customerSystemId: String = KundensystemID.Anonymous,
|
||||
val customerSystemStatus: KundensystemStatusWerte = KundensystemStatus.SynchronizingCustomerSystemId,
|
||||
|
||||
val countMaxJobsPerMessage: Int = 0,
|
||||
|
||||
val supportedHbciVersions: List<HbciVersion> = listOf(),
|
||||
val supportedJobs: List<SerializableJobParameters> = listOf(),
|
||||
val supportedDetailedJobs: List<DetailedSerializableJobParameters> = listOf(),
|
||||
val jobsRequiringTan: Set<String> = emptySet(),
|
||||
|
||||
val pinInfo: PinInfo? = null,
|
||||
|
||||
val accounts: List<AccountData>
|
||||
) {
|
||||
|
||||
@EncodeDefault
|
||||
private val modelVersion: String = "0.6.0"
|
||||
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package net.codinux.banking.fints.serialization
|
||||
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.response.segments.ChangeTanMediaParameters
|
||||
import net.codinux.banking.fints.response.segments.JobParameters
|
||||
import net.codinux.banking.fints.response.segments.RetrieveAccountTransactionsParameters
|
||||
import net.codinux.banking.fints.response.segments.SepaAccountInfoParameters
|
||||
import net.codinux.banking.fints.serialization.jobparameter.*
|
||||
import net.codinux.log.logger
|
||||
|
||||
class SerializedFinTsDataMapper {
|
||||
|
||||
private val log by logger()
|
||||
|
||||
|
||||
fun map(bank: BankData) = SerializedFinTsData(
|
||||
bank.bankCode,
|
||||
bank.customerId,
|
||||
bank.pin,
|
||||
bank.finTs3ServerAddress,
|
||||
bank.bic,
|
||||
|
||||
bank.bankName,
|
||||
bank.countryCode,
|
||||
bank.bpdVersion,
|
||||
|
||||
bank.userId,
|
||||
bank.customerName,
|
||||
bank.updVersion,
|
||||
|
||||
bank.tanMethodsSupportedByBank,
|
||||
bank.tanMethodsAvailableForUser.map { it.securityFunction.code },
|
||||
bank.selectedTanMethod.securityFunction.code,
|
||||
bank.tanMedia,
|
||||
bank.selectedTanMedium?.identifier,
|
||||
|
||||
bank.supportedLanguages,
|
||||
bank.selectedLanguage,
|
||||
bank.customerSystemId,
|
||||
bank.customerSystemStatus,
|
||||
|
||||
bank.countMaxJobsPerMessage,
|
||||
|
||||
bank.supportedHbciVersions,
|
||||
bank.supportedJobs.filterNot { isDetailedJobParameters(it) }.map { mapJobParameters(it) },
|
||||
bank.supportedJobs.filter { isDetailedJobParameters(it) }.mapNotNull { mapDetailedJobParameters(it) },
|
||||
bank.jobsRequiringTan,
|
||||
|
||||
bank.pinInfo,
|
||||
|
||||
bank.accounts
|
||||
)
|
||||
|
||||
private fun isDetailedJobParameters(parameters: JobParameters): Boolean =
|
||||
parameters is RetrieveAccountTransactionsParameters
|
||||
|| parameters is SepaAccountInfoParameters
|
||||
|| parameters is ChangeTanMediaParameters
|
||||
|
||||
private fun mapJobParameters(parameters: JobParameters) = SerializableJobParameters(
|
||||
parameters.jobName,
|
||||
parameters.maxCountJobs,
|
||||
parameters.minimumCountSignatures,
|
||||
parameters.securityClass,
|
||||
|
||||
parameters.segmentId,
|
||||
parameters.segmentNumber,
|
||||
parameters.segmentVersion,
|
||||
|
||||
parameters.segmentString
|
||||
)
|
||||
|
||||
private fun mapDetailedJobParameters(parameters: JobParameters): DetailedSerializableJobParameters? = when (parameters) {
|
||||
is RetrieveAccountTransactionsParameters -> SerializableRetrieveAccountTransactionsParameters(mapJobParameters(parameters), parameters.serverTransactionsRetentionDays, parameters.settingCountEntriesAllowed, parameters.settingAllAccountAllowed, parameters.supportedCamtDataFormats)
|
||||
is SepaAccountInfoParameters -> SerializableSepaAccountInfoParameters(mapJobParameters(parameters), parameters.retrieveSingleAccountAllowed, parameters.nationalAccountRelationshipAllowed, parameters.structuredReferenceAllowed, parameters.settingMaxAllowedEntriesAllowed, parameters.countReservedReferenceLength, parameters.supportedSepaFormats)
|
||||
is ChangeTanMediaParameters -> SerializableChangeTanMediaParameters(mapJobParameters(parameters), parameters.enteringTanListNumberRequired, parameters.enteringCardSequenceNumberRequired, parameters.enteringAtcAndTanRequired, parameters.enteringCardTypeAllowed, parameters.accountInfoRequired, parameters.allowedCardTypes)
|
||||
else -> {
|
||||
log.warn { "${parameters::class} is said to be a DetailedJobParameters class, but found no mapping code for it" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun map(bank: SerializedFinTsData) = BankData(
|
||||
bank.bankCode,
|
||||
bank.customerId,
|
||||
bank.pin,
|
||||
bank.finTs3ServerAddress,
|
||||
bank.bic,
|
||||
|
||||
bank.bankName,
|
||||
bank.countryCode,
|
||||
bank.bpdVersion,
|
||||
|
||||
bank.userId,
|
||||
bank.customerName,
|
||||
bank.updVersion,
|
||||
|
||||
bank.tanMethodsSupportedByBank,
|
||||
bank.tanMethodsSupportedByBank.filter { it.securityFunction.code in bank.identifierOfTanMethodsAvailableForUser },
|
||||
bank.tanMethodsSupportedByBank.first { it.securityFunction.code == bank.selectedTanMethodIdentifier },
|
||||
bank.tanMedia,
|
||||
bank.selectedTanMediumIdentifier?.let { id -> bank.tanMedia.firstOrNull { it.identifier == id } },
|
||||
|
||||
bank.supportedLanguages,
|
||||
bank.selectedLanguage,
|
||||
bank.customerSystemId,
|
||||
bank.customerSystemStatus,
|
||||
|
||||
bank.countMaxJobsPerMessage,
|
||||
|
||||
bank.supportedHbciVersions,
|
||||
bank.supportedJobs.map { mapJobParameters(it) } + bank.supportedDetailedJobs.map { mapDetailedJobParameters(it) },
|
||||
bank.jobsRequiringTan
|
||||
).apply {
|
||||
pinInfo = bank.pinInfo
|
||||
|
||||
bank.accounts.forEach { account ->
|
||||
account.allowedJobs = this.supportedJobs.filter { it.jobName in account.allowedJobNames }
|
||||
this.addAccount(account)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapJobParameters(parameters: SerializableJobParameters) = JobParameters(
|
||||
parameters.jobName,
|
||||
parameters.maxCountJobs,
|
||||
parameters.minimumCountSignatures,
|
||||
parameters.securityClass,
|
||||
|
||||
parameters.segmentString
|
||||
)
|
||||
|
||||
private fun mapDetailedJobParameters(parameters: DetailedSerializableJobParameters): JobParameters = when (parameters) {
|
||||
is SerializableRetrieveAccountTransactionsParameters -> RetrieveAccountTransactionsParameters(mapJobParameters(parameters.jobParameters), parameters.serverTransactionsRetentionDays, parameters.settingCountEntriesAllowed, parameters.settingAllAccountAllowed, parameters.supportedCamtDataFormats)
|
||||
is SerializableSepaAccountInfoParameters -> SepaAccountInfoParameters(mapJobParameters(parameters.jobParameters), parameters.retrieveSingleAccountAllowed, parameters.nationalAccountRelationshipAllowed, parameters.structuredReferenceAllowed, parameters.settingMaxAllowedEntriesAllowed, parameters.countReservedReferenceLength, parameters.supportedSepaFormats)
|
||||
is SerializableChangeTanMediaParameters -> ChangeTanMediaParameters(mapJobParameters(parameters.jobParameters), parameters.enteringTanListNumberRequired, parameters.enteringCardSequenceNumberRequired, parameters.enteringAtcAndTanRequired, parameters.enteringCardTypeAllowed, parameters.accountInfoRequired, parameters.allowedCardTypes)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package net.codinux.banking.fints.serialization.jobparameter
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class DetailedSerializableJobParameters {
|
||||
|
||||
abstract val jobParameters: SerializableJobParameters
|
||||
|
||||
|
||||
override fun toString() = jobParameters.toString()
|
||||
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package net.codinux.banking.fints.serialization.jobparameter
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@SerialName("ChangeTanMediaParameters")
|
||||
class SerializableChangeTanMediaParameters(
|
||||
override val jobParameters: SerializableJobParameters,
|
||||
|
||||
val enteringTanListNumberRequired: Boolean,
|
||||
val enteringCardSequenceNumberRequired: Boolean,
|
||||
val enteringAtcAndTanRequired: Boolean,
|
||||
val enteringCardTypeAllowed: Boolean,
|
||||
val accountInfoRequired: Boolean,
|
||||
val allowedCardTypes: List<Int>
|
||||
) : DetailedSerializableJobParameters()
|
|
@ -1,19 +0,0 @@
|
|||
package net.codinux.banking.fints.serialization.jobparameter
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class SerializableJobParameters(
|
||||
val jobName: String,
|
||||
val maxCountJobs: Int,
|
||||
val minimumCountSignatures: Int,
|
||||
val securityClass: Int?,
|
||||
|
||||
val segmentId: String,
|
||||
val segmentNumber: Int,
|
||||
val segmentVersion: Int,
|
||||
|
||||
val segmentString: String
|
||||
) {
|
||||
override fun toString() = "$jobName $segmentVersion"
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package net.codinux.banking.fints.serialization.jobparameter
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@SerialName("RetrieveAccountTransactionsParameters")
|
||||
class SerializableRetrieveAccountTransactionsParameters(
|
||||
override val jobParameters: SerializableJobParameters,
|
||||
|
||||
val serverTransactionsRetentionDays: Int,
|
||||
val settingCountEntriesAllowed: Boolean,
|
||||
val settingAllAccountAllowed: Boolean,
|
||||
val supportedCamtDataFormats: List<String> = emptyList()
|
||||
) : DetailedSerializableJobParameters() {
|
||||
override fun toString() = "${super.toString()}, serverTransactionsRetentionDays = $serverTransactionsRetentionDays, supportedCamtDataFormats = $supportedCamtDataFormats"
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package net.codinux.banking.fints.serialization.jobparameter
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@SerialName("SepaAccountInfoParameters")
|
||||
class SerializableSepaAccountInfoParameters(
|
||||
override val jobParameters: SerializableJobParameters,
|
||||
|
||||
val retrieveSingleAccountAllowed: Boolean,
|
||||
val nationalAccountRelationshipAllowed: Boolean,
|
||||
val structuredReferenceAllowed: Boolean,
|
||||
val settingMaxAllowedEntriesAllowed: Boolean,
|
||||
val countReservedReferenceLength: Int,
|
||||
val supportedSepaFormats: List<String>
|
||||
) : DetailedSerializableJobParameters() {
|
||||
override fun toString() = "${super.toString()}, supportedSepaFormats = $supportedSepaFormats"
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.mt940
|
||||
|
||||
import net.codinux.banking.fints.log.IMessageLogAppender
|
||||
import net.codinux.banking.fints.transactions.mt940.model.*
|
||||
|
||||
open class Mt940Parser(
|
||||
override var logAppender: IMessageLogAppender? = null
|
||||
) : Mt94xParserBase<AccountStatement>(logAppender), IMt940Parser {
|
||||
|
||||
/**
|
||||
* Parses a whole MT 940 statements string, that is one that ends with a "-" line.
|
||||
*/
|
||||
override fun parseMt940String(mt940String: String): List<AccountStatement> =
|
||||
super.parseMt94xString(mt940String)
|
||||
|
||||
/**
|
||||
* Parses incomplete MT 940 statements string, that is ones that not end with a "-" line,
|
||||
* as they are returned e.g. if a HKKAZ response is dispersed over multiple messages.
|
||||
*
|
||||
* Tries to parse all statements in the string except an incomplete last one and returns an
|
||||
* incomplete last MT 940 statement (if any) as remainder.
|
||||
*
|
||||
* So each single HKKAZ partial response can be parsed immediately, its statements/transactions
|
||||
* be displayed immediately to user and the remainder then be passed together with next partial
|
||||
* HKKAZ response to this method till this whole MT 940 statement is parsed.
|
||||
*/
|
||||
override fun parseMt940Chunk(mt940Chunk: String): Pair<List<AccountStatement>, String?> =
|
||||
super.parseMt94xChunk(mt940Chunk)
|
||||
|
||||
|
||||
override fun createAccountStatement(
|
||||
orderReferenceNumber: String,
|
||||
referenceNumber: String?,
|
||||
bankCodeBicOrIban: String,
|
||||
accountIdentifier: String?,
|
||||
statementNumber: Int,
|
||||
sheetNumber: Int?,
|
||||
transactions: List<Transaction>,
|
||||
fieldsByCode: List<Pair<String, String>>
|
||||
): AccountStatement {
|
||||
val openingBalancePair = fieldsByCode.first { it.first.startsWith(OpeningBalanceCode) }
|
||||
val closingBalancePair = fieldsByCode.first { it.first.startsWith(ClosingBalanceCode) }
|
||||
|
||||
return AccountStatement(
|
||||
orderReferenceNumber, referenceNumber,
|
||||
bankCodeBicOrIban, accountIdentifier,
|
||||
statementNumber, sheetNumber,
|
||||
parseBalance(openingBalancePair.first, openingBalancePair.second),
|
||||
transactions,
|
||||
parseBalance(closingBalancePair.first, closingBalancePair.second)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.mt940
|
||||
|
||||
import net.codinux.banking.fints.log.IMessageLogAppender
|
||||
import net.codinux.banking.fints.transactions.mt940.model.AmountAndCurrency
|
||||
import net.codinux.banking.fints.transactions.mt940.model.InterimAccountStatement
|
||||
import net.codinux.banking.fints.transactions.mt940.model.NumberOfPostingsAndAmount
|
||||
import net.codinux.banking.fints.transactions.mt940.model.Transaction
|
||||
|
||||
open class Mt942Parser(
|
||||
logAppender: IMessageLogAppender? = null
|
||||
) : Mt94xParserBase<InterimAccountStatement>(logAppender) {
|
||||
|
||||
/**
|
||||
* Parses a whole MT 942 statements string, that is one that ends with a "-" line.
|
||||
*/
|
||||
open fun parseMt942String(mt942String: String): List<InterimAccountStatement> =
|
||||
super.parseMt94xString(mt942String)
|
||||
|
||||
/**
|
||||
* Parses incomplete MT 942 statements string, that is ones that not end with a "-" line,
|
||||
* as they are returned e.g. if a HKKAZ response is dispersed over multiple messages.
|
||||
*
|
||||
* Tries to parse all statements in the string except an incomplete last one and returns an
|
||||
* incomplete last MT 942 statement (if any) as remainder.
|
||||
*
|
||||
* So each single HKKAZ partial response can be parsed immediately, its statements/transactions
|
||||
* be displayed immediately to user and the remainder then be passed together with next partial
|
||||
* HKKAZ response to this method till this whole MT 942 statement is parsed.
|
||||
*/
|
||||
open fun parseMt942Chunk(mt942Chunk: String): Pair<List<InterimAccountStatement>, String?> =
|
||||
super.parseMt94xChunk(mt942Chunk)
|
||||
|
||||
|
||||
override fun createAccountStatement(
|
||||
orderReferenceNumber: String,
|
||||
referenceNumber: String?,
|
||||
bankCodeBicOrIban: String,
|
||||
accountIdentifier: String?,
|
||||
statementNumber: Int,
|
||||
sheetNumber: Int?,
|
||||
transactions: List<Transaction>,
|
||||
fieldsByCode: List<Pair<String, String>>
|
||||
): InterimAccountStatement {
|
||||
// also decided against parsing smallest amounts, i don't think they ever going to be used
|
||||
// val smallestAmounts = fieldsByCode.filter { it.first.startsWith(SmallestAmountCode) } // should we parse it? i see no use in it
|
||||
// .mapIndexed { index, field -> parseAmountAndCurrency(field.second, index == 0) }
|
||||
|
||||
// decided against parsing creation time as there are so many non specification confirm time formats that parsing is likely to fail 'cause of this unused value
|
||||
// val creationTime = parseDateTime(fieldsByCode.first { it.first == CreationTimeCode || it.first.startsWith(CreationTimeStartCode) }.second)
|
||||
|
||||
val numberAndTotalOfDebitPostings = fieldsByCode.firstOrNull { it.first.equals(AmountOfDebitPostingsCode) }
|
||||
?.let { parseNumberAndTotalOfPostings(it.second) }
|
||||
val numberAndTotalOfCreditPostings = fieldsByCode.firstOrNull { it.first.equals(AmountOfCreditPostingsCode) }
|
||||
?.let { parseNumberAndTotalOfPostings(it.second) }
|
||||
|
||||
return InterimAccountStatement(
|
||||
orderReferenceNumber, referenceNumber,
|
||||
bankCodeBicOrIban, accountIdentifier,
|
||||
statementNumber, sheetNumber,
|
||||
transactions,
|
||||
numberAndTotalOfDebitPostings,
|
||||
numberAndTotalOfCreditPostings
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseAmountAndCurrency(fieldValue: String, isCreditCharOptional: Boolean = false): AmountAndCurrency {
|
||||
val currency = fieldValue.substring(0, 3)
|
||||
val hasCreditChar = isCreditCharOptional == false || fieldValue[3].isLetter()
|
||||
val isCredit = if (hasCreditChar) fieldValue[3] == 'C' else false
|
||||
val amount = fieldValue.substring(if (hasCreditChar) 4 else 3)
|
||||
|
||||
return AmountAndCurrency(amount, currency, isCredit)
|
||||
}
|
||||
|
||||
protected open fun parseNumberAndTotalOfPostings(fieldValue: String): NumberOfPostingsAndAmount {
|
||||
val currencyStartIndex = fieldValue.indexOfFirst { it.isLetter() }
|
||||
|
||||
val numberOfPostings = fieldValue.substring(0, currencyStartIndex).toInt()
|
||||
val currency = fieldValue.substring(currencyStartIndex, currencyStartIndex + 3)
|
||||
val amount = fieldValue.substring(currencyStartIndex + 3)
|
||||
|
||||
return NumberOfPostingsAndAmount(numberOfPostings, amount, currency)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.mt940.model
|
||||
|
||||
open class AccountStatement(
|
||||
orderReferenceNumber: String,
|
||||
referenceNumber: String?,
|
||||
|
||||
bankCodeBicOrIban: String,
|
||||
accountIdentifier: String?,
|
||||
|
||||
statementNumber: Int,
|
||||
sheetNumber: Int?,
|
||||
|
||||
val openingBalance: Balance,
|
||||
|
||||
transactions: List<Transaction>,
|
||||
|
||||
val closingBalance: Balance,
|
||||
|
||||
val currentValueBalance: Balance? = null,
|
||||
|
||||
val futureValueBalance: Balance? = null,
|
||||
|
||||
/**
|
||||
* Mehrzweckfeld
|
||||
*
|
||||
* Es dürfen nur unstrukturierte Informationen eingestellt werden. Es dürfen keine Informationen,
|
||||
* die auf einzelne Umsätze bezogen sind, eingestellt werden.
|
||||
*
|
||||
* Die Zeilen werden mit <CR><LF> getrennt.
|
||||
*
|
||||
* Max length = 65
|
||||
*/
|
||||
val remittanceInformationField: String? = null
|
||||
|
||||
) : AccountStatementCommon(orderReferenceNumber, referenceNumber, bankCodeBicOrIban, accountIdentifier, statementNumber, sheetNumber, transactions) {
|
||||
|
||||
// for object deserializers
|
||||
private constructor() : this("", "", "", null, 0, null, Balance(), listOf(), Balance())
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$closingBalance ${super.toString()}"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.mt940.model
|
||||
|
||||
class AmountAndCurrency(
|
||||
val amount: String,
|
||||
val currency: String,
|
||||
val isCredit: Boolean
|
||||
) {
|
||||
internal constructor() : this("not an amount", "not a currency", false) // for object deserializers
|
||||
|
||||
override fun toString() = "${if (isCredit == false) "-" else ""}$amount $currency"
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.mt940.model
|
||||
|
||||
open class InterimAccountStatement(
|
||||
orderReferenceNumber: String,
|
||||
referenceNumber: String?,
|
||||
|
||||
bankCodeBicOrIban: String,
|
||||
accountIdentifier: String?,
|
||||
|
||||
statementNumber: Int,
|
||||
sheetNumber: Int?,
|
||||
|
||||
// decided against parsing them, see Mt942Parser
|
||||
// val smallestAmountOfReportedTransactions: AmountAndCurrency,
|
||||
//
|
||||
// val smallestAmountOfReportedCreditTransactions: AmountAndCurrency? = null,
|
||||
//
|
||||
// val creationTime: Instant,
|
||||
|
||||
transactions: List<Transaction>,
|
||||
|
||||
val amountAndTotalOfDebitPostings: NumberOfPostingsAndAmount? = null,
|
||||
|
||||
val amountAndTotalOfCreditPostings: NumberOfPostingsAndAmount? = null,
|
||||
|
||||
) : AccountStatementCommon(orderReferenceNumber, referenceNumber, bankCodeBicOrIban, accountIdentifier, statementNumber, sheetNumber, transactions) {
|
||||
|
||||
// for object deserializers
|
||||
private constructor() : this("", "", "", null, 0, null, listOf())
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "${amountAndTotalOfDebitPostings?.amount} ${super.toString()}"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.mt940.model
|
||||
|
||||
class NumberOfPostingsAndAmount(
|
||||
val numberOfPostings: Int,
|
||||
val amount: String,
|
||||
val currency: String
|
||||
) {
|
||||
private constructor() : this(-1, "not an amount", "not a currency") // for object deserializers
|
||||
|
||||
override fun toString() = "$amount $currency, $numberOfPostings posting(s)"
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.mt940.model
|
||||
|
||||
open class RemittanceInformationField(
|
||||
val unparsedReference: String,
|
||||
/**
|
||||
* AT 02 Name des Überweisenden
|
||||
* AT 03 Name des Zahlungsempfängers (bei mehr als 54 Zeichen wird der Name gekürzt)
|
||||
*/
|
||||
val otherPartyName: String?,
|
||||
/**
|
||||
* BLZ Überweisender / Zahlungsempfänger
|
||||
* Bei SEPA-Zahlungen BIC des Überweisenden / Zahlungsempfängers.
|
||||
*/
|
||||
val otherPartyBankId: String?,
|
||||
/**
|
||||
* AT 01 IBAN des Überweisenden (Zahlungseingang Überweisung)
|
||||
* AT 04 IBAN des Zahlungsempfängers (Eingang Lastschrift)
|
||||
*/
|
||||
val otherPartyAccountId: String?,
|
||||
/**
|
||||
* Buchungstext, z. B. DAUERAUFTRAG, BARGELDAUSZAHLUNG, ONLINE-UEBERWEISUNG, FOLGELASTSCHRIFT, ...
|
||||
*/
|
||||
val postingText: String?,
|
||||
/**
|
||||
* Primanoten-Nr.
|
||||
*/
|
||||
val journalNumber: String?,
|
||||
/**
|
||||
* Bei R-Transaktionen siehe Tabelle der
|
||||
* SEPA-Rückgabecodes, bei SEPALastschriften siehe optionale Belegung
|
||||
* bei GVC 104 und GVC 105 (GVC = Geschäftsvorfallcode)
|
||||
*/
|
||||
val textKeyAddition: String?
|
||||
) {
|
||||
|
||||
/**
|
||||
* (DDAT10; CT-AT41 - Angabe verpflichtend)
|
||||
* (NOTPROVIDED wird nicht eingestellt.
|
||||
* Im Falle von Schecks wird hinter EREF+ die Konstante „SCHECK-NR. “, gefolgt von der Schecknummer angegeben (erst
|
||||
* nach Migration Scheckvordruck auf ISO 20022; November 2016, entspricht dem Inhalt der EndToEndId des
|
||||
* entsprechenden Scheckumsatzes).
|
||||
*/
|
||||
var endToEndReference: String? = null
|
||||
|
||||
var customerReference: String? = null
|
||||
|
||||
/**
|
||||
* (DD-AT01 - Angabe verpflichtend)
|
||||
*/
|
||||
var mandateReference: String? = null
|
||||
|
||||
/**
|
||||
* (DD-AT02 - Angabe verpflichtend bei SEPALastschriften, nicht jedoch bei SEPARücklastschriften)
|
||||
*/
|
||||
var creditorIdentifier: String? = null
|
||||
|
||||
/**
|
||||
* (CT-AT10- Angabe verpflichtend,)
|
||||
* Entweder CRED oder DEBT
|
||||
*/
|
||||
var originatorsIdentificationCode: String? = null
|
||||
|
||||
/**
|
||||
* Summe aus Auslagenersatz und Bearbeitungsprovision bei einer nationalen Rücklastschrift
|
||||
* sowie optionalem Zinsausgleich.
|
||||
*/
|
||||
var compensationAmount: String? = null
|
||||
|
||||
/**
|
||||
* Betrag der ursprünglichen Lastschrift
|
||||
*/
|
||||
var originalAmount: String? = null
|
||||
|
||||
/**
|
||||
* (DD-AT22; CT-AT05 -Angabe verpflichtend, nicht jedoch bei RTransaktionen52)
|
||||
*/
|
||||
var sepaReference: String? = null
|
||||
|
||||
/**
|
||||
* Abweichender Überweisender (CT-AT08) / Abweichender Zahlungsempfänger (DD-AT38)
|
||||
* (optional)53
|
||||
*/
|
||||
var deviantOriginator: String? = null
|
||||
|
||||
/**
|
||||
* Abweichender Zahlungsempfänger (CT-AT28) /
|
||||
* Abweichender Zahlungspflichtiger ((DDAT15)
|
||||
* (optional)53
|
||||
*/
|
||||
var deviantRecipient: String? = null
|
||||
|
||||
var referenceWithNoSpecialType: String? = null
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$otherPartyName $unparsedReference"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.mt940.model
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.codinux.banking.fints.model.Amount
|
||||
|
||||
|
||||
open class StatementLine(
|
||||
|
||||
/**
|
||||
* Soll/Haben-Kennung
|
||||
*
|
||||
* “C” = Credit (Habensaldo)
|
||||
* ”D” = Debit (Sollsaldo)
|
||||
* „RC“ = Storno Haben
|
||||
* „RD“ = Storno Soll
|
||||
*
|
||||
* Max length = 2
|
||||
*/
|
||||
val isCredit: Boolean,
|
||||
|
||||
val isReversal: Boolean,
|
||||
|
||||
/**
|
||||
* Valuta (JJMMTT)
|
||||
*
|
||||
* Length = 6
|
||||
*/
|
||||
val valueDate: LocalDate,
|
||||
|
||||
/**
|
||||
* MMTT
|
||||
*
|
||||
* Length = 4
|
||||
*/
|
||||
val bookingDate: LocalDate?,
|
||||
|
||||
/**
|
||||
* dritte Stelle der Währungsbezeichnung, falls sie zur Unterscheidung notwendig ist
|
||||
*
|
||||
* Length = 1
|
||||
*/
|
||||
val currencyType: String?,
|
||||
|
||||
/**
|
||||
* in Kontowährung
|
||||
*
|
||||
* Max length = 15
|
||||
*/
|
||||
val amount: Amount,
|
||||
|
||||
/**
|
||||
* Codes see p. 177 bottom - 179
|
||||
*
|
||||
* After constant „N“
|
||||
*
|
||||
* Length = 3
|
||||
*/
|
||||
val postingKey: String,
|
||||
|
||||
/**
|
||||
* Kundenreferenz.
|
||||
* Bei Nichtbelegung wird „NONREF“ eingestellt, zum Beispiel bei Schecknummer
|
||||
* Wenn „KREF+“ eingestellt ist, dann erfolgt die Angabe der Referenznummer in Tag :86: .
|
||||
*/
|
||||
val customerReference: String?,
|
||||
|
||||
/**
|
||||
* Bankreferenz
|
||||
*/
|
||||
val bankReference: String?,
|
||||
|
||||
/**
|
||||
* Währungsart und Umsatzbetrag in Ursprungswährung (original currency
|
||||
* amount) in folgendem
|
||||
* Format:
|
||||
* /OCMT/3a..15d/
|
||||
* sowie Währungsart und
|
||||
* Gebührenbetrag
|
||||
* (charges) in folgendem
|
||||
* Format:
|
||||
* /CHGS/3a..15d/
|
||||
* 3a = 3-stelliger
|
||||
* Währungscode gemäß
|
||||
* ISO 4217
|
||||
* ..15d = Betrag mit Komma
|
||||
* als Dezimalzeichen (gemäß SWIFT-Konvention).
|
||||
* Im Falle von SEPALastschriftrückgaben ist
|
||||
* das Feld /OCMT/ mit dem
|
||||
* Originalbetrag und das
|
||||
* Feld /CHGS/ mit der
|
||||
* Summe aus Entgelten
|
||||
* sowie Zinsausgleich zu
|
||||
* belegen.
|
||||
*/
|
||||
val furtherInformationOriginalAmountAndCharges: String? = null
|
||||
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
return "$valueDate ${if (isCredit) "+" else "-"}$amount"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,226 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.swift
|
||||
|
||||
import kotlinx.datetime.*
|
||||
import net.codinux.banking.fints.extensions.EuropeBerlin
|
||||
import net.codinux.banking.fints.log.IMessageLogAppender
|
||||
import net.codinux.banking.fints.model.Amount
|
||||
import net.codinux.banking.fints.transactions.swift.model.ContinuationIndicator
|
||||
import net.codinux.banking.fints.transactions.swift.model.Holding
|
||||
import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings
|
||||
import net.codinux.banking.fints.transactions.swift.model.SwiftMessageBlock
|
||||
|
||||
open class Mt535Parser(
|
||||
logAppender: IMessageLogAppender? = null
|
||||
) : MtParserBase(logAppender) {
|
||||
|
||||
open fun parseMt535String(mt535String: String): List<StatementOfHoldings> {
|
||||
val blocks = parseMtString(mt535String, true)
|
||||
|
||||
// should actually always be only one block, just to be on the safe side
|
||||
return blocks.mapNotNull { parseStatementOfHoldings(it) }
|
||||
}
|
||||
|
||||
protected open fun parseStatementOfHoldings(mt535Block: SwiftMessageBlock): StatementOfHoldings? {
|
||||
try {
|
||||
val containsHoldings = mt535Block.getMandatoryField("17B").endsWith("//Y")
|
||||
val holdings = if (containsHoldings) parseHoldings(mt535Block) else emptyList()
|
||||
|
||||
return parseStatementOfHoldings(holdings, mt535Block)
|
||||
} catch (e: Throwable) {
|
||||
logError("Could not parse MT 535 block:\n$mt535Block", e)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
protected open fun parseStatementOfHoldings(holdings: List<Holding>, mt535Block: SwiftMessageBlock): StatementOfHoldings {
|
||||
val totalBalance = parseBalance(mt535Block.getMandatoryRepeatableField("19A").last())
|
||||
|
||||
val accountStatement = mt535Block.getMandatoryField("97A")
|
||||
val bankCode = accountStatement.substringAfter("//").substringBefore('/')
|
||||
val accountIdentifier = accountStatement.substringAfterLast('/')
|
||||
|
||||
val (pageNumber, continuationIndicator) = parsePageNumber(mt535Block)
|
||||
|
||||
val (statementDate, preparationDate) = parseStatementAndPreparationDate(mt535Block)
|
||||
|
||||
return StatementOfHoldings(bankCode, accountIdentifier, holdings, totalBalance?.first, totalBalance?.second, pageNumber, continuationIndicator, statementDate, preparationDate)
|
||||
}
|
||||
|
||||
// this is a MT5(35) specific balance format
|
||||
protected open fun parseBalance(balanceString: String?): Pair<Amount, String>? {
|
||||
if (balanceString != null) {
|
||||
val balancePart = balanceString.substringAfterLast('/')
|
||||
val amount = balancePart.substring(3)
|
||||
val isNegative = amount.startsWith("N")
|
||||
return Pair(Amount(if (isNegative) "-${amount.substring(1)}" else amount), balancePart.substring(0, 3))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
protected open fun parsePageNumber(mt535Block: SwiftMessageBlock): Pair<Int?, ContinuationIndicator> {
|
||||
return try {
|
||||
val pageNumberStatement = mt535Block.getMandatoryField("28E")
|
||||
val pageNumber = pageNumberStatement.substringBefore('/').toInt()
|
||||
val continuationIndicator = pageNumberStatement.substringAfter('/').let { indicatorString ->
|
||||
ContinuationIndicator.entries.firstOrNull { it.mtValue == indicatorString } ?: ContinuationIndicator.Unknown
|
||||
}
|
||||
|
||||
Pair(pageNumber, continuationIndicator)
|
||||
} catch (e: Throwable) {
|
||||
logError("Could not parse statement and preparation date of block:\n$mt535Block", e)
|
||||
|
||||
Pair(null, ContinuationIndicator.Unknown)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun parseStatementAndPreparationDate(mt535Block: SwiftMessageBlock): Pair<LocalDate?, LocalDate?> {
|
||||
return try {
|
||||
// TODO: differ between 98A (without time) and 98C (with time)
|
||||
// TODO: ignore (before parsing?) 98A/C of holdings which start with ":PRIC//
|
||||
val dates = mt535Block.getMandatoryRepeatableField("98").map { it.substringBefore("//") to parse4DigitYearDate(it.substringAfter("//").substring(0, 8)) } // if given we ignore time
|
||||
val statementDate = dates.firstOrNull { it.first == ":STAT" }?.second // specifications and their implementations: the statement date is actually mandatory, but not all banks actually set it
|
||||
val preparationDate = dates.firstOrNull { it.first == ":PREP" }?.second
|
||||
|
||||
Pair(statementDate, preparationDate)
|
||||
} catch (e: Throwable) {
|
||||
logError("Could not parse statement and preparation date of block:\n$mt535Block", e)
|
||||
Pair(null, null)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun parseHoldings(mt535Block: SwiftMessageBlock): List<Holding> {
|
||||
val blockLines = mt535Block.getFieldsInOrder()
|
||||
val holdingBlocksStartIndices = blockLines.indices.filter { blockLines[it].first == "16R" && blockLines[it].second == "FIN" }
|
||||
val holdingBlocksEndIndices = blockLines.indices.filter { blockLines[it].first == "16S" && blockLines[it].second == "FIN" }
|
||||
|
||||
val holdingBlocks = holdingBlocksStartIndices.mapIndexed { blockIndex, startIndex ->
|
||||
val endIndex = holdingBlocksEndIndices[blockIndex]
|
||||
val holdingBlockLines = blockLines.subList(startIndex + 1, endIndex)
|
||||
SwiftMessageBlock(holdingBlockLines)
|
||||
}
|
||||
|
||||
return holdingBlocks.mapNotNull { parseHolding(it) }
|
||||
}
|
||||
|
||||
protected open fun parseHolding(holdingBlock: SwiftMessageBlock): Holding? =
|
||||
try {
|
||||
val nameStatementLines = holdingBlock.getMandatoryField("35B").split("\n")
|
||||
val isinOrWkn = nameStatementLines.first()
|
||||
val isin = if (isinOrWkn.startsWith("ISIN ")) {
|
||||
isinOrWkn.substringAfter(' ')
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val wkn = if (isin == null) {
|
||||
isinOrWkn
|
||||
} else if (nameStatementLines[1].startsWith("DE")) {
|
||||
nameStatementLines[1]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val name = nameStatementLines.subList(if (isin == null || wkn == null) 1 else 2, nameStatementLines.size).joinToString(" ")
|
||||
|
||||
// TODO: check for optional code :90a: Preis
|
||||
// TODO: check for optional code :94B: Herkunft von Preis / Kurs
|
||||
// TODO: check for optional code :98A: Herkunft von Preis / Kurs
|
||||
// TODO: check for optional code :99A: Anzahl der aufgelaufenen Tage
|
||||
// TODO: check for optional code :92B: Exchange rate
|
||||
|
||||
val holdingTotalBalance = holdingBlock.getMandatoryField("93B")
|
||||
val balanceIsQuantity = holdingTotalBalance.startsWith(":AGGR//UNIT") // == Die Stückzahl wird als Zahl (Zähler) ausgedrückt
|
||||
// else it starts with "AGGR/FAMT" = Die Stückzahl wird als Nennbetrag ausgedrückt. Bei Nennbeträgen wird die Währung durch die „Depotwährung“ in Feld B:70E: bestimmt
|
||||
val totalBalanceWithOptionalSign = holdingTotalBalance.substring(":AGGR//UNIT/".length)
|
||||
val totalBalanceIsNegative = totalBalanceWithOptionalSign.first() == 'N'
|
||||
val totalBalance = if (totalBalanceIsNegative) "-" + totalBalanceWithOptionalSign.substring(1) else totalBalanceWithOptionalSign
|
||||
|
||||
// there's a second ":HOLD//" entry if the currency if the security differs from portfolio's currency // TODO: the 3rd holding of the DK example has this, so implement it to display the correct value
|
||||
val portfolioValueStatement = holdingBlock.getOptionalRepeatableField("19A")?.firstOrNull { it.startsWith(":HOLD//") }
|
||||
val portfolioValue = parseBalance(portfolioValueStatement?.substringAfter(":HOLD//")) // Value for total balance from B:93B: in the same currency as C:19A:
|
||||
|
||||
val (buyingDate, averageCostPrice, averageCostPriceCurrency) = parseHoldingAdditionalInformation(holdingBlock)
|
||||
|
||||
val (marketValue, pricingTime, totalCostPrice) = parseMarketValue(holdingBlock)
|
||||
|
||||
val balance = portfolioValue?.first ?: (if (balanceIsQuantity == false) Amount(totalBalance) else null)
|
||||
val quantity = if (balanceIsQuantity) totalBalance.replace(",", ".").toDoubleOrNull() else null
|
||||
|
||||
Holding(name, isin, wkn, buyingDate, quantity, averageCostPrice, balance, portfolioValue?.second ?: averageCostPriceCurrency, marketValue, pricingTime, totalCostPrice)
|
||||
} catch (e: Throwable) {
|
||||
logError("Could not parse MT 535 holding block:\n$holdingBlock", e)
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
protected open fun parseHoldingAdditionalInformation(holdingBlock: SwiftMessageBlock): Triple<LocalDate?, Amount?, String?> {
|
||||
try {
|
||||
val additionalInformationLines = holdingBlock.getOptionalField("70E")?.split('\n')
|
||||
if (additionalInformationLines != null) {
|
||||
val firstLine = additionalInformationLines.first().substring(":HOLD//".length).let {
|
||||
if (it.startsWith("1")) it.substring(1) else it // specifications and their implementations: line obligatory has to start with '1' but that's not always the case
|
||||
}
|
||||
val currencyOfSafekeepingAccountIsUnit = firstLine.startsWith("STK") // otherwise it's "KON“ = Contracts or ISO currency code of the category currency in the case of securities quoted in percentages
|
||||
|
||||
val firstLineParts = firstLine.split('+')
|
||||
val buyingDate = if (firstLineParts.size > 4) parse4DigitYearDate(firstLineParts[4]) else null
|
||||
|
||||
val secondLine = if (additionalInformationLines.size > 1) additionalInformationLines[1].let { if (it.startsWith("2")) it.substring(1) else it } else "" // cut off "2"; the second line is actually mandatory, but to be on the safe side
|
||||
val secondLineParts = secondLine.split('+')
|
||||
val averageCostPriceAmount = if (secondLineParts.size > 0) secondLineParts[0] else null
|
||||
val averageCostPriceCurrency = if (secondLineParts.size > 1) secondLineParts[1] else null
|
||||
|
||||
// third and fourth line are only filled in in the case of futures contracts
|
||||
|
||||
return Triple(buyingDate, averageCostPriceAmount?.let { Amount(it) }, averageCostPriceCurrency)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logError("Could not parse additional information for holding:\n$holdingBlock", e)
|
||||
}
|
||||
|
||||
return Triple(null, null, null)
|
||||
}
|
||||
|
||||
private fun parseMarketValue(holdingBlock: SwiftMessageBlock): Triple<Amount?, Instant?, Amount?> {
|
||||
try {
|
||||
val subBalanceDetailsLines = holdingBlock.getOptionalField("70C")?.split('\n')
|
||||
if (subBalanceDetailsLines != null) {
|
||||
val thirdLine = if (subBalanceDetailsLines.size > 2) subBalanceDetailsLines[2].let { if (it.startsWith("3")) it.substring(1) else it }.trim() else null
|
||||
val (marketValue, pricingTime) = if (thirdLine != null) {
|
||||
val thirdLineParts = thirdLine.split(' ')
|
||||
val marketValueAmountAndCurrency = if (thirdLineParts.size > 1) thirdLineParts[1].takeIf { it.isNotBlank() } else null
|
||||
val marketValue = marketValueAmountAndCurrency?.let { Amount(it.replace('.', ',').replace("EUR", "")) } // TODO: also check for other currencies
|
||||
val pricingTime = try {
|
||||
if (thirdLineParts.size > 2) thirdLineParts[2].let { if (it.length > 18) LocalDateTime.parse(it.substring(0, 19)).toInstant(TimeZone.EuropeBerlin) else null } else null
|
||||
} catch (e: Throwable) {
|
||||
logError("Could not parse pricing time from line: $thirdLine", e)
|
||||
null
|
||||
}
|
||||
|
||||
marketValue to pricingTime
|
||||
} else {
|
||||
null to null
|
||||
}
|
||||
|
||||
val fourthLine = if (subBalanceDetailsLines.size > 3) subBalanceDetailsLines[3].let { if (it.startsWith("4")) it.substring(1) else it }.trim() else null
|
||||
|
||||
val totalCostPrice = if (fourthLine != null) {
|
||||
val fourthLineParts = fourthLine.split(' ')
|
||||
val totalCostPriceAmountAndCurrency = if (fourthLineParts.size > 0) fourthLineParts[0] else null
|
||||
|
||||
totalCostPriceAmountAndCurrency?.let { Amount(it.replace('.', ',').replace("EUR", "")) } // TODO: also check for other currencies
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return Triple(marketValue, pricingTime, totalCostPrice)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logError("Could not map market value and total cost price, but is a non-standard anyway", e)
|
||||
}
|
||||
|
||||
return Triple(null, null, null)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.swift
|
||||
|
||||
import kotlinx.datetime.*
|
||||
import net.codinux.banking.fints.extensions.EuropeBerlin
|
||||
import net.codinux.banking.fints.log.IMessageLogAppender
|
||||
import net.codinux.banking.fints.model.Amount
|
||||
import net.codinux.banking.fints.transactions.mt940.Mt94xParserBase
|
||||
import net.codinux.banking.fints.transactions.swift.model.SwiftMessageBlock
|
||||
import net.codinux.log.logger
|
||||
|
||||
open class MtParserBase(
|
||||
open var logAppender: IMessageLogAppender? = null
|
||||
) {
|
||||
|
||||
protected val log by logger()
|
||||
|
||||
|
||||
fun parseMtString(mt: String, rememberOrderOfFields: Boolean = false): List<SwiftMessageBlock> {
|
||||
val lines = mt.lines().filterNot { it.isBlank() }
|
||||
|
||||
return parseMtStringLines(lines, rememberOrderOfFields)
|
||||
}
|
||||
|
||||
protected open fun parseMtStringLines(lines: List<String>, rememberOrderOfFields: Boolean = false): List<SwiftMessageBlock> {
|
||||
val messageBlocks = mutableListOf<SwiftMessageBlock>()
|
||||
var currentBlock = SwiftMessageBlock()
|
||||
|
||||
var fieldCode = ""
|
||||
val fieldValueLines = mutableListOf<String>()
|
||||
|
||||
lines.forEach { line ->
|
||||
// end of block
|
||||
if (line.trim() == "-") {
|
||||
if (fieldCode.isNotBlank()) {
|
||||
currentBlock.addField(fieldCode, fieldValueLines, rememberOrderOfFields)
|
||||
}
|
||||
messageBlocks.add(currentBlock)
|
||||
|
||||
currentBlock = SwiftMessageBlock()
|
||||
fieldCode = ""
|
||||
fieldValueLines.clear() // actually not necessary
|
||||
}
|
||||
// start of a new field
|
||||
else if (line.length > 5 && line[0] == ':' && line[1].isDigit() && line[2].isDigit() && (line[3] == ':' || line[3].isLetter() && line[4] == ':')) {
|
||||
if (fieldCode.isNotBlank()) {
|
||||
currentBlock.addField(fieldCode, fieldValueLines, rememberOrderOfFields)
|
||||
}
|
||||
|
||||
val fieldCodeContainsLetter = line[3].isLetter()
|
||||
fieldCode = line.substring(1, if (fieldCodeContainsLetter) 4 else 3)
|
||||
fieldValueLines.clear()
|
||||
fieldValueLines.add(if (fieldCodeContainsLetter) line.substring(5) else line.substring(4))
|
||||
}
|
||||
// a line that belongs to previous field value
|
||||
else {
|
||||
fieldValueLines.add(line)
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldCode.isNotBlank()) {
|
||||
currentBlock.addField(fieldCode, fieldValueLines, rememberOrderOfFields)
|
||||
}
|
||||
if (currentBlock.hasFields) {
|
||||
messageBlocks.add(currentBlock)
|
||||
}
|
||||
|
||||
return messageBlocks
|
||||
}
|
||||
|
||||
|
||||
open fun parse4DigitYearDate(dateString: String): LocalDate {
|
||||
val year = dateString.substring(0, 4).toInt()
|
||||
val month = dateString.substring(4, 6).toInt()
|
||||
val day = dateString.substring(6, 8).toInt()
|
||||
|
||||
return LocalDate(year , month, fixDay(year, month, day))
|
||||
}
|
||||
|
||||
open fun parseDate(dateString: String): LocalDate {
|
||||
try {
|
||||
var year = dateString.substring(0, 2).toInt()
|
||||
val month = dateString.substring(2, 4).toInt()
|
||||
val day = dateString.substring(4, 6).toInt()
|
||||
|
||||
/**
|
||||
* Bei 6-stelligen Datumsangaben (d.h. JJMMTT) wird gemäß SWIFT zwischen dem 20. und 21.
|
||||
* Jahrhundert wie folgt unterschieden:
|
||||
* - Ist das Jahr (d.h. JJ) größer als 79, bezieht sich das Datum auf das 20. Jahrhundert. Ist
|
||||
* das Jahr 79 oder kleiner, bezieht sich das Datum auf das 21. Jahrhundert.
|
||||
* - Ist JJ > 79:JJMMTT = 19JJMMTT
|
||||
* - sonst: JJMMTT = 20JJMMTT
|
||||
* - Damit reicht die Spanne des sechsstelligen Datums von 1980 bis 2079.
|
||||
*/
|
||||
if (year > 79) {
|
||||
year += 1900
|
||||
} else {
|
||||
year += 2000
|
||||
}
|
||||
|
||||
return LocalDate(year , month, fixDay(year, month, day))
|
||||
} catch (e: Throwable) {
|
||||
logError("Could not parse dateString '$dateString'", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixDay(year: Int, month: Int, day: Int): Int {
|
||||
// ah, here we go, banks (in Germany) calculate with 30 days each month, so yes, it can happen that dates
|
||||
// like 30th of February or 29th of February in non-leap years occur, see:
|
||||
// https://de.m.wikipedia.org/wiki/30._Februar#30._Februar_in_der_Zinsberechnung
|
||||
if (month == 2 && (day > 29 || (day > 28 && year % 4 != 0))) { // fix that for banks each month has 30 days
|
||||
return 28
|
||||
}
|
||||
|
||||
return day
|
||||
}
|
||||
|
||||
open fun parseTime(timeString: String): LocalTime {
|
||||
val hour = timeString.substring(0, 2).toInt()
|
||||
val minute = timeString.substring(2, 4).toInt()
|
||||
|
||||
return LocalTime(hour, minute)
|
||||
}
|
||||
|
||||
open fun parseDateTime(dateTimeString: String): Instant {
|
||||
val date = parseDate(dateTimeString.substring(0, 6))
|
||||
|
||||
val time = parseTime(dateTimeString.substring(6, 10))
|
||||
|
||||
val dateTime = LocalDateTime(date, time)
|
||||
|
||||
return if (dateTimeString.length == 15) { // actually mandatory, but by far not always stated: the time zone
|
||||
val plus = dateTimeString[10] == '+'
|
||||
val timeDifference = parseTime(dateTimeString.substring(11))
|
||||
|
||||
dateTime.toInstant(UtcOffset(if (plus) timeDifference.hour else timeDifference.hour * -1, timeDifference.minute))
|
||||
} else { // we then assume the server states the DateTime in FinTS's default time zone, Europe/Berlin
|
||||
dateTime.toInstant(TimeZone.EuropeBerlin)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun parseAmount(amountString: String): Amount {
|
||||
return Amount(amountString)
|
||||
}
|
||||
|
||||
|
||||
protected open fun logError(message: String, e: Throwable?) {
|
||||
logAppender?.logError(Mt94xParserBase::class, message, e)
|
||||
?: run {
|
||||
log.error(e) { message }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.swift.model
|
||||
|
||||
enum class ContinuationIndicator(internal val mtValue: String) {
|
||||
/**
|
||||
* The only page
|
||||
*/
|
||||
SinglePage("ONLY"),
|
||||
|
||||
/**
|
||||
* Intermediate page, more pages follow
|
||||
*/
|
||||
IntermediatePage("MORE"),
|
||||
|
||||
/**
|
||||
* Last page
|
||||
*/
|
||||
LastPage("LAST"),
|
||||
|
||||
Unknown("NotAMtValue")
|
||||
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.swift.model
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.codinux.banking.fints.model.Amount
|
||||
|
||||
@Serializable
|
||||
data class Holding(
|
||||
val name: String,
|
||||
val isin: String?,
|
||||
val wkn: String?,
|
||||
val buyingDate: LocalDate?,
|
||||
val quantity: Double?,
|
||||
/**
|
||||
* (Durchschnittlicher) Einstandspreis/-kurs einer Einheit des Wertpapiers
|
||||
*/
|
||||
val averageCostPrice: Amount?,
|
||||
/**
|
||||
* Gesamter Kurswert aller Einheiten des Wertpapiers
|
||||
*/
|
||||
val totalBalance: Amount?,
|
||||
val currency: String? = null,
|
||||
|
||||
/**
|
||||
* Aktueller Kurswert einer einzelnen Einheit des Wertpapiers
|
||||
*/
|
||||
val marketValue: Amount? = null,
|
||||
/**
|
||||
* Zeitpunkt zu dem der Kurswert bestimmt wurde
|
||||
*/
|
||||
val pricingTime: Instant? = null,
|
||||
/**
|
||||
* Gesamter Einstandspreis
|
||||
*/
|
||||
val totalCostPrice: Amount? = null
|
||||
)
|
|
@ -1,36 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.swift.model
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.codinux.banking.fints.model.Amount
|
||||
|
||||
/**
|
||||
* 4.3 MT 535 Depotaufstellung
|
||||
* „Statement of Holdings“; basiert auf SWIFT „Standards Release Guide“
|
||||
* (letzte berücksichtigte Änderung SRG 1998)
|
||||
*/
|
||||
@Serializable
|
||||
data class StatementOfHoldings(
|
||||
val bankCode: String,
|
||||
val accountIdentifier: String,
|
||||
|
||||
val holdings: List<Holding>,
|
||||
|
||||
val totalBalance: Amount? = null,
|
||||
val currency: String? = null,
|
||||
|
||||
/**
|
||||
* The page number is actually mandatory, but to be prepared for surprises like for [statementDate] i added error
|
||||
* handling and made it optional.
|
||||
*/
|
||||
val pageNumber: Int? = null,
|
||||
val continuationIndicator: ContinuationIndicator = ContinuationIndicator.Unknown,
|
||||
|
||||
/**
|
||||
* The statement date is actually mandatory, but not all banks actually set it.
|
||||
*/
|
||||
val statementDate: LocalDate? = null,
|
||||
val preparationDate: LocalDate? = null
|
||||
) {
|
||||
override fun toString() = "$bankCode ${holdings.size} holdings: ${holdings.joinToString { "{${it.name} ${it.totalBalance}" }}"
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package net.codinux.banking.fints.transactions.swift.model
|
||||
|
||||
class SwiftMessageBlock(
|
||||
initialFields: List<Pair<String, String>>? = null
|
||||
) {
|
||||
|
||||
private val fields = LinkedHashMap<String, MutableList<String>>()
|
||||
|
||||
private val fieldsInOrder = mutableListOf<Pair<String, String>>()
|
||||
|
||||
val hasFields: Boolean
|
||||
get() = fields.isNotEmpty()
|
||||
|
||||
val fieldCodes: Collection<String>
|
||||
get() = fields.keys
|
||||
|
||||
init {
|
||||
initialFields?.forEach { (fieldCode, fieldValue) ->
|
||||
addField(fieldCode, fieldValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun addField(fieldCode: String, fieldValueLines: List<String>, rememberOrderOfFields: Boolean = false) {
|
||||
val fieldValue = fieldValueLines.joinToString("\n")
|
||||
|
||||
addField(fieldCode, fieldValue, rememberOrderOfFields)
|
||||
}
|
||||
|
||||
fun addField(fieldCode: String, fieldValue: String, rememberOrderOfFields: Boolean = false) {
|
||||
fields.getOrPut(fieldCode) { mutableListOf() }.add(fieldValue)
|
||||
|
||||
if (rememberOrderOfFields) {
|
||||
fieldsInOrder.add(Pair(fieldCode, fieldValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getFieldsInOrder(): List<Pair<String, String>> = fieldsInOrder.toList() // make a copy
|
||||
|
||||
fun getMandatoryField(fieldCode: String): String =
|
||||
getMandatoryFieldValue(fieldCode).first()
|
||||
|
||||
fun getOptionalField(fieldCode: String): String? =
|
||||
getOptionalFieldValue(fieldCode)?.first()
|
||||
|
||||
fun getMandatoryRepeatableField(fieldCode: String): List<String> =
|
||||
getMandatoryFieldValue(fieldCode)
|
||||
|
||||
fun getOptionalRepeatableField(fieldCode: String): List<String>? =
|
||||
getOptionalFieldValue(fieldCode)
|
||||
|
||||
private fun getMandatoryFieldValue(fieldCode: String): List<String> =
|
||||
fields[fieldCode] ?: fields.entries.firstOrNull { it.key.startsWith(fieldCode) }?.value
|
||||
?: throw IllegalStateException("Block contains no field with code '$fieldCode'. Available fields: ${fields.keys}")
|
||||
|
||||
private fun getOptionalFieldValue(fieldCode: String): List<String>? = fields[fieldCode]
|
||||
|
||||
|
||||
override fun toString() =
|
||||
if (fieldsInOrder.isNotEmpty()) {
|
||||
fieldsInOrder.joinToString("\n")
|
||||
} else {
|
||||
fields.entries.joinToString("\n") { "${it.key}${it.value}" }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package net.codinux.banking.fints.util
|
||||
|
||||
import net.codinux.banking.fints.model.TanMethod
|
||||
import net.codinux.banking.fints.model.TanMethodType
|
||||
|
||||
|
||||
open class TanMethodSelector {
|
||||
|
||||
companion object {
|
||||
|
||||
val NonVisual = listOf(TanMethodType.DecoupledTan, TanMethodType.DecoupledPushTan, TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.ChipTanManuell, TanMethodType.EnterTan)
|
||||
|
||||
val ImageBased = listOf(TanMethodType.QrCode, TanMethodType.ChipTanQrCode, TanMethodType.photoTan, TanMethodType.ChipTanPhotoTanMatrixCode)
|
||||
|
||||
/**
|
||||
* NonVisualOrImageBased is a good default for most users as it lists the most simplistic ones (which also work with
|
||||
* the command line) first and then continues with image based TAN methods, which for UI applications are easily to display.
|
||||
*/
|
||||
val NonVisualOrImageBased = buildList {
|
||||
// decoupled TAN method is the most simplistic TAN method, user only has to confirm the action in her TAN app, no manual TAN entering required
|
||||
// AppTan is the second most simplistic TAN method: user has to confirm action in her TAN app and then enter the displayed TAN
|
||||
addAll(listOf(TanMethodType.DecoupledTan, TanMethodType.DecoupledPushTan, TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.EnterTan))
|
||||
addAll(ImageBased)
|
||||
addAll(listOf(TanMethodType.ChipTanManuell)) // this is quite inconvenient for user, so i added it as last
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
open fun getSuggestedTanMethod(tanMethods: List<TanMethod>, tanMethodsNotSupportedByApplication: List<TanMethodType> = emptyList()): TanMethod? {
|
||||
return findPreferredTanMethod(tanMethods, NonVisualOrImageBased, tanMethodsNotSupportedByApplication) // we use NonVisualOrImageBased as it provides a good default for most users
|
||||
?: tanMethods.firstOrNull { it.type !in tanMethodsNotSupportedByApplication }
|
||||
}
|
||||
|
||||
open fun findPreferredTanMethod(tanMethods: List<TanMethod>, preferredTanMethods: List<TanMethodType>?, tanMethodsNotSupportedByApplication: List<TanMethodType> = emptyList()): TanMethod? {
|
||||
preferredTanMethods?.forEach { preferredTanMethodType ->
|
||||
if (preferredTanMethodType !in tanMethodsNotSupportedByApplication) {
|
||||
tanMethods.firstOrNull { it.type == preferredTanMethodType }?.let {
|
||||
return it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
|
@ -2,80 +2,78 @@ package net.dankito.banking.client.model
|
|||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.codinux.banking.fints.model.Amount
|
||||
import net.codinux.banking.fints.model.Money
|
||||
import net.codinux.banking.fints.extensions.UnixEpochStart
|
||||
import net.dankito.banking.fints.model.Amount
|
||||
import net.dankito.banking.fints.model.Money
|
||||
import net.dankito.banking.fints.extensions.UnixEpochStart
|
||||
|
||||
|
||||
@Serializable
|
||||
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 reference: String?, // alternative names: purpose, reason
|
||||
|
||||
val unparsedReference: String,
|
||||
val bookingDate: LocalDate,
|
||||
val valueDate: LocalDate,
|
||||
|
||||
val otherPartyName: String?,
|
||||
val otherPartyBankId: String?,
|
||||
val otherPartyBankCode: String?,
|
||||
val otherPartyAccountId: String?,
|
||||
|
||||
val postingText: String?,
|
||||
|
||||
val bookingText: String?,
|
||||
val valueDate: LocalDate,
|
||||
val statementNumber: Int,
|
||||
val sequenceNumber: Int?,
|
||||
val openingBalance: Money?,
|
||||
val closingBalance: Money?,
|
||||
|
||||
val statementNumber: Int,
|
||||
val sheetNumber: Int?,
|
||||
|
||||
val customerReference: String?,
|
||||
val bankReference: String?,
|
||||
val furtherInformation: String?,
|
||||
|
||||
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 journalNumber: String?,
|
||||
val textKeyAddition: String?,
|
||||
val currencyType: String?,
|
||||
val bookingKey: String,
|
||||
val referenceForTheAccountOwner: String,
|
||||
val referenceOfTheAccountServicingInstitution: String?,
|
||||
val supplementaryDetails: String?,
|
||||
|
||||
val orderReferenceNumber: String?,
|
||||
val referenceNumber: String?,
|
||||
|
||||
val isReversal: Boolean
|
||||
val transactionReferenceNumber: String,
|
||||
val relatedReferenceNumber: String?
|
||||
) {
|
||||
|
||||
// for object deserializers
|
||||
internal constructor() : this(Money(Amount.Zero, ""), "", UnixEpochStart, UnixEpochStart, null, null, null, null)
|
||||
internal constructor() : this(Money(Amount.Zero, ""), "", UnixEpochStart, null, null, null, null, UnixEpochStart)
|
||||
|
||||
constructor(amount: Money, unparsedReference: String, bookingDate: LocalDate, valueDate: LocalDate, otherPartyName: String?, otherPartyBankId: String?, otherPartyAccountId: String?, postingText: String?)
|
||||
: this(amount, unparsedReference, bookingDate, valueDate, otherPartyName, otherPartyBankId, otherPartyAccountId, postingText,
|
||||
null, null, 0, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, false)
|
||||
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 (reference != other.reference) return false
|
||||
if (unparsedReference != other.unparsedReference) return false
|
||||
if (bookingDate != other.bookingDate) return false
|
||||
if (otherPartyName != other.otherPartyName) return false
|
||||
if (otherPartyBankId != other.otherPartyBankId) return false
|
||||
if (otherPartyBankCode != other.otherPartyBankCode) return false
|
||||
if (otherPartyAccountId != other.otherPartyAccountId) return false
|
||||
if (postingText != other.postingText) return false
|
||||
if (bookingText != other.bookingText) return false
|
||||
if (valueDate != other.valueDate) return false
|
||||
|
||||
return true
|
||||
|
@ -83,19 +81,19 @@ open class AccountTransaction(
|
|||
|
||||
override fun hashCode(): Int {
|
||||
var result = amount.hashCode()
|
||||
result = 31 * result + reference.hashCode()
|
||||
result = 31 * result + unparsedReference.hashCode()
|
||||
result = 31 * result + bookingDate.hashCode()
|
||||
result = 31 * result + otherPartyName.hashCode()
|
||||
result = 31 * result + otherPartyBankId.hashCode()
|
||||
result = 31 * result + otherPartyAccountId.hashCode()
|
||||
result = 31 * result + postingText.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: $reference"
|
||||
return "$valueDate $amount $otherPartyName: $unparsedReference"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package net.dankito.banking.client.model
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.codinux.banking.fints.model.Currency
|
||||
import net.codinux.banking.fints.model.Money
|
||||
import net.codinux.banking.fints.transactions.swift.model.StatementOfHoldings
|
||||
import net.dankito.banking.fints.model.Currency
|
||||
import net.dankito.banking.fints.model.Money
|
||||
|
||||
|
||||
@Serializable
|
||||
|
@ -19,7 +17,7 @@ open class BankAccount(
|
|||
open val currency: String = Currency.DefaultCurrencyCode, // TODO: may parse to a value object
|
||||
open val accountLimit: String? = null,
|
||||
|
||||
open val serverTransactionsRetentionDays: Int? = null,
|
||||
open val countDaysForWhichTransactionsAreKept: Int? = null,
|
||||
open val isAccountTypeSupportedByApplication: Boolean = false,
|
||||
// TODO: create an enum AccountCapabilities [ RetrieveBalance, RetrieveTransactions, TransferMoney / MoneyTransfer(?), InstantPayment ]
|
||||
open val supportsRetrievingTransactions: Boolean = false,
|
||||
|
@ -37,16 +35,10 @@ open class BankAccount(
|
|||
|
||||
open var retrievedTransactionsFrom: LocalDate? = null
|
||||
|
||||
/**
|
||||
* Gibt wider, wann zuletzt aktuelle Kontoumsätze, d.h. [net.dankito.banking.client.model.parameter.GetAccountDataParameter.retrieveTransactionsTo]
|
||||
* war nicht gesetzt, oder aktuelle [StatementOfHoldings] empfangen wurden.
|
||||
*/
|
||||
open var lastAccountUpdateTime: Instant? = null
|
||||
open var retrievedTransactionsTo: LocalDate? = null
|
||||
|
||||
open var bookedTransactions: List<AccountTransaction> = listOf()
|
||||
|
||||
open var statementOfHoldings: List<StatementOfHoldings> = emptyList()
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$productName ($identifier)"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package net.dankito.banking.client.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.model.TanMethod
|
||||
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
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package net.dankito.banking.client.model.parameter
|
||||
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.TanMethodType
|
||||
import net.codinux.banking.fints.serialization.FinTsModelSerializer
|
||||
import net.dankito.banking.fints.model.BankData
|
||||
import net.dankito.banking.fints.model.TanMethodType
|
||||
import net.dankito.banking.client.model.CustomerCredentials
|
||||
|
||||
|
||||
|
@ -13,15 +12,7 @@ open class FinTsClientParameter(
|
|||
password: String,
|
||||
|
||||
open val preferredTanMethods: List<TanMethodType>? = null,
|
||||
open val tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
|
||||
open val preferredTanMedium: String? = null, // the ID of the medium
|
||||
open val abortIfTanIsRequired: Boolean = false,
|
||||
open val finTsModel: BankData? = null,
|
||||
open val serializedFinTsModel: String? = null
|
||||
) : CustomerCredentials(bankCode, loginName, password) {
|
||||
|
||||
open val finTsModelOrDeserialized: BankData? by lazy {
|
||||
finTsModel ?: serializedFinTsModel?.let { FinTsModelSerializer.deserializeFromJson(it) }
|
||||
}
|
||||
|
||||
}
|
||||
open val finTsModel: BankData? = null
|
||||
) : CustomerCredentials(bankCode, loginName, password)
|
|
@ -1,8 +1,8 @@
|
|||
package net.dankito.banking.client.model.parameter
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.TanMethodType
|
||||
import net.dankito.banking.fints.model.BankData
|
||||
import net.dankito.banking.fints.model.TanMethodType
|
||||
import net.dankito.banking.client.model.BankAccountIdentifier
|
||||
|
||||
|
||||
|
@ -21,13 +21,10 @@ open class GetAccountDataParameter(
|
|||
open val retrieveTransactionsTo: LocalDate? = null,
|
||||
|
||||
preferredTanMethods: List<TanMethodType>? = null,
|
||||
tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
|
||||
preferredTanMedium: String? = null,
|
||||
abortIfTanIsRequired: Boolean = false,
|
||||
finTsModel: BankData? = null,
|
||||
serializedFinTsModel: String? = null,
|
||||
open val defaultBankValues: BankData? = null
|
||||
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, tanMethodsNotSupportedByApplication, preferredTanMedium, abortIfTanIsRequired, finTsModel, serializedFinTsModel) {
|
||||
finTsModel: BankData? = null
|
||||
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, preferredTanMedium, abortIfTanIsRequired, finTsModel) {
|
||||
|
||||
open val retrieveOnlyAccountInfo: Boolean
|
||||
get() = retrieveBalance == false && retrieveTransactions == RetrieveTransactions.No
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package net.dankito.banking.client.model.parameter
|
||||
|
||||
import net.dankito.banking.client.model.BankAccountIdentifier
|
||||
import net.codinux.banking.fints.model.AccountData
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.Money
|
||||
import net.codinux.banking.fints.model.TanMethodType
|
||||
import net.dankito.banking.fints.model.AccountData
|
||||
import net.dankito.banking.fints.model.BankData
|
||||
import net.dankito.banking.fints.model.Money
|
||||
import net.dankito.banking.fints.model.TanMethodType
|
||||
|
||||
|
||||
open class TransferMoneyParameter(
|
||||
|
@ -34,12 +34,10 @@ open class TransferMoneyParameter(
|
|||
open val instantPayment: Boolean = false,
|
||||
|
||||
preferredTanMethods: List<TanMethodType>? = null,
|
||||
tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
|
||||
preferredTanMedium: String? = null,
|
||||
abortIfTanIsRequired: Boolean = false,
|
||||
finTsModel: BankData? = null,
|
||||
serializedFinTsModel: String? = null,
|
||||
|
||||
open val selectAccountToUseForTransfer: ((List<AccountData>) -> AccountData?)? = null // TODO: use BankAccount instead of AccountData
|
||||
|
||||
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, tanMethodsNotSupportedByApplication, preferredTanMedium, abortIfTanIsRequired, finTsModel, serializedFinTsModel)
|
||||
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, preferredTanMedium, abortIfTanIsRequired, finTsModel)
|
|
@ -1,16 +1,15 @@
|
|||
package net.dankito.banking.client.model.response
|
||||
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.MessageLogEntry
|
||||
import net.codinux.banking.fints.serialization.FinTsModelSerializer
|
||||
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 messageLog: List<MessageLogEntry>,
|
||||
open val finTsModel: BankData? = null
|
||||
open val error: ErrorCode?,
|
||||
open val errorMessage: String?,
|
||||
open val messageLogWithoutSensitiveData: List<MessageLogEntry>,
|
||||
open val finTsModel: BankData? = null
|
||||
) {
|
||||
|
||||
internal constructor() : this(null, null, listOf()) // for object deserializers
|
||||
|
@ -22,7 +21,4 @@ open class FinTsClientResponse(
|
|||
open val errorCodeAndMessage: String
|
||||
get() = "$error${errorMessage?.let { " $it" }}"
|
||||
|
||||
// save some CPU cycles, only serialize finTsModel if required
|
||||
open val serializedFinTsModel: String? by lazy { finTsModel?.let { FinTsModelSerializer.serializeToJson(it) } }
|
||||
|
||||
}
|
|
@ -2,16 +2,16 @@ package net.dankito.banking.client.model.response
|
|||
|
||||
import net.dankito.banking.client.model.AccountTransaction
|
||||
import net.dankito.banking.client.model.CustomerAccount
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.dankito.banking.fints.model.*
|
||||
|
||||
|
||||
open class GetAccountDataResponse(
|
||||
error: ErrorCode?,
|
||||
errorMessage: String?,
|
||||
open val customerAccount: CustomerAccount?,
|
||||
messageLog: List<MessageLogEntry>,
|
||||
messageLogWithoutSensitiveData: List<MessageLogEntry>,
|
||||
finTsModel: BankData? = null
|
||||
) : FinTsClientResponse(error, errorMessage, messageLog, finTsModel) {
|
||||
) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel) {
|
||||
|
||||
internal constructor() : this(null, null, null, listOf()) // for object deserializers
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package net.dankito.banking.client.model.response
|
||||
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.MessageLogEntry
|
||||
import net.dankito.banking.fints.model.BankData
|
||||
import net.dankito.banking.fints.model.MessageLogEntry
|
||||
|
||||
|
||||
open class TransferMoneyResponse(
|
||||
error: ErrorCode?,
|
||||
errorMessage: String?,
|
||||
messageLog: List<MessageLogEntry>,
|
||||
finTsModel: BankData? = null
|
||||
) : FinTsClientResponse(error, errorMessage, messageLog, finTsModel)
|
||||
error: ErrorCode?,
|
||||
errorMessage: String?,
|
||||
messageLogWithoutSensitiveData: List<MessageLogEntry>,
|
||||
finTsModel: BankData? = null
|
||||
) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel)
|
|
@ -6,7 +6,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import net.codinux.banking.fints.model.Amount
|
||||
import net.dankito.banking.fints.model.Amount
|
||||
|
||||
|
||||
class AmountSerializer : KSerializer<Amount> {
|
||||
|
|
|
@ -6,7 +6,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import net.codinux.banking.fints.model.Currency
|
||||
import net.dankito.banking.fints.model.Currency
|
||||
|
||||
|
||||
class CurrencySerializer : KSerializer<Currency> {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package net.codinux.banking.fints
|
||||
package net.dankito.banking.fints
|
||||
|
||||
import net.dankito.banking.client.model.parameter.FinTsClientParameter
|
||||
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
||||
|
@ -6,21 +6,15 @@ import net.dankito.banking.client.model.parameter.TransferMoneyParameter
|
|||
import net.dankito.banking.client.model.response.ErrorCode
|
||||
import net.dankito.banking.client.model.response.GetAccountDataResponse
|
||||
import net.dankito.banking.client.model.response.TransferMoneyResponse
|
||||
import net.codinux.banking.fints.callback.FinTsClientCallback
|
||||
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
||||
import net.codinux.banking.fints.mapper.FinTsModelMapper
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.KundensystemID
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.codinux.banking.fints.response.client.FinTsClientResponse
|
||||
import net.codinux.banking.fints.response.client.GetAccountInfoResponse
|
||||
import net.codinux.banking.fints.response.client.GetAccountTransactionsResponse
|
||||
import net.codinux.banking.fints.response.segments.AccountType
|
||||
import net.codinux.banking.fints.response.segments.BankParameters
|
||||
import net.codinux.banking.fints.util.BicFinder
|
||||
import net.codinux.log.LogLevel
|
||||
import net.codinux.log.LoggerFactory
|
||||
import kotlin.js.JsName
|
||||
import kotlin.jvm.JvmName
|
||||
import net.dankito.banking.fints.callback.FinTsClientCallback
|
||||
import net.dankito.banking.fints.config.FinTsClientConfiguration
|
||||
import net.dankito.banking.fints.mapper.FinTsModelMapper
|
||||
import net.dankito.banking.fints.model.*
|
||||
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.util.BicFinder
|
||||
|
||||
|
||||
open class FinTsClient(
|
||||
|
@ -41,59 +35,56 @@ open class FinTsClient(
|
|||
protected open val bicFinder = BicFinder()
|
||||
|
||||
|
||||
init {
|
||||
LoggerFactory.getLogger("net.codinux.banking.fints.log.MessageLogCollector").level = if (config.options.appendFinTsMessagesToLog) {
|
||||
LogLevel.Debug
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open suspend fun getAccountDataAsync(bankCode: String, loginName: String, password: String): GetAccountDataResponse {
|
||||
return getAccountDataAsync(GetAccountDataParameter(bankCode, loginName, password))
|
||||
}
|
||||
|
||||
open suspend fun getAccountDataAsync(param: GetAccountDataParameter): GetAccountDataResponse {
|
||||
val basicAccountDataResponse = getRequiredDataToSendUserJobs(param)
|
||||
val bank = basicAccountDataResponse.finTsModel
|
||||
val finTsServerAddress = config.finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
|
||||
if (finTsServerAddress.isNullOrBlank()) {
|
||||
return GetAccountDataResponse(ErrorCode.BankDoesNotSupportFinTs3, "Either bank does not support FinTS 3.0 or we don't know its FinTS server address", null, listOf())
|
||||
}
|
||||
|
||||
if (basicAccountDataResponse.successful == false || param.retrieveOnlyAccountInfo || bank == null) {
|
||||
return GetAccountDataResponse(basicAccountDataResponse.error, basicAccountDataResponse.errorMessage, null,
|
||||
basicAccountDataResponse.messageLog, bank)
|
||||
val bank = mapper.mapToBankData(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.messageLog, bank)
|
||||
} else {
|
||||
return getAccountData(param, getAccountInfoResponse.bank, getAccountInfoResponse.bank.accounts, getAccountInfoResponse)
|
||||
}
|
||||
} else {
|
||||
return getAccountData(param, bank, bank.accounts, basicAccountDataResponse.messageLog)
|
||||
return getAccountData(param, bank, accounts.map { mapper.mapToAccountData(it, param) }, null)
|
||||
}
|
||||
}
|
||||
|
||||
protected open suspend fun getAccountData(param: GetAccountDataParameter, bank: BankData, accounts: List<AccountData>, previousJobMessageLog: List<MessageLogEntry>?): GetAccountDataResponse {
|
||||
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), previousJobMessageLog ?: listOf(), bank)
|
||||
return GetAccountDataResponse(ErrorCode.NoneOfTheAccountsSupportsRetrievingData, errorMessage, mapper.map(bank), previousJobResponse?.messageLog ?: listOf(), bank)
|
||||
}
|
||||
|
||||
for (account in accountsSupportingRetrievingTransactions) {
|
||||
val response = getAccountTransactions(param, bank, account)
|
||||
retrievedTransactionsResponses.add(response)
|
||||
|
||||
if (response.tanRequiredButWeWereToldToAbortIfSo || response.userCancelledAction) { // if user cancelled action or TAN is required but we were told to abort then, then don't continue with next account
|
||||
break
|
||||
}
|
||||
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, param.retrieveTransactionsTo),
|
||||
mapper.mergeMessageLog(previousJobMessageLog, *retrievedTransactionsResponses.map { it.messageLog }.toTypedArray()), bank)
|
||||
return GetAccountDataResponse(errorCode, mapper.mapErrorMessages(unsuccessfulJob), mapper.map(bank, retrievedTransactionsResponses),
|
||||
mapper.mergeMessageLog(previousJobResponse, *retrievedTransactionsResponses.toTypedArray()), bank)
|
||||
}
|
||||
|
||||
protected open suspend fun getAccountTransactions(param: GetAccountDataParameter, bank: BankData, account: AccountData): GetAccountTransactionsResponse {
|
||||
val context = JobContext(JobContextType.GetTransactions, this.callback, config, bank, account, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium)
|
||||
protected open suspend fun getAccountData(param: GetAccountDataParameter, bank: BankData, account: AccountData): GetAccountTransactionsResponse {
|
||||
val context = JobContext(JobContextType.GetTransactions, this.callback, config, bank, account)
|
||||
|
||||
return config.jobExecutor.getTransactionsAsync(context, mapper.toGetAccountTransactionsParameter(param, bank, account))
|
||||
}
|
||||
|
@ -151,7 +142,7 @@ open class FinTsClient(
|
|||
accountToUse = selectedAccount
|
||||
}
|
||||
|
||||
val context = JobContext(JobContextType.TransferMoney, this.callback, config, bank, accountToUse, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium)
|
||||
val context = JobContext(JobContextType.TransferMoney, this.callback, config, bank, accountToUse)
|
||||
|
||||
val response = config.jobExecutor.transferMoneyAsync(context, BankTransferData(param.recipientName, param.recipientAccountIdentifier, recipientBankIdentifier,
|
||||
param.amount, param.reference, param.instantPayment))
|
||||
|
@ -172,65 +163,25 @@ open class FinTsClient(
|
|||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures all basic data to initialize a dialog with strong customer authorization is retrieved so you can send your
|
||||
* actual jobs (Geschäftsvorfälle) to your bank's FinTS server.
|
||||
*
|
||||
* These data include:
|
||||
* - Bank communication data like FinTS server address, BIC, bank name, bank code used for FinTS.
|
||||
* - BPD (BankParameterDaten): bank name, BPD version, supported languages, supported HBCI versions, supported TAN methods,
|
||||
* max count jobs per message (Anzahl Geschäftsvorfallsarten) (see [BankParameters] [BankParameters](src/commonMain/kotlin/net/codinux/banking/fints/response/segmentsBankParameters) ).
|
||||
* - Min and max online banking password length, min TAN length, hint for login name (for all: if available)
|
||||
* - UPD (UserParameterDaten): username, UPD version.
|
||||
* - Customer system ID (Kundensystem-ID, see [KundensystemID]), TAN methods available for user and may user's TAN media.
|
||||
* - Which jobs the bank supports and which jobs need strong customer authorization (= require HKTAN segment).
|
||||
* - Which jobs the user is allowed to use.
|
||||
* - Which jobs can be called for a specific bank account.
|
||||
*
|
||||
* When implementing your own jobs, call this method first, then send an init dialog message and in next message your actual jobs.
|
||||
*
|
||||
* More or less implements everything of 02 FinTS_3.0_Formals.pdf so that you can start directly with the jobs from
|
||||
* 04 FinTS_3.0_Messages_Geschaeftsvorfaelle.pdf
|
||||
*/
|
||||
open suspend fun getRequiredDataToSendUserJobs(param: FinTsClientParameter): net.dankito.banking.client.model.response.FinTsClientResponse {
|
||||
param.finTsModelOrDeserialized?.let { finTsModel ->
|
||||
return net.dankito.banking.client.model.response.FinTsClientResponse(null, null, emptyList(), finTsModel)
|
||||
}
|
||||
|
||||
val defaultValues = (param as? GetAccountDataParameter)?.defaultBankValues
|
||||
|
||||
val finTsServerAddress = defaultValues?.finTs3ServerAddress ?: config.finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
|
||||
if (finTsServerAddress.isNullOrBlank()) {
|
||||
return net.dankito.banking.client.model.response.FinTsClientResponse(ErrorCode.BankDoesNotSupportFinTs3, "Either bank does not support FinTS 3.0 or we don't know its FinTS server address", emptyList(), null)
|
||||
}
|
||||
|
||||
val bank = mapper.mapToBankData(param, finTsServerAddress, defaultValues)
|
||||
|
||||
val getAccountInfoResponse = getAccountInfo(param, bank)
|
||||
|
||||
return net.dankito.banking.client.model.response.FinTsClientResponse(mapper.mapErrorCode(getAccountInfoResponse), mapper.mapErrorMessages(getAccountInfoResponse),
|
||||
getAccountInfoResponse.messageLog, bank)
|
||||
}
|
||||
|
||||
protected open suspend fun getAccountInfo(param: FinTsClientParameter, bank: BankData): GetAccountInfoResponse {
|
||||
param.finTsModelOrDeserialized?.let {
|
||||
param.finTsModel?.let {
|
||||
// TODO: implement
|
||||
// return GetAccountInfoResponse(it)
|
||||
}
|
||||
|
||||
val context = JobContext(JobContextType.GetAccountInfo, this.callback, config, bank, null, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium)
|
||||
val context = JobContext(JobContextType.GetAccountInfo, this.callback, config, bank)
|
||||
|
||||
/* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */
|
||||
|
||||
val newUserInfoResponse = config.jobExecutor.retrieveBasicDataLikeUsersTanMethods(context)
|
||||
|
||||
/* 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 newUserInfoResponse = config.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 = config.jobExecutor.getAccounts(context)
|
||||
|
||||
return GetAccountInfoResponse(context, getAccountsResponse)
|
|
@ -1,14 +1,17 @@
|
|||
package net.codinux.banking.fints
|
||||
package net.dankito.banking.fints
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.*
|
||||
import net.codinux.banking.fints.callback.FinTsClientCallback
|
||||
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
||||
import net.codinux.banking.fints.extensions.minusDays
|
||||
import net.codinux.banking.fints.extensions.todayAtEuropeBerlin
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.*
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.codinux.banking.fints.response.BankResponse
|
||||
import net.codinux.banking.fints.response.client.*
|
||||
import net.dankito.banking.fints.callback.FinTsClientCallback
|
||||
import net.dankito.banking.fints.config.FinTsClientConfiguration
|
||||
import net.dankito.banking.fints.extensions.minusDays
|
||||
import net.dankito.banking.fints.extensions.todayAtEuropeBerlin
|
||||
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.*
|
||||
import net.dankito.banking.fints.model.*
|
||||
import net.dankito.banking.fints.response.BankResponse
|
||||
import net.dankito.banking.fints.response.client.*
|
||||
import net.dankito.banking.fints.webclient.IWebClient
|
||||
|
||||
|
||||
/**
|
||||
|
@ -23,6 +26,19 @@ open class FinTsClientDeprecated(
|
|||
constructor(callback: FinTsClientCallback) : this(FinTsClientConfiguration(), callback)
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves information about bank (e.g. supported HBCI versions, FinTS server address,
|
||||
* supported jobs, ...).
|
||||
*
|
||||
* On success [bank] parameter is updated afterwards.
|
||||
*/
|
||||
open fun getAnonymousBankInfoAsync(bank: BankData, callback: (FinTsClientResponse) -> Unit) {
|
||||
|
||||
GlobalScope.launch {
|
||||
callback(getAnonymousBankInfo(bank))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information about bank (e.g. supported HBCI versions, FinTS server address,
|
||||
* supported jobs, ...).
|
||||
|
@ -37,13 +53,13 @@ open class FinTsClientDeprecated(
|
|||
}
|
||||
|
||||
|
||||
open suspend fun addAccountAsync(param: AddAccountParameter): AddAccountResponse {
|
||||
val bank = param.bank
|
||||
val context = JobContext(JobContextType.AddAccount, this.callback, config, bank, null, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium)
|
||||
open suspend fun addAccountAsync(parameter: AddAccountParameter): AddAccountResponse {
|
||||
val bank = parameter.bank
|
||||
val context = JobContext(JobContextType.AddAccount, this.callback, config, bank)
|
||||
|
||||
/* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */
|
||||
|
||||
val newUserInfoResponse = config.jobExecutor.retrieveBasicDataLikeUsersTanMethods(context)
|
||||
val newUserInfoResponse = config.jobExecutor.retrieveBasicDataLikeUsersTanMethods(context, parameter.preferredTanMethods, parameter.preferredTanMedium)
|
||||
|
||||
if (newUserInfoResponse.successful == false) { // bank parameter (FinTS server address, ...) already seem to be wrong
|
||||
return AddAccountResponse(context, newUserInfoResponse)
|
||||
|
@ -52,7 +68,7 @@ open class FinTsClientDeprecated(
|
|||
/* 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 */
|
||||
|
||||
return addAccountGetAccountsAndTransactions(context, param)
|
||||
return addAccountGetAccountsAndTransactions(context, parameter)
|
||||
}
|
||||
|
||||
protected open suspend fun addAccountGetAccountsAndTransactions(context: JobContext, parameter: AddAccountParameter): AddAccountResponse {
|
||||
|
@ -118,11 +134,11 @@ open class FinTsClientDeprecated(
|
|||
return GetAccountTransactionsParameter(bank, account, account.supportsRetrievingBalance, ninetyDaysAgo, abortIfTanIsRequired = true)
|
||||
}
|
||||
|
||||
open suspend fun getAccountTransactionsAsync(param: GetAccountTransactionsParameter): GetAccountTransactionsResponse {
|
||||
open suspend fun getAccountTransactionsAsync(parameter: GetAccountTransactionsParameter): GetAccountTransactionsResponse {
|
||||
|
||||
val context = JobContext(JobContextType.GetTransactions, this.callback, config, param.bank, param.account)
|
||||
val context = JobContext(JobContextType.GetTransactions, this.callback, config, parameter.bank, parameter.account)
|
||||
|
||||
return config.jobExecutor.getTransactionsAsync(context, param)
|
||||
return config.jobExecutor.getTransactionsAsync(context, parameter)
|
||||
}
|
||||
|
||||
|
||||
|
@ -135,7 +151,7 @@ open class FinTsClientDeprecated(
|
|||
}
|
||||
|
||||
|
||||
open suspend fun changeTanMedium(newActiveTanMedium: TanMedium, bank: BankData): FinTsClientResponse {
|
||||
open suspend fun changeTanMedium(newActiveTanMedium: TanGeneratorTanMedium, bank: BankData): FinTsClientResponse {
|
||||
val context = JobContext(JobContextType.ChangeTanMedium, this.callback, config, bank)
|
||||
|
||||
val response = config.jobExecutor.changeTanMedium(context, newActiveTanMedium)
|
|
@ -1,11 +1,11 @@
|
|||
package net.codinux.banking.fints
|
||||
package net.dankito.banking.fints
|
||||
|
||||
import net.codinux.banking.fints.callback.FinTsClientCallback
|
||||
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.codinux.banking.fints.response.client.AddAccountResponse
|
||||
import net.codinux.banking.fints.response.client.FinTsClientResponse
|
||||
import net.codinux.banking.fints.response.client.GetAccountTransactionsResponse
|
||||
import net.dankito.banking.fints.callback.FinTsClientCallback
|
||||
import net.dankito.banking.fints.config.FinTsClientConfiguration
|
||||
import net.dankito.banking.fints.model.*
|
||||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
||||
import net.dankito.banking.fints.response.client.FinTsClientResponse
|
||||
import net.dankito.banking.fints.response.client.GetAccountTransactionsResponse
|
||||
|
||||
|
||||
open class FinTsClientForCustomer(
|
|
@ -1,28 +1,26 @@
|
|||
package net.codinux.banking.fints
|
||||
package net.dankito.banking.fints
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.codinux.banking.fints.extensions.*
|
||||
import net.codinux.log.logger
|
||||
import net.codinux.banking.fints.messages.MessageBuilder
|
||||
import net.codinux.banking.fints.messages.MessageBuilderResult
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.signatur.VersionDesSicherheitsverfahrens
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.*
|
||||
import net.codinux.banking.fints.messages.segmente.id.CustomerSegmentId
|
||||
import net.codinux.banking.fints.messages.segmente.id.ISegmentId
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.codinux.banking.fints.model.mapper.ModelMapper
|
||||
import net.codinux.banking.fints.response.BankResponse
|
||||
import net.codinux.banking.fints.response.InstituteSegmentId
|
||||
import net.codinux.banking.fints.response.client.*
|
||||
import net.codinux.banking.fints.response.segments.*
|
||||
import net.codinux.banking.fints.tan.FlickerCodeDecoder
|
||||
import net.codinux.banking.fints.tan.TanImageDecoder
|
||||
import net.codinux.banking.fints.util.TanMethodSelector
|
||||
import net.codinux.log.Log
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import net.dankito.banking.fints.messages.MessageBuilder
|
||||
import net.dankito.banking.fints.messages.MessageBuilderResult
|
||||
import net.dankito.banking.fints.messages.datenelemente.implementierte.signatur.VersionDesSicherheitsverfahrens
|
||||
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.*
|
||||
import net.dankito.banking.fints.messages.segmente.id.CustomerSegmentId
|
||||
import net.dankito.banking.fints.messages.segmente.id.ISegmentId
|
||||
import net.dankito.banking.fints.model.*
|
||||
import net.dankito.banking.fints.model.mapper.ModelMapper
|
||||
import net.dankito.banking.fints.response.BankResponse
|
||||
import net.dankito.banking.fints.response.InstituteSegmentId
|
||||
import net.dankito.banking.fints.response.client.*
|
||||
import net.dankito.banking.fints.response.segments.*
|
||||
import net.dankito.banking.fints.tan.FlickerCodeDecoder
|
||||
import net.dankito.banking.fints.tan.TanImageDecoder
|
||||
import net.dankito.banking.fints.util.TanMethodSelector
|
||||
import net.dankito.banking.fints.extensions.minusDays
|
||||
import net.dankito.banking.fints.extensions.todayAtEuropeBerlin
|
||||
import net.dankito.banking.fints.extensions.todayAtSystemDefaultTimeZone
|
||||
|
||||
|
||||
/**
|
||||
|
@ -74,7 +72,8 @@ open class FinTsJobExecutor(
|
|||
*
|
||||
* Be aware this method resets BPD, UPD and selected TAN method!
|
||||
*/
|
||||
open suspend fun retrieveBasicDataLikeUsersTanMethods(context: JobContext): BankResponse {
|
||||
open suspend fun retrieveBasicDataLikeUsersTanMethods(context: JobContext, preferredTanMethods: List<TanMethodType>? = null, preferredTanMedium: String? = null,
|
||||
closeDialog: Boolean = false): BankResponse {
|
||||
val bank = context.bank
|
||||
|
||||
// just to ensure settings are in its initial state and that bank sends us bank parameter (BPD),
|
||||
|
@ -90,7 +89,7 @@ open class FinTsJobExecutor(
|
|||
bank.resetSelectedTanMethod()
|
||||
|
||||
// this is the only case where Einschritt-TAN-Verfahren is accepted: to get user's TAN methods
|
||||
context.startNewDialog(versionOfSecurityProcedure = VersionDesSicherheitsverfahrens.Version_1)
|
||||
context.startNewDialog(closeDialog, versionOfSecurityProcedure = VersionDesSicherheitsverfahrens.Version_1)
|
||||
|
||||
val message = messageBuilder.createInitDialogMessage(context)
|
||||
|
||||
|
@ -103,10 +102,12 @@ open class FinTsJobExecutor(
|
|||
if (bank.tanMethodsAvailableForUser.isEmpty()) { // could not retrieve supported tan methods for user
|
||||
return getTanMethodsResponse
|
||||
} else {
|
||||
getUsersTanMethod(context)
|
||||
getUsersTanMethod(context, preferredTanMethods)
|
||||
|
||||
if (bank.isTanMethodSelected && bank.tanMedia.isEmpty() && bank.tanMethodsAvailableForUser.any { it.nameOfTanMediumRequired } && isJobSupported(bank, CustomerSegmentId.TanMediaList)) { // tan media not retrieved yet
|
||||
getTanMediaList(context, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien, context.preferredTanMedium)
|
||||
if (bank.isTanMethodSelected == false) {
|
||||
return getTanMethodsResponse
|
||||
} else if (bank.tanMedia.isEmpty() && isJobSupported(bank, CustomerSegmentId.TanMediaList)) { // tan media not retrieved yet
|
||||
getTanMediaList(context, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien, preferredTanMedium)
|
||||
|
||||
return getTanMethodsResponse // TODO: judge if bank requires selecting TAN media and if though evaluate getTanMediaListResponse
|
||||
} else {
|
||||
|
@ -145,7 +146,6 @@ open class FinTsJobExecutor(
|
|||
return BankResponse(true, internalError = "Die TAN Verfahren der Bank konnten nicht ermittelt werden") // TODO: translate
|
||||
} else {
|
||||
bank.tanMethodsAvailableForUser = bank.tanMethodsSupportedByBank
|
||||
.filterNot { context.tanMethodsNotSupportedByApplication.contains(it.type) }
|
||||
|
||||
val didSelectTanMethod = getUsersTanMethod(context)
|
||||
|
||||
|
@ -202,29 +202,6 @@ open class FinTsJobExecutor(
|
|||
var balance: Money? = balanceResponse?.getFirstSegmentById<BalanceSegment>(InstituteSegmentId.Balance)?.let {
|
||||
Money(it.balance, it.currency)
|
||||
}
|
||||
|
||||
// TODO: for larger portfolios there can be a Aufsetzpunkt, but for balances we currently do not support sending multiple messages
|
||||
val statementOfHoldings = balanceResponse?.getFirstSegmentById<SecuritiesAccountBalanceSegment>(InstituteSegmentId.SecuritiesAccountBalance)?.let {
|
||||
val statementOfHoldings = it.statementOfHoldings
|
||||
val statementOfHolding = statementOfHoldings.firstOrNull { it.totalBalance != null }
|
||||
if (statementOfHolding != null) {
|
||||
balance = Money(statementOfHolding.totalBalance!!, statementOfHolding.currency ?: Currency.DefaultCurrencyCode)
|
||||
}
|
||||
statementOfHoldings
|
||||
} ?: emptyList()
|
||||
|
||||
if (parameter.account.supportsRetrievingAccountTransactions == false) {
|
||||
if (balanceResponse == null) {
|
||||
return GetAccountTransactionsResponse(context, BankResponse(false, "Balance could not be retrieved"), RetrievedAccountData.unsuccessful(parameter.account))
|
||||
} else {
|
||||
val successful = balance != null || balanceResponse.tanRequiredButWeWereToldToAbortIfSo
|
||||
val retrievedData = RetrievedAccountData(parameter.account, successful, balance, emptyList(), emptyList(), statementOfHoldings, Instant.nowExt(), null, null, balanceResponse?.internalError)
|
||||
|
||||
return GetAccountTransactionsResponse(context, balanceResponse, retrievedData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val bookedTransactions = mutableSetOf<AccountTransaction>()
|
||||
val unbookedTransactions = mutableSetOf<Any>()
|
||||
|
||||
|
@ -240,30 +217,27 @@ open class FinTsJobExecutor(
|
|||
context.bank, parameter.account)
|
||||
|
||||
bookedTransactions.addAll(chunkTransaction)
|
||||
remainingMt940String = remainder ?: ""
|
||||
remainingMt940String = remainder
|
||||
|
||||
parameter.retrievedChunkListener?.invoke(bookedTransactions)
|
||||
}
|
||||
|
||||
response.getFirstSegmentById<ReceivedCreditCardTransactionsAndBalance>(InstituteSegmentId.CreditCardTransactions)?.let { creditCardTransactionsSegment ->
|
||||
balance = Money(creditCardTransactionsSegment.balance.amount, creditCardTransactionsSegment.balance.currency ?: "EUR")
|
||||
bookedTransactions.addAll(creditCardTransactionsSegment.transactions.map { AccountTransaction(parameter.account, it.amount, it.description, it.bookingDate, it.valueDate, it.transactionDescriptionBase ?: "", null, null) })
|
||||
bookedTransactions.addAll(creditCardTransactionsSegment.transactions.map { AccountTransaction(parameter.account, it.amount, it.description, it.bookingDate, it.transactionDescriptionBase ?: "", null, null, "", it.valueDate) })
|
||||
}
|
||||
}
|
||||
|
||||
val startTime = Instant.nowExt()
|
||||
|
||||
val response = getAndHandleResponseForMessage(context, message)
|
||||
|
||||
closeDialog(context)
|
||||
|
||||
val successful = response.tanRequiredButWeWereToldToAbortIfSo
|
||||
|| (response.successful && (parameter.alsoRetrieveBalance == false || balance != null))
|
||||
|| (parameter.account.supportsRetrievingAccountTransactions == false && balance != null)
|
||||
|| (response.successful && (parameter.alsoRetrieveBalance == false || balance != null))
|
||||
val fromDate = parameter.fromDate
|
||||
?: parameter.account.serverTransactionsRetentionDays?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) }
|
||||
?: parameter.account.countDaysForWhichTransactionsAreKept?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) }
|
||||
?: bookedTransactions.minByOrNull { it.valueDate }?.valueDate
|
||||
val retrievedData = RetrievedAccountData(parameter.account, successful, balance, bookedTransactions, unbookedTransactions, statementOfHoldings, startTime, fromDate, parameter.toDate ?: LocalDate.todayAtEuropeBerlin(), response.internalError)
|
||||
val retrievedData = RetrievedAccountData(parameter.account, successful, balance, bookedTransactions, unbookedTransactions, fromDate, parameter.toDate ?: LocalDate.todayAtEuropeBerlin(), response.internalError)
|
||||
|
||||
return GetAccountTransactionsResponse(context, response, retrievedData,
|
||||
if (parameter.maxCountEntries != null) parameter.isSettingMaxCountEntriesAllowedByBank else null)
|
||||
|
@ -333,7 +307,6 @@ open class FinTsJobExecutor(
|
|||
|
||||
bank.selectedTanMedium = preferredTanMedium?.let { bank.tanMedia.firstOrNull { it.mediumName == preferredTanMedium } }
|
||||
?: bank.selectedTanMedium?.let { selected -> bank.tanMedia.firstOrNull { it.mediumName == selected.mediumName } } // try to find selectedTanMedium in new TanMedia instances
|
||||
?: bank.tanMedia.firstOrNull { it.status == TanMediumStatus.Aktiv && it.mediumName != null }
|
||||
?: bank.tanMedia.firstOrNull { it.mediumName != null }
|
||||
}
|
||||
|
||||
|
@ -341,7 +314,7 @@ open class FinTsJobExecutor(
|
|||
}
|
||||
|
||||
|
||||
open suspend fun changeTanMedium(context: JobContext, newActiveTanMedium: TanMedium): BankResponse {
|
||||
open suspend fun changeTanMedium(context: JobContext, newActiveTanMedium: TanGeneratorTanMedium): BankResponse {
|
||||
val bank = context.bank
|
||||
|
||||
if (bank.changeTanMediumParameters?.enteringAtcAndTanRequired == true) {
|
||||
|
@ -358,7 +331,7 @@ open class FinTsJobExecutor(
|
|||
}
|
||||
}
|
||||
|
||||
protected open suspend fun sendChangeTanMediumMessage(context: JobContext, newActiveTanMedium: TanMedium, enteredAtc: EnterTanGeneratorAtcResult?): BankResponse {
|
||||
protected open suspend fun sendChangeTanMediumMessage(context: JobContext, newActiveTanMedium: TanGeneratorTanMedium, enteredAtc: EnterTanGeneratorAtcResult?): BankResponse {
|
||||
|
||||
return sendMessageInNewDialogAndHandleResponse(context, null, true) {
|
||||
messageBuilder.createChangeTanMediumMessage(context, newActiveTanMedium, enteredAtc?.tan, enteredAtc?.atc)
|
||||
|
@ -401,36 +374,20 @@ open class FinTsJobExecutor(
|
|||
protected open suspend fun handleEnteringTanRequired(context: JobContext, tanResponse: TanResponse, response: BankResponse): BankResponse {
|
||||
// on all platforms run on Dispatchers.Main, but on iOS skip this (or wrap in withContext(Dispatchers.IO) )
|
||||
// val enteredTanResult = GlobalScope.async {
|
||||
val tanChallenge = createTanChallenge(tanResponse, modelMapper.mapToActionRequiringTan(context.type), context.bank, context.account)
|
||||
val tanChallenge = createTanChallenge(tanResponse, modelMapper.mapToActionRequiringTan(context.type), context.bank, context.account)
|
||||
|
||||
context.callback.enterTan(tanChallenge)
|
||||
context.callback.enterTan(tanChallenge)
|
||||
|
||||
mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse)
|
||||
while (tanChallenge.enterTanResult == null) {
|
||||
delay(250)
|
||||
|
||||
var invocationCount = 0 // TODO: remove again
|
||||
mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse)
|
||||
|
||||
while (tanChallenge.isEnteringTanDone == false) {
|
||||
delay(500)
|
||||
|
||||
if (++invocationCount % 10 == 0) {
|
||||
Log.info { "Waiting for TAN input invocation count: $invocationCount" }
|
||||
// TODO: add a timeout of e.g. 30 min
|
||||
}
|
||||
|
||||
val now = Instant.nowExt()
|
||||
if ((tanChallenge.tanExpirationTime != null && now > tanChallenge.tanExpirationTime) ||
|
||||
// most TANs a valid 5 - 15 minutes. So terminate wait process after that time
|
||||
(tanChallenge.tanExpirationTime == null && now > tanChallenge.challengeCreationTimestamp.plusMinutes(15))) {
|
||||
if (tanChallenge.isEnteringTanDone == false) {
|
||||
Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
||||
|
||||
tanChallenge.tanExpired()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
val enteredTanResult = tanChallenge.enterTanResult!!
|
||||
// }
|
||||
|
||||
return handleEnterTanResult(context, enteredTanResult, tanResponse, response)
|
||||
}
|
||||
|
@ -444,82 +401,27 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
return when (tanMethod.type) {
|
||||
TanMethodType.ChipTanFlickercode ->
|
||||
FlickerCodeTanChallenge(
|
||||
FlickerCodeDecoder().decodeChallenge(challenge, tanMethod.hhdVersion ?: getFallbackHhdVersion(challenge)),
|
||||
forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime)
|
||||
FlickerCodeDecoder().decodeChallenge(challenge, tanMethod.hhdVersion ?: HHDVersion.HHD_1_4), // HHD 1.4 is currently the most used version
|
||||
forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account)
|
||||
|
||||
TanMethodType.ChipTanQrCode, TanMethodType.ChipTanPhotoTanMatrixCode,
|
||||
TanMethodType.QrCode, TanMethodType.photoTan ->
|
||||
ImageTanChallenge(TanImageDecoder().decodeChallenge(challenge), forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime)
|
||||
ImageTanChallenge(TanImageDecoder().decodeChallenge(challenge), forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account)
|
||||
|
||||
else -> TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime)
|
||||
else -> TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun getFallbackHhdVersion(challenge: String): HHDVersion {
|
||||
if (challenge.length <= 35) { // is this true in all circumstances?
|
||||
return HHDVersion.HHD_1_3
|
||||
}
|
||||
|
||||
return HHDVersion.HHD_1_4 // HHD 1.4 is currently the most used version
|
||||
}
|
||||
|
||||
protected open suspend fun mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context: JobContext, tanChallenge: TanChallenge, tanResponse: TanResponse) {
|
||||
protected open fun mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context: JobContext, tanChallenge: TanChallenge, tanResponse: TanResponse) {
|
||||
context.bank.selectedTanMethod.decoupledParameters?.let { decoupledTanMethodParameters ->
|
||||
if (decoupledTanMethodParameters.periodicStateRequestsAllowed) {
|
||||
val responseAfterApprovingDecoupledTan =
|
||||
automaticallyRetrieveIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse, decoupledTanMethodParameters)
|
||||
|
||||
if (responseAfterApprovingDecoupledTan != null) {
|
||||
tanChallenge.userApprovedDecoupledTan(responseAfterApprovingDecoupledTan)
|
||||
} else {
|
||||
tanChallenge.userDidNotEnterTan()
|
||||
}
|
||||
if (tanResponse.tanProcess == TanProcess.AppTan && decoupledTanMethodParameters.periodicStateRequestsAllowed) {
|
||||
automaticallyRetrieveIfUserEnteredDecoupledTan(context, tanChallenge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open suspend fun automaticallyRetrieveIfUserEnteredDecoupledTan(context: JobContext, tanChallenge: TanChallenge, tanResponse: TanResponse, parameters: DecoupledTanMethodParameters): BankResponse? {
|
||||
protected open fun automaticallyRetrieveIfUserEnteredDecoupledTan(context: JobContext, tanChallenge: TanChallenge) {
|
||||
log.info { "automaticallyRetrieveIfUserEnteredDecoupledTan() called for $tanChallenge" }
|
||||
|
||||
delay(max(5, parameters.initialDelayInSecondsForStateRequest).seconds)
|
||||
|
||||
var iteration = 0
|
||||
val minWaitTime = when {
|
||||
parameters.maxNumberOfStateRequests <= 10 -> 30
|
||||
parameters.maxNumberOfStateRequests <= 24 -> 10
|
||||
else -> 3
|
||||
}
|
||||
val delayForNextStateRequest = max(minWaitTime, parameters.delayInSecondsForNextStateRequest).seconds
|
||||
|
||||
while (iteration < parameters.maxNumberOfStateRequests) {
|
||||
try {
|
||||
val message = messageBuilder.createDecoupledTanStatusMessage(context, tanResponse)
|
||||
|
||||
val response = getAndHandleResponseForMessage(context, message)
|
||||
|
||||
val tanFeedbacks = response.segmentFeedbacks.filter { it.referenceSegmentNumber == MessageBuilder.SignedMessagePayloadFirstSegmentNumber }
|
||||
if (tanFeedbacks.isNotEmpty()) {
|
||||
// new feedback code for Decoupled TAN: 0900 Sicherheitsfreigabe gültig
|
||||
// Sparkasse responds for pushTan with: HIRMS:4:2:3+0020::Der Auftrag wurde ausgeführt.+0020::Die gebuchten Umsätze wurden übermittelt.'
|
||||
val isTanApproved = tanFeedbacks.any { it.feedbacks.any { it.responseCode == 900 || it.responseCode == 20 } }
|
||||
if (isTanApproved) {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
iteration++
|
||||
// sometimes delayInSecondsForNextStateRequests is only 1 or 2 seconds, that's too fast i think
|
||||
delay(delayForNextStateRequest)
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not check status of Decoupled TAN" }
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
tanChallenge.tanExpired()
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
protected open suspend fun handleEnterTanResult(context: JobContext, enteredTanResult: EnterTanResult, tanResponse: TanResponse,
|
||||
|
@ -527,18 +429,19 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
|
||||
if (enteredTanResult.changeTanMethodTo != null) {
|
||||
return handleUserAsksToChangeTanMethodAndResendLastMessage(context, enteredTanResult.changeTanMethodTo)
|
||||
} else if (enteredTanResult.changeTanMediumTo != null) {
|
||||
}
|
||||
else if (enteredTanResult.changeTanMediumTo is TanGeneratorTanMedium) {
|
||||
return handleUserAsksToChangeTanMediumAndResendLastMessage(context, enteredTanResult.changeTanMediumTo,
|
||||
enteredTanResult.changeTanMediumResultCallback)
|
||||
} else if (enteredTanResult.userApprovedDecoupledTan == true && enteredTanResult.responseAfterApprovingDecoupledTan != null) {
|
||||
return enteredTanResult.responseAfterApprovingDecoupledTan
|
||||
} else if (enteredTanResult.enteredTan == null) {
|
||||
}
|
||||
else if (enteredTanResult.enteredTan == null) {
|
||||
// i tried to send a HKTAN with cancelJob = true but then i saw there are no tan methods that support cancellation (at least not at my bank)
|
||||
// but it's not required anyway, tan times out after some time. Simply don't respond anything and close dialog
|
||||
response.tanRequiredButUserDidNotEnterOne = true
|
||||
|
||||
return response
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return sendTanToBank(context, enteredTanResult.enteredTan, tanResponse)
|
||||
}
|
||||
}
|
||||
|
@ -562,7 +465,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
return resendMessageInNewDialog(context, lastCreatedMessage)
|
||||
}
|
||||
|
||||
protected open suspend fun handleUserAsksToChangeTanMediumAndResendLastMessage(context: JobContext, changeTanMediumTo: TanMedium,
|
||||
protected open suspend fun handleUserAsksToChangeTanMediumAndResendLastMessage(context: JobContext, changeTanMediumTo: TanGeneratorTanMedium,
|
||||
changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?): BankResponse {
|
||||
|
||||
val lastCreatedMessage = context.dialog.currentMessage
|
||||
|
@ -590,8 +493,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
|
||||
val initDialogResponse = initDialogWithStrongCustomerAuthentication(context)
|
||||
|
||||
// if lastCreatedMessage was a dialog init message, there's no need to send this message again, we just initialized a new dialog in initDialogWithStrongCustomerAuthentication()
|
||||
if (initDialogResponse.successful == false || lastCreatedMessage.isDialogInitMessage()) {
|
||||
if (initDialogResponse.successful == false) {
|
||||
return initDialogResponse
|
||||
} else {
|
||||
val newMessage = messageBuilder.rebuildMessage(context, lastCreatedMessage)
|
||||
|
@ -664,7 +566,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
|
||||
protected open suspend fun initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context: JobContext): BankResponse {
|
||||
|
||||
context.startNewDialog() // don't know if it's ok for all invocations of this method to set closeDialog to false (was actually only set in getAccounts())
|
||||
context.startNewDialog(false) // don't know if it's ok for all invocations of this method to set closeDialog to false (was actually only set in getAccounts())
|
||||
|
||||
val message = messageBuilder.createInitDialogMessage(context)
|
||||
|
||||
|
@ -740,7 +642,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
return BankResponse(true, noTanMethodSelected = noTanMethodSelected, internalError = errorMessage)
|
||||
}
|
||||
|
||||
open suspend fun getUsersTanMethod(context: JobContext): Boolean {
|
||||
open suspend fun getUsersTanMethod(context: JobContext, preferredTanMethods: List<TanMethodType>? = null): Boolean {
|
||||
val bank = context.bank
|
||||
|
||||
if (bank.tanMethodsAvailableForUser.size == 1) { // user has only one TAN method -> set it and we're done
|
||||
|
@ -748,13 +650,13 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
return true
|
||||
}
|
||||
else {
|
||||
tanMethodSelector.findPreferredTanMethod(bank.tanMethodsAvailableForUser, context.preferredTanMethods, context.tanMethodsNotSupportedByApplication)?.let {
|
||||
tanMethodSelector.findPreferredTanMethod(bank.tanMethodsAvailableForUser, preferredTanMethods)?.let {
|
||||
bank.selectedTanMethod = it
|
||||
return true
|
||||
}
|
||||
|
||||
// we know user's supported tan methods, now ask user which one to select
|
||||
val suggestedTanMethod = tanMethodSelector.getSuggestedTanMethod(bank.tanMethodsAvailableForUser, context.tanMethodsNotSupportedByApplication)
|
||||
val suggestedTanMethod = tanMethodSelector.getSuggestedTanMethod(bank.tanMethodsAvailableForUser)
|
||||
|
||||
val selectedTanMethod = context.callback.askUserForTanMethod(bank.tanMethodsAvailableForUser, suggestedTanMethod)
|
||||
|
||||
|
@ -775,14 +677,14 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
|
||||
protected open fun updateBankAndCustomerDataIfResponseSuccessful(context: JobContext, response: BankResponse) {
|
||||
if (response.successful) {
|
||||
updateBankAndCustomerData(context.bank, response, context)
|
||||
updateBankAndCustomerData(context.bank, response)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun updateBankAndCustomerData(bank: BankData, response: BankResponse, context: JobContext) {
|
||||
protected open fun updateBankAndCustomerData(bank: BankData, response: BankResponse) {
|
||||
updateBankData(bank, response)
|
||||
|
||||
modelMapper.updateCustomerData(bank, response, context)
|
||||
modelMapper.updateCustomerData(bank, response)
|
||||
}
|
||||
|
||||
|
|
@ -1,18 +1,17 @@
|
|||
package net.codinux.banking.fints
|
||||
package net.dankito.banking.fints
|
||||
|
||||
import net.codinux.log.logger
|
||||
import net.codinux.banking.fints.messages.MessageBuilder
|
||||
import net.codinux.banking.fints.messages.MessageBuilderResult
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.codinux.banking.fints.response.BankResponse
|
||||
import net.codinux.banking.fints.response.segments.TanResponse
|
||||
import net.codinux.banking.fints.util.IBase64Service
|
||||
import net.codinux.banking.fints.util.PureKotlinBase64Service
|
||||
import net.codinux.banking.fints.webclient.IWebClient
|
||||
import net.codinux.banking.fints.webclient.KtorWebClient
|
||||
import net.codinux.banking.fints.webclient.WebClientResponse
|
||||
import net.codinux.banking.fints.extensions.getAllExceptionMessagesJoined
|
||||
import net.codinux.banking.fints.response.segments.ReceivedSegment
|
||||
import net.dankito.banking.fints.messages.MessageBuilder
|
||||
import net.dankito.banking.fints.messages.MessageBuilderResult
|
||||
import net.dankito.banking.fints.model.*
|
||||
import net.dankito.banking.fints.response.BankResponse
|
||||
import net.dankito.banking.fints.response.segments.TanResponse
|
||||
import net.dankito.banking.fints.util.IBase64Service
|
||||
import net.dankito.banking.fints.util.PureKotlinBase64Service
|
||||
import net.dankito.banking.fints.webclient.IWebClient
|
||||
import net.dankito.banking.fints.webclient.KtorWebClient
|
||||
import net.dankito.banking.fints.webclient.WebClientResponse
|
||||
import net.dankito.banking.fints.extensions.getAllExceptionMessagesJoined
|
||||
|
||||
|
||||
open class RequestExecutor(
|
||||
|
@ -105,11 +104,9 @@ open class RequestExecutor(
|
|||
try {
|
||||
val decodedResponse = decodeBase64Response(responseBody)
|
||||
|
||||
val parsedResponse = context.responseParser.parse(decodedResponse)
|
||||
addMessageLog(context, MessageLogEntryType.Received, decodedResponse)
|
||||
|
||||
addMessageLog(context, MessageLogEntryType.Received, decodedResponse, parsedResponse.receivedSegments)
|
||||
|
||||
return parsedResponse
|
||||
return context.responseParser.parse(decodedResponse)
|
||||
} catch (e: Exception) {
|
||||
logError(context, "Could not decode responseBody:\r\n'$responseBody'", e)
|
||||
|
||||
|
@ -167,8 +164,8 @@ open class RequestExecutor(
|
|||
}
|
||||
|
||||
|
||||
protected open fun addMessageLog(context: JobContext, type: MessageLogEntryType, message: String, parsedSegments: List<ReceivedSegment> = emptyList()) {
|
||||
context.addMessageLog(type, message, parsedSegments)
|
||||
protected open fun addMessageLog(context: JobContext, type: MessageLogEntryType, message: String) {
|
||||
context.addMessageLog(type, message)
|
||||
}
|
||||
|
||||
protected open fun logError(context: JobContext, message: String, e: Exception?) {
|
|
@ -1,7 +1,7 @@
|
|||
package net.codinux.banking.fints.callback
|
||||
package net.dankito.banking.fints.callback
|
||||
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||
import net.dankito.banking.fints.model.*
|
||||
|
||||
|
||||
interface FinTsClientCallback {
|
||||
|
@ -25,13 +25,6 @@ interface FinTsClientCallback {
|
|||
*
|
||||
* If you do not support entering TAN generator ATC, return [EnterTanGeneratorAtcResult.userDidNotEnterAtc]
|
||||
*/
|
||||
suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanMedium): EnterTanGeneratorAtcResult
|
||||
|
||||
/**
|
||||
* Gets fired when a FinTS message get sent to bank server, a FinTS message is received from bank server or an error occurred.
|
||||
*
|
||||
* Be aware, in order that this message gets fired [net.codinux.banking.fints.config.FinTsClientOptions.fireCallbackOnMessageLogs] has to be set to true.
|
||||
*/
|
||||
fun messageLogAdded(messageLogEntry: MessageLogEntry)
|
||||
suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package net.codinux.banking.fints.callback
|
||||
package net.dankito.banking.fints.callback
|
||||
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||
import net.dankito.banking.fints.model.*
|
||||
|
||||
|
||||
open class NoOpFinTsClientCallback : FinTsClientCallback {
|
||||
|
@ -14,12 +14,8 @@ open class NoOpFinTsClientCallback : FinTsClientCallback {
|
|||
return tanChallenge.userDidNotEnterTan()
|
||||
}
|
||||
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanMedium): EnterTanGeneratorAtcResult {
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
|
||||
return EnterTanGeneratorAtcResult.userDidNotEnterAtc()
|
||||
}
|
||||
|
||||
override fun messageLogAdded(messageLogEntry: MessageLogEntry) {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,19 +1,18 @@
|
|||
package net.codinux.banking.fints.callback
|
||||
package net.dankito.banking.fints.callback
|
||||
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||
import net.dankito.banking.fints.model.*
|
||||
|
||||
|
||||
open class SimpleFinTsClientCallback(
|
||||
protected open val askUserForTanMethod: ((supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?) -> TanMethod?)? = null,
|
||||
protected open val messageLogAdded: ((MessageLogEntry) -> Unit)? = null,
|
||||
protected open val enterTanGeneratorAtc: ((bank: BankData, tanMedium: TanMedium) -> EnterTanGeneratorAtcResult)? = null,
|
||||
protected open val enterTan: ((tanChallenge: TanChallenge) -> Unit)? = null
|
||||
protected open val enterTan: ((tanChallenge: TanChallenge) -> Unit)? = null,
|
||||
protected open val enterTanGeneratorAtc: ((bank: BankData, tanMedium: TanGeneratorTanMedium) -> EnterTanGeneratorAtcResult)? = null,
|
||||
protected open val askUserForTanMethod: ((supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?) -> TanMethod?)? = null
|
||||
) : FinTsClientCallback {
|
||||
|
||||
constructor() : this(null as ((tanChallenge: TanChallenge) -> Unit)?) // Swift does not support default parameter values -> create constructor overloads
|
||||
constructor() : this(null) // Swift does not support default parameter values -> create constructor overloads
|
||||
|
||||
constructor(enterTan: ((tanChallenge: TanChallenge) -> Unit)?) : this(null, null, null, enterTan)
|
||||
constructor(enterTan: ((tanChallenge: TanChallenge) -> Unit)?) : this(enterTan, null)
|
||||
|
||||
|
||||
override suspend fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?): TanMethod? {
|
||||
|
@ -25,12 +24,8 @@ open class SimpleFinTsClientCallback(
|
|||
enterTan?.invoke(tanChallenge) ?: run { tanChallenge.userDidNotEnterTan() }
|
||||
}
|
||||
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanMedium): EnterTanGeneratorAtcResult {
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
|
||||
return enterTanGeneratorAtc?.invoke(bank, tanMedium) ?: EnterTanGeneratorAtcResult.userDidNotEnterAtc()
|
||||
}
|
||||
|
||||
override fun messageLogAdded(messageLogEntry: MessageLogEntry) {
|
||||
messageLogAdded?.invoke(messageLogEntry)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
package net.codinux.banking.fints.config
|
||||
package net.dankito.banking.fints.config
|
||||
|
||||
import net.codinux.banking.fints.FinTsJobExecutor
|
||||
import net.codinux.banking.fints.RequestExecutor
|
||||
import net.codinux.banking.fints.messages.MessageBuilder
|
||||
import net.codinux.banking.fints.model.mapper.ModelMapper
|
||||
import net.codinux.banking.fints.util.FinTsServerAddressFinder
|
||||
import net.codinux.banking.fints.util.IBase64Service
|
||||
import net.codinux.banking.fints.util.PureKotlinBase64Service
|
||||
import net.codinux.banking.fints.util.TanMethodSelector
|
||||
import net.codinux.banking.fints.webclient.IWebClient
|
||||
import net.codinux.banking.fints.webclient.KtorWebClient
|
||||
import net.dankito.banking.fints.FinTsJobExecutor
|
||||
import net.dankito.banking.fints.RequestExecutor
|
||||
import net.dankito.banking.fints.messages.MessageBuilder
|
||||
import net.dankito.banking.fints.model.mapper.ModelMapper
|
||||
import net.dankito.banking.fints.util.FinTsServerAddressFinder
|
||||
import net.dankito.banking.fints.util.IBase64Service
|
||||
import net.dankito.banking.fints.util.PureKotlinBase64Service
|
||||
import net.dankito.banking.fints.util.TanMethodSelector
|
||||
import net.dankito.banking.fints.webclient.IWebClient
|
||||
import net.dankito.banking.fints.webclient.KtorWebClient
|
||||
|
||||
class FinTsClientConfiguration(
|
||||
var options: FinTsClientOptions = FinTsClientOptions(),
|
|
@ -0,0 +1,13 @@
|
|||
package net.dankito.banking.fints.config
|
||||
|
||||
import net.dankito.banking.fints.model.ProductData
|
||||
|
||||
data class FinTsClientOptions(
|
||||
val removeSensitiveDataFromMessageLog: Boolean = true,
|
||||
val version: String = "1.0.0", // TODO: get version dynamically
|
||||
val productName: String = "15E53C26816138699C7B6A3E8"
|
||||
) {
|
||||
|
||||
val product: ProductData by lazy { ProductData(productName, version) }
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package net.codinux.banking.fints.extensions
|
||||
package net.dankito.banking.fints.extensions
|
||||
|
||||
import kotlinx.datetime.*
|
||||
import kotlin.js.JsName
|
||||
|
@ -12,7 +12,7 @@ fun LocalDate.Companion.todayAtSystemDefaultTimeZone(): LocalDate {
|
|||
}
|
||||
|
||||
fun LocalDate.Companion.todayAtEuropeBerlin(): LocalDate {
|
||||
return nowAt(TimeZone.EuropeBerlin)
|
||||
return nowAt(TimeZone.europeBerlin)
|
||||
}
|
||||
|
||||
@JsName("nowAtForDate")
|
|
@ -1,4 +1,4 @@
|
|||
package net.codinux.banking.fints.extensions
|
||||
package net.dankito.banking.fints.extensions
|
||||
|
||||
import kotlinx.datetime.*
|
||||
|
||||
|
@ -9,9 +9,9 @@ fun LocalDateTime.Companion.nowAtUtc(): LocalDateTime {
|
|||
}
|
||||
|
||||
fun LocalDateTime.Companion.nowAtEuropeBerlin(): LocalDateTime {
|
||||
return nowAt(TimeZone.EuropeBerlin)
|
||||
return nowAt(TimeZone.europeBerlin)
|
||||
}
|
||||
|
||||
fun LocalDateTime.Companion.nowAt(timeZone: TimeZone): LocalDateTime {
|
||||
return Instant.nowExt().toLocalDateTime(timeZone)
|
||||
return Clock.System.now().toLocalDateTime(timeZone)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package net.codinux.banking.fints.extensions
|
||||
package net.dankito.banking.fints.extensions
|
||||
|
||||
|
||||
fun Int.toStringWithMinDigits(minimumCountDigits: Int, fillerString: Char = '0'): String {
|
|
@ -0,0 +1,11 @@
|
|||
package net.dankito.banking.fints.extensions
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
fun randomWithSeed(): Random = Random(randomSeed())
|
||||
|
||||
fun randomSeed(): Long {
|
||||
return Clock.System.now().nanosecondsOfSecond.toLong() + Clock.System.now().toEpochMilliseconds()
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package net.codinux.banking.fints.extensions
|
||||
package net.dankito.banking.fints.extensions
|
||||
|
||||
|
||||
/**
|
|
@ -1,4 +1,4 @@
|
|||
package net.codinux.banking.fints.extensions
|
||||
package net.dankito.banking.fints.extensions
|
||||
|
||||
|
||||
fun Throwable.getAllExceptionMessagesJoined(maxDepth: Int = 5): String {
|
|
@ -0,0 +1,7 @@
|
|||
package net.dankito.banking.fints.extensions
|
||||
|
||||
import kotlinx.datetime.TimeZone
|
||||
|
||||
|
||||
val TimeZone.Companion.europeBerlin: TimeZone
|
||||
get() = TimeZone.of("Europe/Berlin")
|
|
@ -0,0 +1,10 @@
|
|||
package net.dankito.banking.fints.log
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
interface IMessageLogAppender {
|
||||
|
||||
fun logError(loggingClass: KClass<*>, message: String, e: Exception? = null)
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package net.dankito.banking.fints.log
|
||||
|
||||
import net.dankito.banking.fints.model.AccountData
|
||||
import net.dankito.banking.fints.model.BankData
|
||||
import net.dankito.banking.fints.model.MessageType
|
||||
import net.dankito.banking.fints.model.JobContextType
|
||||
|
||||
|
||||
class MessageContext(
|
||||
val jobType: JobContextType,
|
||||
val dialogType: MessageType,
|
||||
val jobNumber: Int,
|
||||
val dialogNumber: Int,
|
||||
val messageNumber: Int,
|
||||
val bank: BankData,
|
||||
val account: AccountData?
|
||||
)
|
|
@ -1,22 +1,19 @@
|
|||
package net.codinux.banking.fints.log
|
||||
package net.dankito.banking.fints.log
|
||||
|
||||
import net.codinux.log.LoggerFactory
|
||||
import net.codinux.log.logger
|
||||
import net.codinux.banking.fints.callback.FinTsClientCallback
|
||||
import net.codinux.banking.fints.config.FinTsClientOptions
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.MessageLogEntry
|
||||
import net.codinux.banking.fints.model.MessageLogEntryType
|
||||
import net.codinux.banking.fints.extensions.getInnerException
|
||||
import net.codinux.banking.fints.extensions.nthIndexOf
|
||||
import net.codinux.banking.fints.extensions.toStringWithMinDigits
|
||||
import net.codinux.banking.fints.response.segments.ReceivedSegment
|
||||
import net.codinux.banking.fints.util.FinTsUtils
|
||||
import net.dankito.banking.fints.config.FinTsClientOptions
|
||||
import net.dankito.banking.fints.model.BankData
|
||||
import net.dankito.banking.fints.model.MessageLogEntry
|
||||
import net.dankito.banking.fints.model.MessageLogEntryType
|
||||
import net.dankito.banking.fints.extensions.getInnerException
|
||||
import net.dankito.banking.fints.extensions.nthIndexOf
|
||||
import net.dankito.banking.fints.extensions.toStringWithMinDigits
|
||||
import net.dankito.banking.fints.util.FinTsUtils
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
open class MessageLogCollector(
|
||||
private val callback: FinTsClientCallback,
|
||||
private val options: FinTsClientOptions = FinTsClientOptions(),
|
||||
private val finTsUtils: FinTsUtils = FinTsUtils()
|
||||
) {
|
||||
|
@ -37,57 +34,43 @@ open class MessageLogCollector(
|
|||
|
||||
// in either case remove sensitive data after response is parsed as otherwise some information like account holder name and accounts may is not set yet on BankData
|
||||
open val messageLog: List<MessageLogEntry>
|
||||
// safe CPU cycles by only removing sensitive data if messageLog is really requested
|
||||
get() = _messageLog.map {
|
||||
val message = createMessageForLog(it)
|
||||
val messageWithoutSensitiveData = if (options.removeSensitiveDataFromMessageLog) {
|
||||
safelyRemoveSensitiveDataFromMessage(message, it.context.bank)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
// safe CPU cycles by only formatting and removing sensitive data if messageLog is really requested
|
||||
get() = _messageLog.map { MessageLogEntry(it.type, it.context, it.messageTrace, createMessageForLog(it), it.error, it.time) }
|
||||
|
||||
MessageLogEntry(it.type, it.context, it.messageTrace, message, messageWithoutSensitiveData, it.error, it.parsedSegments, it.time)
|
||||
}
|
||||
|
||||
private fun createMessageForLog(logEntry: MessageLogEntry): String =
|
||||
if (logEntry.type == MessageLogEntryType.Error) {
|
||||
logEntry.message + (if (logEntry.error != null) NewLine + getStackTrace(logEntry.error!!) else "")
|
||||
private fun createMessageForLog(logEntry: MessageLogEntry): String {
|
||||
val message = if (logEntry.type == MessageLogEntryType.Error) {
|
||||
logEntry.messageTrace + logEntry.message + (if (logEntry.error != null) NewLine + getStackTrace(logEntry.error!!) else "")
|
||||
} else {
|
||||
logEntry.message
|
||||
logEntry.messageTrace + "\n" + prettyPrintFinTsMessage(logEntry.message)
|
||||
}
|
||||
|
||||
|
||||
open fun addMessageLog(type: MessageLogEntryType, message: String, context: MessageContext, parsedSegments: List<ReceivedSegment> = emptyList()) {
|
||||
val messageTrace = createMessageTraceString(type, context)
|
||||
val prettyPrintMessage = prettyPrintMessageIfRequired(message)
|
||||
|
||||
log.debug { "$messageTrace\n$prettyPrintMessage" }
|
||||
|
||||
addMessageLogEntry(type, context, messageTrace, prettyPrintMessage, null, parsedSegments)
|
||||
return if (options.removeSensitiveDataFromMessageLog) {
|
||||
safelyRemoveSensitiveDataFromMessage(message, logEntry.context.bank)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
open fun logError(loggingClass: KClass<*>, message: String, context: MessageContext, e: Throwable? = null) {
|
||||
|
||||
open fun addMessageLog(type: MessageLogEntryType, message: String, context: MessageContext) {
|
||||
val messageTrace = createMessageTraceString(type, context)
|
||||
|
||||
addMessageLogEntry(type, context, messageTrace, message)
|
||||
|
||||
log.debug { "$messageTrace\n${prettyPrintFinTsMessage(message)}" }
|
||||
}
|
||||
|
||||
open fun logError(loggingClass: KClass<*>, message: String, context: MessageContext, e: Exception? = null) {
|
||||
val type = MessageLogEntryType.Error
|
||||
val messageTrace = createMessageTraceString(type, context)
|
||||
val prettyPrintMessage = prettyPrintFinTsMessage(message) // error messages almost always get logged / displayed -> pretty print
|
||||
|
||||
LoggerFactory.getLogger(loggingClass).error(e) { "$messageTrace\n$prettyPrintMessage" }
|
||||
LoggerFactory.getLogger(loggingClass).error(e) { messageTrace + messageTrace }
|
||||
|
||||
addMessageLogEntry(type, context, messageTrace, prettyPrintMessage, e)
|
||||
addMessageLogEntry(type, context, messageTrace, message, e)
|
||||
}
|
||||
|
||||
protected open fun addMessageLogEntry(type: MessageLogEntryType, context: MessageContext, messageTrace: String, message: String, error: Throwable? = null, parsedSegments: List<ReceivedSegment> = emptyList()) {
|
||||
if (options.collectMessageLog || options.fireCallbackOnMessageLogs) {
|
||||
val newEntry = MessageLogEntry(type, context, messageTrace, message, null, error, parsedSegments)
|
||||
|
||||
if (options.collectMessageLog) {
|
||||
_messageLog.add(newEntry)
|
||||
}
|
||||
|
||||
if (options.fireCallbackOnMessageLogs) {
|
||||
callback.messageLogAdded(newEntry)
|
||||
}
|
||||
}
|
||||
protected open fun addMessageLogEntry(type: MessageLogEntryType, context: MessageContext, messageTrace: String, message: String, error: Throwable? = null) {
|
||||
_messageLog.add(MessageLogEntry(type, context, messageTrace, message, error))
|
||||
}
|
||||
|
||||
|
||||
|
@ -95,7 +78,7 @@ open class MessageLogCollector(
|
|||
return "${twoDigits(context.jobNumber)}_${twoDigits(context.dialogNumber)}_${twoDigits(context.messageNumber)}_" +
|
||||
"${context.bank.bankCode}_${context.bank.customerId}" +
|
||||
"${ context.account?.let { "_${it.accountIdentifier}" } ?: "" }_" +
|
||||
"${context.jobType.name}_${context.messageType.name} " +
|
||||
"${context.jobType.name}_${context.dialogType.name} " +
|
||||
"${getMessageTypeString(type)}:"
|
||||
}
|
||||
|
||||
|
@ -105,19 +88,12 @@ open class MessageLogCollector(
|
|||
|
||||
protected open fun getMessageTypeString(type: MessageLogEntryType): String {
|
||||
return when (type) {
|
||||
MessageLogEntryType.Sent -> "01 Sending message"
|
||||
MessageLogEntryType.Received -> "02 Received message"
|
||||
MessageLogEntryType.Error -> "03 Error"
|
||||
MessageLogEntryType.Sent -> "Sending message"
|
||||
MessageLogEntryType.Received -> "Received message"
|
||||
MessageLogEntryType.Error -> "Error"
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun prettyPrintMessageIfRequired(message: String): String =
|
||||
if (options.collectMessageLog || options.fireCallbackOnMessageLogs || log.isDebugEnabled) { // only use CPU cycles if message will ever be used / displayed
|
||||
prettyPrintFinTsMessage(message)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
|
||||
protected open fun prettyPrintFinTsMessage(message: String): String =
|
||||
finTsUtils.prettyPrintFinTsMessage(message)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package net.dankito.banking.fints.mapper
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Be aware that Java DateFormat is not thread safe!
|
||||
*/
|
||||
expect class DateFormatter constructor(pattern: String) {
|
||||
|
||||
fun parseDate(dateString: String): LocalDate?
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package net.codinux.banking.fints.mapper
|
||||
package net.dankito.banking.fints.mapper
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.dankito.banking.client.model.*
|
||||
|
@ -7,14 +7,14 @@ import net.dankito.banking.client.model.parameter.FinTsClientParameter
|
|||
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.codinux.banking.fints.messages.datenelemente.abgeleiteteformate.Laenderkennzeichen
|
||||
import net.codinux.banking.fints.model.*
|
||||
import net.codinux.banking.fints.response.client.FinTsClientResponse
|
||||
import net.codinux.banking.fints.response.client.GetAccountTransactionsResponse
|
||||
import net.codinux.banking.fints.response.segments.AccountType
|
||||
import net.codinux.banking.fints.util.BicFinder
|
||||
import net.codinux.banking.fints.extensions.minusDays
|
||||
import net.codinux.banking.fints.extensions.todayAtEuropeBerlin
|
||||
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
|
||||
import net.dankito.banking.fints.util.BicFinder
|
||||
import net.dankito.banking.fints.extensions.minusDays
|
||||
import net.dankito.banking.fints.extensions.todayAtEuropeBerlin
|
||||
|
||||
|
||||
open class FinTsModelMapper {
|
||||
|
@ -22,11 +22,8 @@ open class FinTsModelMapper {
|
|||
protected open val bicFinder = BicFinder()
|
||||
|
||||
|
||||
open fun mapToBankData(param: FinTsClientParameter, finTsServerAddress: String, defaultValues: BankData? = null): BankData {
|
||||
return BankData(
|
||||
param.bankCode, param.loginName, param.password, finTsServerAddress,
|
||||
defaultValues?.bic ?: bicFinder.findBic(param.bankCode) ?: "", defaultValues?.bankName ?: ""
|
||||
)
|
||||
open fun mapToBankData(param: FinTsClientParameter, finTsServerAddress: String): BankData {
|
||||
return BankData(param.bankCode, param.loginName, param.password, finTsServerAddress, bicFinder.findBic(param.bankCode) ?: "")
|
||||
}
|
||||
|
||||
open fun mapToAccountData(credentials: BankAccountIdentifier, param: FinTsClientParameter): AccountData {
|
||||
|
@ -54,7 +51,7 @@ open class FinTsModelMapper {
|
|||
|
||||
open fun map(account: AccountData): BankAccount {
|
||||
return BankAccount(account.accountIdentifier, account.subAccountAttribute, account.iban, account.accountHolderName, map(account.accountType), account.productName,
|
||||
account.currency ?: Currency.DefaultCurrencyCode, account.accountLimit, account.serverTransactionsRetentionDays, account.isAccountTypeSupportedByApplication,
|
||||
account.currency ?: Currency.DefaultCurrencyCode, account.accountLimit, account.countDaysForWhichTransactionsAreKept, account.isAccountTypeSupportedByApplication,
|
||||
account.supportsRetrievingAccountTransactions, account.supportsRetrievingBalance, account.supportsTransferringMoney, account.supportsRealTimeTransfer)
|
||||
}
|
||||
|
||||
|
@ -73,34 +70,16 @@ open class FinTsModelMapper {
|
|||
}
|
||||
}
|
||||
|
||||
open fun map(bank: BankData, retrievedTransactionsResponses: List<GetAccountTransactionsResponse>, retrieveTransactionsTo: LocalDate? = null): CustomerAccount {
|
||||
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 ->
|
||||
accountTransactionsResponse.balance?.let { balance ->
|
||||
bankAccount.balance = balance
|
||||
}
|
||||
|
||||
if (accountTransactionsResponse.retrievedTransactionsFrom != null && (bankAccount.retrievedTransactionsFrom == null ||
|
||||
accountTransactionsResponse.retrievedTransactionsFrom!! < bankAccount.retrievedTransactionsFrom!!)) {
|
||||
bankAccount.retrievedTransactionsFrom = accountTransactionsResponse.retrievedTransactionsFrom
|
||||
}
|
||||
|
||||
val retrievalTime = accountTransactionsResponse.retrievalTime
|
||||
if (retrieveTransactionsTo == null && (bankAccount.lastAccountUpdateTime == null || bankAccount.lastAccountUpdateTime!! <= retrievalTime || // if retrieveTransactionsTo is set, then we don't retrieve all current transactions -> don't set lastAccountUpdateTime
|
||||
(bankAccount.supportsRetrievingTransactions == false && accountTransactionsResponse.statementOfHoldings.isNotEmpty()))) { // TODO: really check for supportsRetrievingTransactions == false if statementOfHoldings are set? Are there really accounts that support HKWPD and HKKAZ?
|
||||
bankAccount.lastAccountUpdateTime = retrievalTime
|
||||
}
|
||||
|
||||
if (accountTransactionsResponse.bookedTransactions.isNotEmpty()) {
|
||||
bankAccount.bookedTransactions = bankAccount.bookedTransactions.toMutableList().apply {
|
||||
addAll(map(accountTransactionsResponse))
|
||||
}
|
||||
}
|
||||
|
||||
bankAccount.statementOfHoldings = accountTransactionsResponse.statementOfHoldings
|
||||
bankAccount.balance = accountTransactionsResponse.balance ?: Money.Zero
|
||||
bankAccount.retrievedTransactionsFrom = accountTransactionsResponse.retrievedTransactionsFrom
|
||||
bankAccount.retrievedTransactionsTo = accountTransactionsResponse.retrievedTransactionsTo
|
||||
bankAccount.bookedTransactions = map(accountTransactionsResponse)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,29 +90,15 @@ open class FinTsModelMapper {
|
|||
return data.bookedTransactions.map { map(it) }
|
||||
}
|
||||
|
||||
open fun map(transaction: net.codinux.banking.fints.model.AccountTransaction): AccountTransaction {
|
||||
return AccountTransaction(
|
||||
transaction.amount, transaction.reference,
|
||||
transaction.bookingDate, transaction.valueDate,
|
||||
transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId,
|
||||
|
||||
transaction.postingText,
|
||||
|
||||
transaction.openingBalance, transaction.closingBalance,
|
||||
|
||||
transaction.statementNumber, transaction.sheetNumber,
|
||||
|
||||
transaction.customerReference, transaction.bankReference, transaction.furtherInformation,
|
||||
|
||||
transaction.endToEndReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
|
||||
transaction.compensationAmount, transaction.originalAmount, transaction.deviantOriginator, transaction.deviantRecipient,
|
||||
transaction.referenceWithNoSpecialType,
|
||||
|
||||
transaction.journalNumber, transaction.textKeyAddition,
|
||||
transaction.orderReferenceNumber, transaction.referenceNumber,
|
||||
|
||||
transaction.isReversal
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
@ -192,10 +157,6 @@ open class FinTsModelMapper {
|
|||
else errorMessages.joinToString("\r\n")
|
||||
}
|
||||
|
||||
open fun mergeMessageLog(vararg messageLogs: List<MessageLogEntry>?): List<MessageLogEntry> {
|
||||
return messageLogs.filterNotNull().flatten()
|
||||
}
|
||||
|
||||
open fun mergeMessageLog(vararg responses: FinTsClientResponse?): List<MessageLogEntry> {
|
||||
return responses.filterNotNull().flatMap { it.messageLog }
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package net.codinux.banking.fints.messages
|
||||
package net.dankito.banking.fints.messages
|
||||
|
||||
|
||||
enum class Existenzstatus {
|
|
@ -1,4 +1,4 @@
|
|||
package net.codinux.banking.fints.messages
|
||||
package net.dankito.banking.fints.messages
|
||||
|
||||
import io.ktor.utils.io.charsets.Charsets
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue