diff --git a/fints4k/build.gradle b/fints4k/build.gradle index 57890872..d17580a2 100644 --- a/fints4k/build.gradle +++ b/fints4k/build.gradle @@ -72,6 +72,7 @@ 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") } diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/FinTsClient.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/FinTsClient.kt index 19588611..901e347d 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/FinTsClient.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/FinTsClient.kt @@ -19,6 +19,8 @@ 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 open class FinTsClient( @@ -57,7 +59,7 @@ open class FinTsClient( if (basicAccountDataResponse.successful == false || param.retrieveOnlyAccountInfo || basicAccountDataResponse.finTsModel == null) { return GetAccountDataResponse(basicAccountDataResponse.error, basicAccountDataResponse.errorMessage, null, - basicAccountDataResponse.messageLogWithoutSensitiveData, basicAccountDataResponse.finTsModel) + basicAccountDataResponse.messageLogWithoutSensitiveData, basicAccountDataResponse.finTsModel, basicAccountDataResponse.serializedFinTsModel) } else { val bank = basicAccountDataResponse.finTsModel!! return getAccountData(param, bank, bank.accounts, basicAccountDataResponse.messageLogWithoutSensitiveData) @@ -71,7 +73,7 @@ open class FinTsClient( 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), previousJobMessageLog ?: listOf(), bank, serialize(bank)) } for (account in accountsSupportingRetrievingTransactions) { @@ -87,7 +89,7 @@ open class FinTsClient( 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) + mapper.mergeMessageLog(previousJobMessageLog, *retrievedTransactionsResponses.map { it.messageLog }.toTypedArray()), bank, serialize(bank)) } protected open suspend fun getAccountTransactions(param: GetAccountDataParameter, bank: BankData, account: AccountData): GetAccountTransactionsResponse { @@ -122,7 +124,7 @@ open class FinTsClient( if (getAccountInfoResponse.successful == false) { return TransferMoneyResponse(mapper.mapErrorCode(getAccountInfoResponse), mapper.mapErrorMessages(getAccountInfoResponse), - getAccountInfoResponse.messageLog, bank) + getAccountInfoResponse.messageLog, bank, serialize(bank)) } else { return transferMoneyAsync(param, recipientBankIdentifier, getAccountInfoResponse.bank, getAccountInfoResponse.bank.accounts, getAccountInfoResponse) } @@ -136,14 +138,14 @@ open class FinTsClient( val accountToUse: AccountData if (accountsSupportingTransfer.isEmpty()) { - return TransferMoneyResponse(ErrorCode.NoAccountSupportsMoneyTransfer, "None of the accounts $accounts supports money transfer", previousJobResponse?.messageLog ?: listOf(), bank) + return TransferMoneyResponse(ErrorCode.NoAccountSupportsMoneyTransfer, "None of the accounts $accounts supports money transfer", previousJobResponse?.messageLog ?: listOf(), bank, serialize(bank)) } else if (accountsSupportingTransfer.size == 1) { accountToUse = accountsSupportingTransfer.first() } else { val selectedAccount = param.selectAccountToUseForTransfer?.invoke(accountsSupportingTransfer) if (selectedAccount == null) { - return TransferMoneyResponse(ErrorCode.MoreThanOneAccountSupportsMoneyTransfer, "More than one of the accounts $accountsSupportingTransfer supports money transfer, so we cannot clearly determine which one to use for this transfer", previousJobResponse?.messageLog ?: listOf(), bank) + return TransferMoneyResponse(ErrorCode.MoreThanOneAccountSupportsMoneyTransfer, "More than one of the accounts $accountsSupportingTransfer supports money transfer, so we cannot clearly determine which one to use for this transfer", previousJobResponse?.messageLog ?: listOf(), bank, serialize(bank)) } accountToUse = selectedAccount @@ -154,7 +156,7 @@ open class FinTsClient( val response = config.jobExecutor.transferMoneyAsync(context, BankTransferData(param.recipientName, param.recipientAccountIdentifier, recipientBankIdentifier, param.amount, param.reference, param.instantPayment)) - return TransferMoneyResponse(mapper.mapErrorCode(response), mapper.mapErrorMessages(response), mapper.mergeMessageLog(previousJobResponse, response), bank) + return TransferMoneyResponse(mapper.mapErrorCode(response), mapper.mapErrorMessages(response), mapper.mergeMessageLog(previousJobResponse, response), bank, serialize(bank)) } private fun getRecipientBankCode(param: TransferMoneyParameter): String? { @@ -192,7 +194,7 @@ open class FinTsClient( */ open suspend fun getRequiredDataToSendUserJobs(param: FinTsClientParameter): net.dankito.banking.client.model.response.FinTsClientResponse { if (param.finTsModel != null) { - return net.dankito.banking.client.model.response.FinTsClientResponse(null, null, emptyList(), param.finTsModel) + return net.dankito.banking.client.model.response.FinTsClientResponse(null, null, emptyList(), param.finTsModel, serialize(param.finTsModel)) } val defaultValues = (param as? GetAccountDataParameter)?.defaultBankValues @@ -207,7 +209,7 @@ open class FinTsClient( val getAccountInfoResponse = getAccountInfo(param, bank) return net.dankito.banking.client.model.response.FinTsClientResponse(mapper.mapErrorCode(getAccountInfoResponse), mapper.mapErrorMessages(getAccountInfoResponse), - getAccountInfoResponse.messageLog, bank) + getAccountInfoResponse.messageLog, bank, serialize(bank)) } protected open suspend fun getAccountInfo(param: FinTsClientParameter, bank: BankData): GetAccountInfoResponse { @@ -234,4 +236,12 @@ open class FinTsClient( return GetAccountInfoResponse(context, getAccountsResponse) } + + @JvmName("serializeNullable") + @JsName("serializeNullable") + private fun serialize(bank: BankData?) = + bank?.let { serialize(bank) } + + private fun serialize(bank: BankData): String? = mapper.serialize(bank) + } \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/mapper/FinTsModelMapper.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/mapper/FinTsModelMapper.kt index c74b7781..1e138eb0 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/mapper/FinTsModelMapper.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/mapper/FinTsModelMapper.kt @@ -1,10 +1,6 @@ package net.codinux.banking.fints.mapper import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.atTime -import kotlinx.datetime.toInstant -import net.codinux.banking.fints.extensions.EuropeBerlin import net.dankito.banking.client.model.* import net.dankito.banking.client.model.AccountTransaction import net.dankito.banking.client.model.parameter.FinTsClientParameter @@ -19,9 +15,12 @@ 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.codinux.banking.fints.serialization.FinTsModelSerializer -open class FinTsModelMapper { +open class FinTsModelMapper( + private val serializer: FinTsModelSerializer = FinTsModelSerializer() +) { protected open val bicFinder = BicFinder() @@ -204,4 +203,7 @@ open class FinTsModelMapper { return responses.filterNotNull().flatMap { it.messageLog } } + open fun serialize(finTsModel: BankData?): String? = + finTsModel?.let { serializer.serializeToJson(finTsModel) } + } \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/datenelemente/implementierte/tan/TanMedium.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/datenelemente/implementierte/tan/TanMedium.kt index aef9c059..e02b728f 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/datenelemente/implementierte/tan/TanMedium.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/messages/datenelemente/implementierte/tan/TanMedium.kt @@ -23,6 +23,11 @@ open class TanMedium( internal constructor() : this(TanMediumKlasse.AlleMedien, TanMediumStatus.Verfuegbar, null) // for object deserializers + val identifier: String by lazy { + "$mediumClass $mediumName $status ${tanGenerator?.cardNumber} ${mobilePhone?.concealedPhoneNumber ?: mobilePhone?.concealedPhoneNumber}" + } + + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/AccountData.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/AccountData.kt index 95e6f369..8ef7121f 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/AccountData.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/AccountData.kt @@ -1,12 +1,15 @@ package net.codinux.banking.fints.model +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import net.codinux.banking.fints.FinTsClient import net.codinux.banking.fints.messages.datenelemente.abgeleiteteformate.Laenderkennzeichen import net.codinux.banking.fints.messages.segmente.id.CustomerSegmentId import net.codinux.banking.fints.response.segments.AccountType import net.codinux.banking.fints.response.segments.JobParameters - +@Serializable open class AccountData( open val accountIdentifier: String, open val subAccountAttribute: String?, @@ -20,6 +23,7 @@ open class AccountData( open val productName: String?, open val accountLimit: String?, open val allowedJobNames: List, + @Transient // can be restored from bank.supportedJobs and this.allowedJobNames open var allowedJobs: List = listOf() ) { @@ -40,6 +44,7 @@ open class AccountData( open var serverTransactionsRetentionDays: Int? = null + @SerialName("supportedFeatures") protected open val _supportedFeatures = mutableSetOf() open val supportedFeatures: Collection diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/mapper/ModelMapper.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/mapper/ModelMapper.kt index 786e2015..7c738532 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/mapper/ModelMapper.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/model/mapper/ModelMapper.kt @@ -152,7 +152,7 @@ open class ModelMapper( } } - protected open fun findTanMethod(securityFunction: Sicherheitsfunktion, bank: BankData): TanMethod? { + open fun findTanMethod(securityFunction: Sicherheitsfunktion, bank: BankData): TanMethod? { return bank.tanMethodsSupportedByBank.firstOrNull { it.securityFunction == securityFunction } } @@ -173,7 +173,7 @@ open class ModelMapper( account.setSupportsFeature(AccountFeature.RealTimeTransfer, messageBuilder.supportsSepaRealTimeTransfer(bank, account)) } - protected open fun mapToTanMethods(tanInfo: TanInfo): List { + open fun mapToTanMethods(tanInfo: TanInfo): List { return tanInfo.tanProcedureParameters.methodParameters.mapNotNull { mapToTanMethod(it, tanInfo.segmentVersion) } diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/segments/ReceivedSegment.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/segments/ReceivedSegment.kt index 648ffa91..034cbcb0 100644 --- a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/segments/ReceivedSegment.kt +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/response/segments/ReceivedSegment.kt @@ -1,17 +1,13 @@ package net.codinux.banking.fints.response.segments import net.codinux.banking.fints.messages.Separators -import kotlin.jvm.Transient open class ReceivedSegment( open val segmentId: String, - @Transient open val segmentNumber: Int, open val segmentVersion: Int, - @Transient open val referenceSegmentNumber: Int? = null, - @Transient open val segmentString: String ) { diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/FinTsModelSerializer.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/FinTsModelSerializer.kt new file mode 100644 index 00000000..3c6765a7 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/FinTsModelSerializer.kt @@ -0,0 +1,48 @@ +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 + +open class 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() + + + open 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 + } + } + + open fun deserializeFromJson(serializedFinTsData: String): BankData? = try { + val serializedData = json.decodeFromString(serializedFinTsData) + + mapper.map(serializedData) + } catch (e: Throwable) { + log.error(e) { "Could not deserialize BankData from JSON:\n$serializedFinTsData"} + null + } + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/SerializedFinTsData.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/SerializedFinTsData.kt new file mode 100644 index 00000000..a4b2607e --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/SerializedFinTsData.kt @@ -0,0 +1,57 @@ +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, + val identifierOfTanMethodsAvailableForUser: List = listOf(), + val selectedTanMethodIdentifier: String, + val tanMedia: List = listOf(), + val selectedTanMediumIdentifier: String? = null, + + val supportedLanguages: List = listOf(), + val selectedLanguage: Dialogsprache = Dialogsprache.Default, + val customerSystemId: String = KundensystemID.Anonymous, + val customerSystemStatus: KundensystemStatusWerte = KundensystemStatus.SynchronizingCustomerSystemId, + + val countMaxJobsPerMessage: Int = 0, + + val supportedHbciVersions: List = listOf(), + val supportedJobs: List = listOf(), + val supportedDetailedJobs: List = listOf(), + val jobsRequiringTan: Set = emptySet(), + + val pinInfo: PinInfo? = null, + + val accounts: List +) { + + @EncodeDefault + private val modelVersion: String = "0.6.0" + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/SerializedFinTsDataMapper.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/SerializedFinTsDataMapper.kt new file mode 100644 index 00000000..41df5a75 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/SerializedFinTsDataMapper.kt @@ -0,0 +1,138 @@ +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) + } + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/DetailedSerializableJobParameters.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/DetailedSerializableJobParameters.kt new file mode 100644 index 00000000..19d95cd9 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/DetailedSerializableJobParameters.kt @@ -0,0 +1,13 @@ +package net.codinux.banking.fints.serialization.jobparameter + +import kotlinx.serialization.Serializable + +@Serializable +sealed class DetailedSerializableJobParameters { + + abstract val jobParameters: SerializableJobParameters + + + override fun toString() = jobParameters.toString() + +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableChangeTanMediaParameters.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableChangeTanMediaParameters.kt new file mode 100644 index 00000000..e2837ef2 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableChangeTanMediaParameters.kt @@ -0,0 +1,17 @@ +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 +) : DetailedSerializableJobParameters() \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableJobParameters.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableJobParameters.kt new file mode 100644 index 00000000..00117f2c --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableJobParameters.kt @@ -0,0 +1,19 @@ +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" +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableRetrieveAccountTransactionsParameters.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableRetrieveAccountTransactionsParameters.kt new file mode 100644 index 00000000..99213db8 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableRetrieveAccountTransactionsParameters.kt @@ -0,0 +1,17 @@ +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 = emptyList() +) : DetailedSerializableJobParameters() { + override fun toString() = "${super.toString()}, serverTransactionsRetentionDays = $serverTransactionsRetentionDays, supportedCamtDataFormats = $supportedCamtDataFormats" +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableSepaAccountInfoParameters.kt b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableSepaAccountInfoParameters.kt new file mode 100644 index 00000000..9a030706 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/codinux/banking/fints/serialization/jobparameter/SerializableSepaAccountInfoParameters.kt @@ -0,0 +1,19 @@ +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 +) : DetailedSerializableJobParameters() { + override fun toString() = "${super.toString()}, supportedSepaFormats = $supportedSepaFormats" +} \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/FinTsClientResponse.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/FinTsClientResponse.kt index fd99e501..48b2048d 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/FinTsClientResponse.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/FinTsClientResponse.kt @@ -9,7 +9,8 @@ open class FinTsClientResponse( open val error: ErrorCode?, open val errorMessage: String?, open val messageLogWithoutSensitiveData: List, - open val finTsModel: BankData? = null + open val finTsModel: BankData? = null, + open val serializedFinTsModel: String? = null ) { internal constructor() : this(null, null, listOf()) // for object deserializers diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/GetAccountDataResponse.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/GetAccountDataResponse.kt index 96851952..779d7706 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/GetAccountDataResponse.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/GetAccountDataResponse.kt @@ -10,8 +10,9 @@ open class GetAccountDataResponse( errorMessage: String?, open val customerAccount: CustomerAccount?, messageLogWithoutSensitiveData: List, - finTsModel: BankData? = null -) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel) { + finTsModel: BankData? = null, + serializedFinTsModel: String? = null +) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel, serializedFinTsModel) { internal constructor() : this(null, null, null, listOf()) // for object deserializers diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/TransferMoneyResponse.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/TransferMoneyResponse.kt index f9bd54f4..e5b8247f 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/TransferMoneyResponse.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/client/model/response/TransferMoneyResponse.kt @@ -8,5 +8,6 @@ open class TransferMoneyResponse( error: ErrorCode?, errorMessage: String?, messageLogWithoutSensitiveData: List, - finTsModel: BankData? = null -) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel) \ No newline at end of file + finTsModel: BankData? = null, + serializedFinTsModel: String? = null +) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel, serializedFinTsModel) \ No newline at end of file diff --git a/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/serialization/FinTsModelSerializerTest.kt b/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/serialization/FinTsModelSerializerTest.kt new file mode 100644 index 00000000..d8ab3bd4 --- /dev/null +++ b/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/serialization/FinTsModelSerializerTest.kt @@ -0,0 +1,586 @@ +package net.codinux.banking.fints.serialization + +import net.codinux.banking.fints.messages.MessageBuilder +import net.codinux.banking.fints.messages.datenelemente.abgeleiteteformate.Laenderkennzeichen +import net.codinux.banking.fints.messages.datenelemente.implementierte.Dialogsprache +import net.codinux.banking.fints.messages.datenelemente.implementierte.HbciVersion +import net.codinux.banking.fints.messages.datenelemente.implementierte.KundensystemStatusWerte +import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMediumStatus +import net.codinux.banking.fints.model.AccountData +import net.codinux.banking.fints.model.AccountFeature +import net.codinux.banking.fints.model.BankData +import net.codinux.banking.fints.model.mapper.ModelMapper +import net.codinux.banking.fints.response.InstituteSegmentId +import net.codinux.banking.fints.response.ResponseParser +import net.codinux.banking.fints.response.segments.AccountType +import net.codinux.banking.fints.response.segments.TanInfo +import net.codinux.banking.fints.response.segments.TanMediaList +import net.codinux.banking.fints.test.assertContains +import net.codinux.banking.fints.test.assertSize +import net.codinux.banking.fints.test.assertTrue +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class FinTsModelSerializerTest { + + companion object { + private val bankCode = "10010010" + private val bic = "ABCDDEBBXXX" + private val bankName = "Abzockbank" + private val serverAddress = "https://abzockbank.de/fints" + private val bpd = 17 + + private val customerId = "SuperUser" + private val password = "Liebe" + private val customerName = "Monika Superfrau" + private val upd = 27 + + } + + + private val underTest = FinTsModelSerializer() + + + @Test + fun serializeToJson() { + val bank = createTestData() + + val result = underTest.serializeToJson(bank, true) + + assertEquals(serializedFinTsData, result) + } + + @Test + fun deserializeFromJson() { + val result = underTest.deserializeFromJson(serializedFinTsData) + + assertNotNull(result) + + assertSize(8, result.tanMethodsSupportedByBank) + assertSize(4, result.tanMethodsAvailableForUser) + assertContains(result.tanMethodsSupportedByBank, result.tanMethodsAvailableForUser) // check that it contains exactly the same object instances + assertNotNull(result.selectedTanMethod) + assertContains(result.tanMethodsSupportedByBank, result.selectedTanMethod) // check that it contains exactly the same object instance + + assertSize(3, result.tanMedia) + assertNotNull(result.selectedTanMedium) + assertContains(result.tanMedia, result.selectedTanMedium) // check that it contains exactly the same object instance + + assertSize(14, result.supportedJobs) + assertSize(33, result.jobsRequiringTan) + + result.accounts.forEach { account -> + assertTrue(account.allowedJobs.isNotEmpty()) + assertContains(result.supportedJobs, account.allowedJobs) // check that it contains exactly the same object instances + } + + assertEquals(serializedFinTsData, underTest.serializeToJson(result, true)) + } + + + private fun createTestData(): BankData { + val parser = ResponseParser() + val mapper = ModelMapper(MessageBuilder()) + + val bankResponse = parser.parse(""" + HIRMS:5:2:4+3050::BPD nicht mehr aktuell, aktuelle Version enthalten.+3920::Zugelassene Zwei-Schritt-Verfahren für den Benutzer.:910:911:912:913+0020::Der Auftrag wurde ausgeführt.' + HISALS:145:5:4+1+1' + HISALS:12:8:4+1+1+0+J' + HIKAZS:123:5:4+1+1+360:J:N' + HICCSS:96:1:4+1+1+0' + HIIPZS:22:1:4+1+1+0+;:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.09' + DIKKUS:67:2:4+1+1+0+90:N:J' + HITABS:153:4:4+1+1+0' + HITAUS:154:1:4+1+1+0+N:N:J' + HITANS:169:6:4+1+1+1+J:N:0:910:2:HHD1.3.0:::chipTAN manuell:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:911:2:HHD1.3.2OPT:HHDOPT1:1.3.2:chipTAN optisch:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:912:2:HHD1.3.2USB:HHDUSB1:1.3.2:chipTAN-USB:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:913:2:Q1S:Secoder_UC:1.2.0:chipTAN-QR:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:920:2:smsTAN:::smsTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:5:921:2:pushTAN:::pushTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:2:900:2:iTAN:::iTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:0' + HITANS:170:7:4+1+1+1+N:N:0:922:2:pushTAN-dec:Decoupled::pushTAN 2.0:::Aufforderung:2048:J:2:N:0:0:N:N:00:2:N:2:180:1:1:J:J:923:2:pushTAN-cas:Decoupled::pushTAN 2.0:::Aufforderung:2048:J:2:N:0:0:N:N:00:2:N:5:180:1:1:J:J' + HITAB:5:4:3+1+G:2:1234567890::::::::::SparkassenCard (Debitkarte)::::::::+G:1:1234567891::::::::::SparkassenCard (Debitkarte)::::::::' + HITAB:5:4:3+0+A:1:::::::::::Alle Geräte::::::::' + HIPINS:78:1:3+1+1+0+5:20:6:VR-NetKey oder Alias::HKTAN:N:HKKAZ:J:HKSAL:N:HKEKA:N:HKPAE:J:HKPSP:N:HKQTG:N:HKCSA:J:HKCSB:N:HKCSL:J:HKCCS:J:HKSPA:N:HKDSE:J:HKBSE:J:HKBME:J:HKCDL:J:HKPPD:J:HKCDN:J:HKDSB:N:HKCUB:N:HKDSW:J:HKAUB:J:HKBBS:N:HKDMB:N:HKDBS:N:HKBMB:N:HKECA:N:HKCMB:N:HKCME:J:HKCML:J:HKWDU:N:HKWPD:N:HKDME:J:HKCCM:J:HKCDB:N:HKCDE:J:HKCSE:J:HKCUM:J:HKKAU:N:HKKIF:N:HKBAZ:N:HKZDF:J:HKCAZ:J:HKDDB:N:HKDDE:J:HKDDL:J:HKDDN:J:HKKAA:N:HKPOF:N:HKIPS:N:HKIPZ:J:HKBML:J:HKBSA:J:HKBSL:J:HKDML:J:HKDSA:J:HKDSL:J:HKZDA:J:HKZDL:N:GKVPU:N:GKVPD:N' + HISPAS:42:1:4+20+1+0+N:N:N:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.001.02' + HISPAS:43:2:4+20+1+0+N:N:N:N:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.001.02' + HISPAS:44:3:4+20+1+0+N:N:N:N:0:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.001.02' + """.trimIndent().replace("\n", "").replace("\r", "")) + + val tanMethods = bankResponse.getSegmentsById(InstituteSegmentId.TanInfo).flatMap { mapper.mapToTanMethods(it) } + val tanMedia = bankResponse.getSegmentsById(InstituteSegmentId.TanMediaList).flatMap { it.tanMedia } + + return BankData( + bankCode, customerId, password, serverAddress, bic, bankName, Laenderkennzeichen.Germany, bpd, + customerId, customerName, upd, + + tanMethods, tanMethods.filter { it.securityFunction.code.startsWith("91") }, tanMethods.first { it.securityFunction.code == "912" }, + tanMedia, tanMedia.first { it.status == TanMediumStatus.Aktiv }, + + listOf(Dialogsprache.German, Dialogsprache.English), Dialogsprache.German, "47", KundensystemStatusWerte.Benoetigt, + 1, listOf(HbciVersion.FinTs_3_0_0) + ).apply { + this.addAccount(createCheckingAccount(this, listOf("HKSAL", "HKKAZ", "HKCCS", "HKIPZ"))) + this.addAccount(createCreditCardAccount(this, listOf("DKKKU"))) + + mapper.updateBankData(this, bankResponse) + + this.jobsRequiringTan = this.jobsRequiringTan.sorted().toSet() // sort them for comparability in tests + } + } + + private fun createCheckingAccount(bank: BankData, allowedJobNames: List) = + createAccount(bank, AccountType.Girokonto, "12345678", "Kontokorrent", allowedJobNames, AccountFeature.entries) + + private fun createCreditCardAccount(bank: BankData, allowedJobNames: List) = + createAccount(bank, AccountType.Kreditkartenkonto, "4321876521096543", "Visa-Card", allowedJobNames, listOf(AccountFeature.RetrieveAccountTransactions)) + + private fun createAccount(bank: BankData, type: AccountType, accountIdentifier: String, productName: String? = null, allowedJobNames: List, features: Collection = emptyList(), subAccountAttribute: String? = null) = AccountData( + accountIdentifier, subAccountAttribute, Laenderkennzeichen.Germany, bankCode, "DE11$bankCode$accountIdentifier", customerId, type, "EUR", customerName, productName, "T:1000,:EUR", allowedJobNames, bank.supportedJobs.filter { allowedJobNames.contains(it.jobName) } + ).apply { + this.serverTransactionsRetentionDays = 270 + + features.forEach { feature -> + this.setSupportsFeature(feature, true) + } + } + + + private val serializedFinTsData = """ + { + "bankCode": "10010010", + "customerId": "SuperUser", + "pin": "Liebe", + "finTs3ServerAddress": "https://abzockbank.de/fints", + "bic": "ABCDDEBBXXX", + "bankName": "Abzockbank", + "countryCode": 280, + "bpdVersion": 17, + "userId": "SuperUser", + "customerName": "Monika Superfrau", + "updVersion": 27, + "tanMethodsSupportedByBank": [ + { + "displayName": "chipTAN manuell", + "securityFunction": "PIN_TAN_910", + "type": "ChipTanManuell", + "hhdVersion": "HHD_1_3", + "maxTanInputLength": 6, + "allowedTanFormat": "Numeric" + }, + { + "displayName": "chipTAN optisch", + "securityFunction": "PIN_TAN_911", + "type": "ChipTanFlickercode", + "hhdVersion": "HHD_1_3", + "maxTanInputLength": 6, + "allowedTanFormat": "Numeric" + }, + { + "displayName": "chipTAN-USB", + "securityFunction": "PIN_TAN_912", + "type": "ChipTanUsb", + "hhdVersion": "HHD_1_3", + "maxTanInputLength": 6, + "allowedTanFormat": "Numeric" + }, + { + "displayName": "chipTAN-QR", + "securityFunction": "PIN_TAN_913", + "type": "ChipTanQrCode", + "maxTanInputLength": 6, + "allowedTanFormat": "Numeric" + }, + { + "displayName": "smsTAN", + "securityFunction": "PIN_TAN_920", + "type": "SmsTan", + "maxTanInputLength": 6, + "allowedTanFormat": "Numeric", + "nameOfTanMediumRequired": true + }, + { + "displayName": "pushTAN", + "securityFunction": "PIN_TAN_921", + "type": "AppTan", + "maxTanInputLength": 6, + "allowedTanFormat": "Numeric", + "nameOfTanMediumRequired": true + }, + { + "displayName": "pushTAN 2.0", + "securityFunction": "PIN_TAN_922", + "type": "DecoupledTan", + "nameOfTanMediumRequired": true, + "hktanVersion": 7, + "decoupledParameters": { + "manualConfirmationAllowed": true, + "periodicStateRequestsAllowed": true, + "maxNumberOfStateRequests": 180, + "initialDelayInSecondsForStateRequest": 1, + "delayInSecondsForNextStateRequest": 1 + } + }, + { + "displayName": "pushTAN 2.0", + "securityFunction": "PIN_TAN_923", + "type": "DecoupledTan", + "nameOfTanMediumRequired": true, + "hktanVersion": 7, + "decoupledParameters": { + "manualConfirmationAllowed": true, + "periodicStateRequestsAllowed": true, + "maxNumberOfStateRequests": 180, + "initialDelayInSecondsForStateRequest": 1, + "delayInSecondsForNextStateRequest": 1 + } + } + ], + "identifierOfTanMethodsAvailableForUser": [ + "910", + "911", + "912", + "913" + ], + "selectedTanMethodIdentifier": "912", + "tanMedia": [ + { + "mediumClass": "TanGenerator", + "status": "Verfuegbar", + "mediumName": "SparkassenCard (Debitkarte)", + "tanGenerator": { + "cardNumber": "1234567890", + "cardSequenceNumber": null, + "cardType": null, + "validFrom": null, + "validTo": null + } + }, + { + "mediumClass": "TanGenerator", + "status": "Aktiv", + "mediumName": "SparkassenCard (Debitkarte)", + "tanGenerator": { + "cardNumber": "1234567891", + "cardSequenceNumber": null, + "cardType": null, + "validFrom": null, + "validTo": null + } + }, + { + "mediumClass": "AlleMedien", + "status": "Aktiv", + "mediumName": "Alle Geräte" + } + ], + "selectedTanMediumIdentifier": "TanGenerator SparkassenCard (Debitkarte) Aktiv 1234567891 null", + "supportedLanguages": [ + "German", + "English" + ], + "selectedLanguage": "German", + "customerSystemId": "47", + "customerSystemStatus": "Benoetigt", + "countMaxJobsPerMessage": 1, + "supportedHbciVersions": [ + "FinTs_3_0_0" + ], + "supportedJobs": [ + { + "jobName": "HKSAL", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": null, + "segmentId": "HISALS", + "segmentNumber": 145, + "segmentVersion": 5, + "segmentString": "HISALS:145:5:4+1+1" + }, + { + "jobName": "HKSAL", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "HISALS", + "segmentNumber": 12, + "segmentVersion": 8, + "segmentString": "HISALS:12:8:4+1+1+0+J" + }, + { + "jobName": "HKCCS", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "HICCSS", + "segmentNumber": 96, + "segmentVersion": 1, + "segmentString": "HICCSS:96:1:4+1+1+0" + }, + { + "jobName": "HKIPZ", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "HIIPZS", + "segmentNumber": 22, + "segmentVersion": 1, + "segmentString": "HIIPZS:22:1:4+1+1+0+;:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.09" + }, + { + "jobName": "HKTAB", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "HITABS", + "segmentNumber": 153, + "segmentVersion": 4, + "segmentString": "HITABS:153:4:4+1+1+0" + }, + { + "jobName": "HKTAN", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": 1, + "segmentId": "HITANS", + "segmentNumber": 169, + "segmentVersion": 6, + "segmentString": "HITANS:169:6:4+1+1+1+J:N:0:910:2:HHD1.3.0:::chipTAN manuell:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:911:2:HHD1.3.2OPT:HHDOPT1:1.3.2:chipTAN optisch:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:912:2:HHD1.3.2USB:HHDUSB1:1.3.2:chipTAN-USB:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:913:2:Q1S:Secoder_UC:1.2.0:chipTAN-QR:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:920:2:smsTAN:::smsTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:5:921:2:pushTAN:::pushTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:2:900:2:iTAN:::iTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:0" + }, + { + "jobName": "HKTAN", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": 1, + "segmentId": "HITANS", + "segmentNumber": 170, + "segmentVersion": 7, + "segmentString": "HITANS:170:7:4+1+1+1+N:N:0:922:2:pushTAN-dec:Decoupled::pushTAN 2.0:::Aufforderung:2048:J:2:N:0:0:N:N:00:2:N:2:180:1:1:J:J:923:2:pushTAN-cas:Decoupled::pushTAN 2.0:::Aufforderung:2048:J:2:N:0:0:N:N:00:2:N:5:180:1:1:J:J" + }, + { + "jobName": "HKPIN", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "HIPINS", + "segmentNumber": 78, + "segmentVersion": 1, + "segmentString": "HIPINS:78:1:3+1+1+0+5:20:6:VR-NetKey oder Alias::HKTAN:N:HKKAZ:J:HKSAL:N:HKEKA:N:HKPAE:J:HKPSP:N:HKQTG:N:HKCSA:J:HKCSB:N:HKCSL:J:HKCCS:J:HKSPA:N:HKDSE:J:HKBSE:J:HKBME:J:HKCDL:J:HKPPD:J:HKCDN:J:HKDSB:N:HKCUB:N:HKDSW:J:HKAUB:J:HKBBS:N:HKDMB:N:HKDBS:N:HKBMB:N:HKECA:N:HKCMB:N:HKCME:J:HKCML:J:HKWDU:N:HKWPD:N:HKDME:J:HKCCM:J:HKCDB:N:HKCDE:J:HKCSE:J:HKCUM:J:HKKAU:N:HKKIF:N:HKBAZ:N:HKZDF:J:HKCAZ:J:HKDDB:N:HKDDE:J:HKDDL:J:HKDDN:J:HKKAA:N:HKPOF:N:HKIPS:N:HKIPZ:J:HKBML:J:HKBSA:J:HKBSL:J:HKDML:J:HKDSA:J:HKDSL:J:HKZDA:J:HKZDL:N:GKVPU:N:GKVPD:N" + } + ], + "supportedDetailedJobs": [ + { + "type": "RetrieveAccountTransactionsParameters", + "jobParameters": { + "jobName": "HKKAZ", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": null, + "segmentId": "HIKAZS", + "segmentNumber": 123, + "segmentVersion": 5, + "segmentString": "HIKAZS:123:5:4+1+1+360:J:N" + }, + "serverTransactionsRetentionDays": 360, + "settingCountEntriesAllowed": true, + "settingAllAccountAllowed": false + }, + { + "type": "RetrieveAccountTransactionsParameters", + "jobParameters": { + "jobName": "DKKKU", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "DIKKUS", + "segmentNumber": 67, + "segmentVersion": 2, + "segmentString": "DIKKUS:67:2:4+1+1+0+90:N:J" + }, + "serverTransactionsRetentionDays": 90, + "settingCountEntriesAllowed": false, + "settingAllAccountAllowed": true + }, + { + "type": "ChangeTanMediaParameters", + "jobParameters": { + "jobName": "HKTAU", + "maxCountJobs": 1, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "HITAUS", + "segmentNumber": 154, + "segmentVersion": 1, + "segmentString": "HITAUS:154:1:4+1+1+0+N:N:J" + }, + "enteringTanListNumberRequired": false, + "enteringCardSequenceNumberRequired": false, + "enteringAtcAndTanRequired": true, + "enteringCardTypeAllowed": false, + "accountInfoRequired": false, + "allowedCardTypes": [ + ] + }, + { + "type": "SepaAccountInfoParameters", + "jobParameters": { + "jobName": "HKSPA", + "maxCountJobs": 20, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "HISPAS", + "segmentNumber": 42, + "segmentVersion": 1, + "segmentString": "HISPAS:42:1:4+20+1+0+N:N:N:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.001.02" + }, + "retrieveSingleAccountAllowed": false, + "nationalAccountRelationshipAllowed": false, + "structuredReferenceAllowed": false, + "settingMaxAllowedEntriesAllowed": false, + "countReservedReferenceLength": 0, + "supportedSepaFormats": [ + "urn:iso:std:iso:20022:tech:xsd:pain.001.003.03", + "urn:iso:std:iso:20022:tech:xsd:pain.008.003.02", + "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03", + "urn:iso:std:iso:20022:tech:xsd:pain.008.001.02" + ] + }, + { + "type": "SepaAccountInfoParameters", + "jobParameters": { + "jobName": "HKSPA", + "maxCountJobs": 20, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "HISPAS", + "segmentNumber": 43, + "segmentVersion": 2, + "segmentString": "HISPAS:43:2:4+20+1+0+N:N:N:N:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.001.02" + }, + "retrieveSingleAccountAllowed": false, + "nationalAccountRelationshipAllowed": false, + "structuredReferenceAllowed": false, + "settingMaxAllowedEntriesAllowed": false, + "countReservedReferenceLength": 0, + "supportedSepaFormats": [ + "urn:iso:std:iso:20022:tech:xsd:pain.001.003.03", + "urn:iso:std:iso:20022:tech:xsd:pain.008.003.02", + "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03", + "urn:iso:std:iso:20022:tech:xsd:pain.008.001.02" + ] + }, + { + "type": "SepaAccountInfoParameters", + "jobParameters": { + "jobName": "HKSPA", + "maxCountJobs": 20, + "minimumCountSignatures": 1, + "securityClass": 0, + "segmentId": "HISPAS", + "segmentNumber": 44, + "segmentVersion": 3, + "segmentString": "HISPAS:44:3:4+20+1+0+N:N:N:N:0:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.001.02" + }, + "retrieveSingleAccountAllowed": false, + "nationalAccountRelationshipAllowed": false, + "structuredReferenceAllowed": false, + "settingMaxAllowedEntriesAllowed": false, + "countReservedReferenceLength": 0, + "supportedSepaFormats": [ + "urn:iso:std:iso:20022:tech:xsd:pain.001.003.03", + "urn:iso:std:iso:20022:tech:xsd:pain.008.003.02", + "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03", + "urn:iso:std:iso:20022:tech:xsd:pain.008.001.02" + ] + } + ], + "jobsRequiringTan": [ + "HKAUB", + "HKBME", + "HKBML", + "HKBSA", + "HKBSE", + "HKBSL", + "HKCAZ", + "HKCCM", + "HKCCS", + "HKCDE", + "HKCDL", + "HKCDN", + "HKCME", + "HKCML", + "HKCSA", + "HKCSE", + "HKCSL", + "HKCUM", + "HKDDE", + "HKDDL", + "HKDDN", + "HKDME", + "HKDML", + "HKDSA", + "HKDSE", + "HKDSL", + "HKDSW", + "HKIPZ", + "HKKAZ", + "HKPAE", + "HKPPD", + "HKZDA", + "HKZDF" + ], + "pinInfo": { + "minPinLength": 5, + "maxPinLength": 20, + "minTanLength": 6, + "userIdHint": "VR-NetKey oder Alias", + "customerIdHint": null + }, + "accounts": [ + { + "accountIdentifier": "12345678", + "subAccountAttribute": null, + "bankCountryCode": 280, + "bankCode": "10010010", + "iban": "DE111001001012345678", + "customerId": "SuperUser", + "accountType": "Girokonto", + "currency": "EUR", + "accountHolderName": "Monika Superfrau", + "productName": "Kontokorrent", + "accountLimit": "T:1000,:EUR", + "allowedJobNames": [ + "HKSAL", + "HKKAZ", + "HKCCS", + "HKIPZ" + ], + "serverTransactionsRetentionDays": 270, + "supportedFeatures": [ + "RetrieveBalance", + "RetrieveAccountTransactions", + "TransferMoney", + "RealTimeTransfer" + ] + }, + { + "accountIdentifier": "4321876521096543", + "subAccountAttribute": null, + "bankCountryCode": 280, + "bankCode": "10010010", + "iban": "DE11100100104321876521096543", + "customerId": "SuperUser", + "accountType": "Kreditkartenkonto", + "currency": "EUR", + "accountHolderName": "Monika Superfrau", + "productName": "Visa-Card", + "accountLimit": "T:1000,:EUR", + "allowedJobNames": [ + "DKKKU" + ], + "serverTransactionsRetentionDays": 270, + "supportedFeatures": [ + "RetrieveAccountTransactions" + ] + } + ], + "modelVersion": "0.6.0" + } + """.trimIndent() +} \ No newline at end of file diff --git a/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/test/AssertExtensions.kt b/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/test/AssertExtensions.kt index e8df9587..107c1c2c 100644 --- a/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/test/AssertExtensions.kt +++ b/fints4k/src/commonTest/kotlin/net/codinux/banking/fints/test/AssertExtensions.kt @@ -61,6 +61,12 @@ fun assertContains(collection: Collection, vararg items: T) { } } +fun assertContains(collection: Collection, items: Collection) { + items.forEach { item -> + kotlin.test.assertContains(collection, item) + } +} + inline fun assertThrows(action: () -> Unit) { try {