Compare commits

..

No commits in common. "main" and "v1.0.0-Alpha-11" have entirely different histories.

419 changed files with 3002 additions and 6414 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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",
]
}

View file

@ -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) | payers/debtors reference party (for credit transfer / payees / creditors reference party (for a direct debit) |
| Abweichender Zahlungsempfänger (CT-AT28) / Abweichender Zahlungspflichtiger (DDAT15) | payees/creditors reference party / payers/debtors reference party |
| | |
| Überweisender | Payer, debtor |
| Zahlungsempfänger | Payee, creditor |
| Zahlungseingang | Payment receipt |
| Lastschrift | direct debit |
| | |
| | |
| Primanoten-Nr. | Journal no. |
| | |

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +0,0 @@
package net.codinux.banking.fints.extensions
import kotlinx.datetime.TimeZone
val TimeZone.Companion.EuropeBerlin: TimeZone
get() = TimeZone.of("Europe/Berlin")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 ?: ""
}
}

View file

@ -1,8 +0,0 @@
package net.codinux.banking.fints.messages.segmente.id
interface ISegmentId {
val id: String
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package net.codinux.banking.fints.extensions
package net.dankito.banking.fints.extensions
/**

View file

@ -1,4 +1,4 @@
package net.codinux.banking.fints.extensions
package net.dankito.banking.fints.extensions
fun Throwable.getAllExceptionMessagesJoined(maxDepth: Int = 5): String {

View file

@ -0,0 +1,7 @@
package net.dankito.banking.fints.extensions
import kotlinx.datetime.TimeZone
val TimeZone.Companion.europeBerlin: TimeZone
get() = TimeZone.of("Europe/Berlin")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package net.codinux.banking.fints.messages
package net.dankito.banking.fints.messages
enum class Existenzstatus {

View file

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