Implemented serializing FinTS data

This commit is contained in:
dankito 2024-10-14 21:33:08 +02:00
parent d7d2702869
commit 7cdb7247c8
20 changed files with 968 additions and 26 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String>,
@Transient // can be restored from bank.supportedJobs and this.allowedJobNames
open var allowedJobs: List<JobParameters> = listOf()
) {
@ -40,6 +44,7 @@ open class AccountData(
open var serverTransactionsRetentionDays: Int? = null
@SerialName("supportedFeatures")
protected open val _supportedFeatures = mutableSetOf<AccountFeature>()
open val supportedFeatures: Collection<AccountFeature>

View File

@ -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<TanMethod> {
open fun mapToTanMethods(tanInfo: TanInfo): List<TanMethod> {
return tanInfo.tanProcedureParameters.methodParameters.mapNotNull {
mapToTanMethod(it, tanInfo.segmentVersion)
}

View File

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

View File

@ -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>(serializedFinTsData)
mapper.map(serializedData)
} catch (e: Throwable) {
log.error(e) { "Could not deserialize BankData from JSON:\n$serializedFinTsData"}
null
}
}

View File

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

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

View File

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

View File

@ -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<Int>
) : DetailedSerializableJobParameters()

View File

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

View File

@ -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<String> = emptyList()
) : DetailedSerializableJobParameters() {
override fun toString() = "${super.toString()}, serverTransactionsRetentionDays = $serverTransactionsRetentionDays, supportedCamtDataFormats = $supportedCamtDataFormats"
}

View File

@ -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<String>
) : DetailedSerializableJobParameters() {
override fun toString() = "${super.toString()}, supportedSepaFormats = $supportedSepaFormats"
}

View File

@ -9,7 +9,8 @@ open class FinTsClientResponse(
open val error: ErrorCode?,
open val errorMessage: String?,
open val messageLogWithoutSensitiveData: List<MessageLogEntry>,
open val finTsModel: BankData? = null
open val finTsModel: BankData? = null,
open val serializedFinTsModel: String? = null
) {
internal constructor() : this(null, null, listOf()) // for object deserializers

View File

@ -10,8 +10,9 @@ open class GetAccountDataResponse(
errorMessage: String?,
open val customerAccount: CustomerAccount?,
messageLogWithoutSensitiveData: List<MessageLogEntry>,
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

View File

@ -8,5 +8,6 @@ open class TransferMoneyResponse(
error: ErrorCode?,
errorMessage: String?,
messageLogWithoutSensitiveData: List<MessageLogEntry>,
finTsModel: BankData? = null
) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel)
finTsModel: BankData? = null,
serializedFinTsModel: String? = null
) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel, serializedFinTsModel)

View File

@ -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<TanInfo>(InstituteSegmentId.TanInfo).flatMap { mapper.mapToTanMethods(it) }
val tanMedia = bankResponse.getSegmentsById<TanMediaList>(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<String>) =
createAccount(bank, AccountType.Girokonto, "12345678", "Kontokorrent", allowedJobNames, AccountFeature.entries)
private fun createCreditCardAccount(bank: BankData, allowedJobNames: List<String>) =
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<String>, features: Collection<AccountFeature> = 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()
}

View File

@ -61,6 +61,12 @@ fun <T : Any?> assertContains(collection: Collection<T>, vararg items: T) {
}
}
fun <T : Any?> assertContains(collection: Collection<T>, items: Collection<T>) {
items.forEach { item ->
kotlin.test.assertContains(collection, item)
}
}
inline fun <reified T : Throwable> assertThrows(action: () -> Unit) {
try {