Compare commits
27 commits
v1.0.0-Alp
...
main
Author | SHA1 | Date | |
---|---|---|---|
bf17bde9f5 | |||
9f3e4eff4d | |||
f89de94aa7 | |||
684b3fb40e | |||
df692ea222 | |||
636963b3d4 | |||
4802493886 | |||
ecf930fcad | |||
ce3b1d32d7 | |||
c39789dfde | |||
ab0b676216 | |||
20fe60d9f6 | |||
529caeaa87 | |||
3d6c68e743 | |||
7cdb7247c8 | |||
d7d2702869 | |||
67b58117e1 | |||
66801a1c7a | |||
2410504ede | |||
2a3b962af5 | |||
8346fb5077 | |||
8dc2174081 | |||
05322aface | |||
dcbbe043f0 | |||
65d983a5e7 | |||
9aad2a5101 | |||
be3a2df6d9 |
59 changed files with 1339 additions and 192 deletions
|
@ -1,6 +1,6 @@
|
|||
// TODO: move to versions.gradle
|
||||
ext {
|
||||
appVersionName = "1.0.0-Alpha-14"
|
||||
appVersionName = "1.0.0-Alpha-16-SNAPSHOT"
|
||||
|
||||
|
||||
/* Test */
|
||||
|
|
|
@ -13,6 +13,10 @@ kotlin {
|
|||
compilerOptions {
|
||||
// suppresses compiler warning: [EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING] 'expect'/'actual' classes (including interfaces, objects, annotations, enums, and 'actual' typealiases) are in Beta.
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
|
||||
if (System.getProperty("idea.debugger.dispatch.addr") != null) {
|
||||
freeCompilerArgs.add("-Xdebug")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -72,6 +76,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")
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
@ -54,13 +56,13 @@ open class FinTsClient(
|
|||
|
||||
open suspend fun getAccountDataAsync(param: GetAccountDataParameter): GetAccountDataResponse {
|
||||
val basicAccountDataResponse = getRequiredDataToSendUserJobs(param)
|
||||
val bank = basicAccountDataResponse.finTsModel
|
||||
|
||||
if (basicAccountDataResponse.successful == false || param.retrieveOnlyAccountInfo || basicAccountDataResponse.finTsModel == null) {
|
||||
if (basicAccountDataResponse.successful == false || param.retrieveOnlyAccountInfo || bank == null) {
|
||||
return GetAccountDataResponse(basicAccountDataResponse.error, basicAccountDataResponse.errorMessage, null,
|
||||
basicAccountDataResponse.messageLogWithoutSensitiveData, basicAccountDataResponse.finTsModel)
|
||||
basicAccountDataResponse.messageLog, bank)
|
||||
} else {
|
||||
val bank = basicAccountDataResponse.finTsModel!!
|
||||
return getAccountData(param, bank, bank.accounts, basicAccountDataResponse.messageLogWithoutSensitiveData)
|
||||
return getAccountData(param, bank, bank.accounts, basicAccountDataResponse.messageLog)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,8 +193,8 @@ open class FinTsClient(
|
|||
* 04 FinTS_3.0_Messages_Geschaeftsvorfaelle.pdf
|
||||
*/
|
||||
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)
|
||||
param.finTsModelOrDeserialized?.let { finTsModel ->
|
||||
return net.dankito.banking.client.model.response.FinTsClientResponse(null, null, emptyList(), finTsModel)
|
||||
}
|
||||
|
||||
val defaultValues = (param as? GetAccountDataParameter)?.defaultBankValues
|
||||
|
@ -211,7 +213,7 @@ open class FinTsClient(
|
|||
}
|
||||
|
||||
protected open suspend fun getAccountInfo(param: FinTsClientParameter, bank: BankData): GetAccountInfoResponse {
|
||||
param.finTsModel?.let {
|
||||
param.finTsModelOrDeserialized?.let {
|
||||
// TODO: implement
|
||||
// return GetAccountInfoResponse(it)
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ open class FinTsClientDeprecated(
|
|||
}
|
||||
|
||||
|
||||
open suspend fun changeTanMedium(newActiveTanMedium: TanGeneratorTanMedium, bank: BankData): FinTsClientResponse {
|
||||
open suspend fun changeTanMedium(newActiveTanMedium: TanMedium, bank: BankData): FinTsClientResponse {
|
||||
val context = JobContext(JobContextType.ChangeTanMedium, this.callback, config, bank)
|
||||
|
||||
val response = config.jobExecutor.changeTanMedium(context, newActiveTanMedium)
|
||||
|
|
|
@ -341,7 +341,7 @@ open class FinTsJobExecutor(
|
|||
}
|
||||
|
||||
|
||||
open suspend fun changeTanMedium(context: JobContext, newActiveTanMedium: TanGeneratorTanMedium): BankResponse {
|
||||
open suspend fun changeTanMedium(context: JobContext, newActiveTanMedium: TanMedium): BankResponse {
|
||||
val bank = context.bank
|
||||
|
||||
if (bank.changeTanMediumParameters?.enteringAtcAndTanRequired == true) {
|
||||
|
@ -358,7 +358,7 @@ open class FinTsJobExecutor(
|
|||
}
|
||||
}
|
||||
|
||||
protected open suspend fun sendChangeTanMediumMessage(context: JobContext, newActiveTanMedium: TanGeneratorTanMedium, enteredAtc: EnterTanGeneratorAtcResult?): BankResponse {
|
||||
protected open suspend fun sendChangeTanMediumMessage(context: JobContext, newActiveTanMedium: TanMedium, enteredAtc: EnterTanGeneratorAtcResult?): BankResponse {
|
||||
|
||||
return sendMessageInNewDialogAndHandleResponse(context, null, true) {
|
||||
messageBuilder.createChangeTanMediumMessage(context, newActiveTanMedium, enteredAtc?.tan, enteredAtc?.atc)
|
||||
|
@ -444,7 +444,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
return when (tanMethod.type) {
|
||||
TanMethodType.ChipTanFlickercode ->
|
||||
FlickerCodeTanChallenge(
|
||||
FlickerCodeDecoder().decodeChallenge(challenge, tanMethod.hhdVersion ?: HHDVersion.HHD_1_4), // HHD 1.4 is currently the most used version
|
||||
FlickerCodeDecoder().decodeChallenge(challenge, tanMethod.hhdVersion ?: getFallbackHhdVersion(challenge)),
|
||||
forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime)
|
||||
|
||||
TanMethodType.ChipTanQrCode, TanMethodType.ChipTanPhotoTanMatrixCode,
|
||||
|
@ -455,6 +455,14 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
}
|
||||
}
|
||||
|
||||
protected open fun getFallbackHhdVersion(challenge: String): HHDVersion {
|
||||
if (challenge.length <= 35) { // is this true in all circumstances?
|
||||
return HHDVersion.HHD_1_3
|
||||
}
|
||||
|
||||
return HHDVersion.HHD_1_4 // HHD 1.4 is currently the most used version
|
||||
}
|
||||
|
||||
protected open suspend fun mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context: JobContext, tanChallenge: TanChallenge, tanResponse: TanResponse) {
|
||||
context.bank.selectedTanMethod.decoupledParameters?.let { decoupledTanMethodParameters ->
|
||||
if (decoupledTanMethodParameters.periodicStateRequestsAllowed) {
|
||||
|
@ -519,7 +527,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
|
||||
if (enteredTanResult.changeTanMethodTo != null) {
|
||||
return handleUserAsksToChangeTanMethodAndResendLastMessage(context, enteredTanResult.changeTanMethodTo)
|
||||
} else if (enteredTanResult.changeTanMediumTo is TanGeneratorTanMedium) {
|
||||
} else if (enteredTanResult.changeTanMediumTo != null) {
|
||||
return handleUserAsksToChangeTanMediumAndResendLastMessage(context, enteredTanResult.changeTanMediumTo,
|
||||
enteredTanResult.changeTanMediumResultCallback)
|
||||
} else if (enteredTanResult.userApprovedDecoupledTan == true && enteredTanResult.responseAfterApprovingDecoupledTan != null) {
|
||||
|
@ -554,7 +562,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
return resendMessageInNewDialog(context, lastCreatedMessage)
|
||||
}
|
||||
|
||||
protected open suspend fun handleUserAsksToChangeTanMediumAndResendLastMessage(context: JobContext, changeTanMediumTo: TanGeneratorTanMedium,
|
||||
protected open suspend fun handleUserAsksToChangeTanMediumAndResendLastMessage(context: JobContext, changeTanMediumTo: TanMedium,
|
||||
changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?): BankResponse {
|
||||
|
||||
val lastCreatedMessage = context.dialog.currentMessage
|
||||
|
@ -582,7 +590,8 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
|
|||
|
||||
val initDialogResponse = initDialogWithStrongCustomerAuthentication(context)
|
||||
|
||||
if (initDialogResponse.successful == false) {
|
||||
// if lastCreatedMessage was a dialog init message, there's no need to send this message again, we just initialized a new dialog in initDialogWithStrongCustomerAuthentication()
|
||||
if (initDialogResponse.successful == false || lastCreatedMessage.isDialogInitMessage()) {
|
||||
return initDialogResponse
|
||||
} else {
|
||||
val newMessage = messageBuilder.rebuildMessage(context, lastCreatedMessage)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package net.codinux.banking.fints.callback
|
||||
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.model.*
|
||||
|
||||
|
||||
|
@ -25,7 +25,7 @@ interface FinTsClientCallback {
|
|||
*
|
||||
* If you do not support entering TAN generator ATC, return [EnterTanGeneratorAtcResult.userDidNotEnterAtc]
|
||||
*/
|
||||
suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult
|
||||
suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanMedium): EnterTanGeneratorAtcResult
|
||||
|
||||
/**
|
||||
* Gets fired when a FinTS message get sent to bank server, a FinTS message is received from bank server or an error occurred.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package net.codinux.banking.fints.callback
|
||||
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.model.*
|
||||
|
||||
|
||||
|
@ -14,7 +14,7 @@ open class NoOpFinTsClientCallback : FinTsClientCallback {
|
|||
return tanChallenge.userDidNotEnterTan()
|
||||
}
|
||||
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanMedium): EnterTanGeneratorAtcResult {
|
||||
return EnterTanGeneratorAtcResult.userDidNotEnterAtc()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package net.codinux.banking.fints.callback
|
||||
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.model.*
|
||||
|
||||
|
||||
open class SimpleFinTsClientCallback(
|
||||
protected open val askUserForTanMethod: ((supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?) -> TanMethod?)? = null,
|
||||
protected open val messageLogAdded: ((MessageLogEntry) -> Unit)? = null,
|
||||
protected open val enterTanGeneratorAtc: ((bank: BankData, tanMedium: TanGeneratorTanMedium) -> EnterTanGeneratorAtcResult)? = null,
|
||||
protected open val enterTanGeneratorAtc: ((bank: BankData, tanMedium: TanMedium) -> EnterTanGeneratorAtcResult)? = null,
|
||||
protected open val enterTan: ((tanChallenge: TanChallenge) -> Unit)? = null
|
||||
) : FinTsClientCallback {
|
||||
|
||||
|
@ -25,7 +25,7 @@ open class SimpleFinTsClientCallback(
|
|||
enterTan?.invoke(tanChallenge) ?: run { tanChallenge.userDidNotEnterTan() }
|
||||
}
|
||||
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanMedium): EnterTanGeneratorAtcResult {
|
||||
return enterTanGeneratorAtc?.invoke(bank, tanMedium) ?: EnterTanGeneratorAtcResult.userDidNotEnterAtc()
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,12 @@ import net.codinux.banking.fints.model.JobContextType
|
|||
|
||||
class MessageContext(
|
||||
val jobType: JobContextType,
|
||||
val dialogType: MessageType,
|
||||
val messageType: MessageType,
|
||||
val jobNumber: Int,
|
||||
val dialogNumber: Int,
|
||||
val messageNumber: Int,
|
||||
val bank: BankData,
|
||||
val account: AccountData?
|
||||
)
|
||||
) {
|
||||
override fun toString() = "${jobNumber}_${dialogNumber}_$messageNumber ${bank.bankCode} $jobType $messageType"
|
||||
}
|
|
@ -37,32 +37,29 @@ open class MessageLogCollector(
|
|||
|
||||
// in either case remove sensitive data after response is parsed as otherwise some information like account holder name and accounts may is not set yet on BankData
|
||||
open val messageLog: List<MessageLogEntry>
|
||||
// safe CPU cycles by only formatting and removing sensitive data if messageLog is really requested
|
||||
get() = _messageLog.map { MessageLogEntry(it.type, it.context, it.messageTrace, createMessageForLog(it), it.error, it.parsedSegments, it.time) }
|
||||
// safe CPU cycles by only removing sensitive data if messageLog is really requested
|
||||
get() = _messageLog.map {
|
||||
val message = createMessageForLog(it)
|
||||
val messageWithoutSensitiveData = if (options.removeSensitiveDataFromMessageLog) {
|
||||
safelyRemoveSensitiveDataFromMessage(message, it.context.bank)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
|
||||
private fun createMessageForLog(logEntry: MessageLogEntry): String {
|
||||
val message = if (logEntry.type == MessageLogEntryType.Error) {
|
||||
MessageLogEntry(it.type, it.context, it.messageTrace, message, messageWithoutSensitiveData, it.error, it.parsedSegments, it.time)
|
||||
}
|
||||
|
||||
private fun createMessageForLog(logEntry: MessageLogEntry): String =
|
||||
if (logEntry.type == MessageLogEntryType.Error) {
|
||||
logEntry.message + (if (logEntry.error != null) NewLine + getStackTrace(logEntry.error!!) else "")
|
||||
} else {
|
||||
logEntry.message
|
||||
}
|
||||
|
||||
return if (options.removeSensitiveDataFromMessageLog) {
|
||||
safelyRemoveSensitiveDataFromMessage(message, logEntry.context.bank)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open fun addMessageLog(type: MessageLogEntryType, message: String, context: MessageContext, parsedSegments: List<ReceivedSegment> = emptyList()) {
|
||||
val messageTrace = createMessageTraceString(type, context)
|
||||
|
||||
val prettyPrintMessage = if (options.collectMessageLog || options.fireCallbackOnMessageLogs || log.isDebugEnabled) { // only use CPU cycles if message will ever be used / displayed
|
||||
prettyPrintFinTsMessage(message)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
val prettyPrintMessage = prettyPrintMessageIfRequired(message)
|
||||
|
||||
log.debug { "$messageTrace\n$prettyPrintMessage" }
|
||||
|
||||
|
@ -72,15 +69,16 @@ open class MessageLogCollector(
|
|||
open fun logError(loggingClass: KClass<*>, message: String, context: MessageContext, e: Throwable? = null) {
|
||||
val type = MessageLogEntryType.Error
|
||||
val messageTrace = createMessageTraceString(type, context)
|
||||
val prettyPrintMessage = prettyPrintFinTsMessage(message) // error messages almost always get logged / displayed -> pretty print
|
||||
|
||||
LoggerFactory.getLogger(loggingClass).error(e) { messageTrace + message }
|
||||
LoggerFactory.getLogger(loggingClass).error(e) { "$messageTrace\n$prettyPrintMessage" }
|
||||
|
||||
addMessageLogEntry(type, context, messageTrace, message, e)
|
||||
addMessageLogEntry(type, context, messageTrace, prettyPrintMessage, e)
|
||||
}
|
||||
|
||||
protected open fun addMessageLogEntry(type: MessageLogEntryType, context: MessageContext, messageTrace: String, message: String, error: Throwable? = null, parsedSegments: List<ReceivedSegment> = emptyList()) {
|
||||
if (options.collectMessageLog || options.fireCallbackOnMessageLogs) {
|
||||
val newEntry = MessageLogEntry(type, context, messageTrace, message, error, parsedSegments)
|
||||
val newEntry = MessageLogEntry(type, context, messageTrace, message, null, error, parsedSegments)
|
||||
|
||||
if (options.collectMessageLog) {
|
||||
_messageLog.add(newEntry)
|
||||
|
@ -97,7 +95,7 @@ open class MessageLogCollector(
|
|||
return "${twoDigits(context.jobNumber)}_${twoDigits(context.dialogNumber)}_${twoDigits(context.messageNumber)}_" +
|
||||
"${context.bank.bankCode}_${context.bank.customerId}" +
|
||||
"${ context.account?.let { "_${it.accountIdentifier}" } ?: "" }_" +
|
||||
"${context.jobType.name}_${context.dialogType.name} " +
|
||||
"${context.jobType.name}_${context.messageType.name} " +
|
||||
"${getMessageTypeString(type)}:"
|
||||
}
|
||||
|
||||
|
@ -113,6 +111,13 @@ open class MessageLogCollector(
|
|||
}
|
||||
}
|
||||
|
||||
protected open fun prettyPrintMessageIfRequired(message: String): String =
|
||||
if (options.collectMessageLog || options.fireCallbackOnMessageLogs || log.isDebugEnabled) { // only use CPU cycles if message will ever be used / displayed
|
||||
prettyPrintFinTsMessage(message)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
|
||||
protected open fun prettyPrintFinTsMessage(message: String): String =
|
||||
finTsUtils.prettyPrintFinTsMessage(message)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,10 +4,7 @@ import net.codinux.banking.fints.extensions.randomWithSeed
|
|||
import net.codinux.banking.fints.messages.datenelemente.implementierte.Aufsetzpunkt
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.KundensystemID
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.Synchronisierungsmodus
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedienArtVersion
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMediumKlasse
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanProcess
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.*
|
||||
import net.codinux.banking.fints.messages.segmente.Segment
|
||||
import net.codinux.banking.fints.messages.segmente.Synchronisierung
|
||||
import net.codinux.banking.fints.messages.segmente.id.CustomerSegmentId
|
||||
|
@ -294,7 +291,7 @@ open class MessageBuilder(protected val utils: FinTsUtils = FinTsUtils()) {
|
|||
}
|
||||
|
||||
// TODO: no HKTAN needed?
|
||||
open fun createChangeTanMediumMessage(context: JobContext, newActiveTanMedium: TanGeneratorTanMedium,
|
||||
open fun createChangeTanMediumMessage(context: JobContext, newActiveTanMedium: TanMedium,
|
||||
tan: String? = null, atc: Int? = null): MessageBuilderResult {
|
||||
|
||||
val result = getSupportedVersionsOfJobForBank(CustomerSegmentId.ChangeTanMedium, context.bank, listOf(1, 2, 3))
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package net.codinux.banking.fints.messages
|
||||
|
||||
import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.Segmentkopf
|
||||
import net.codinux.banking.fints.messages.segmente.Segment
|
||||
import net.codinux.banking.fints.messages.segmente.implementierte.Verarbeitungsvorbereitung
|
||||
import net.codinux.banking.fints.messages.segmente.implementierte.ZweiSchrittTanEinreichung
|
||||
|
||||
|
||||
|
@ -32,4 +34,10 @@ open class MessageBuilderResult(
|
|||
&& messageBodySegments.first() is ZweiSchrittTanEinreichung
|
||||
}
|
||||
|
||||
open fun isDialogInitMessage(): Boolean =
|
||||
messageBodySegments.any { it is Verarbeitungsvorbereitung }
|
||||
|
||||
|
||||
override fun toString() = "${messageBodySegments.joinToString { (it.dataElementsAndGroups.firstOrNull() as? Segmentkopf)?.let { "${it.identifier}:${it.segmentVersion}" } ?: "<No Segment header>" } }}"
|
||||
|
||||
}
|
|
@ -1,24 +1,19 @@
|
|||
package net.codinux.banking.fints.messages.datenelemente.implementierte.tan
|
||||
|
||||
import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.account.KontoverbindungInternational
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.dankito.banking.client.model.BankAccountIdentifier
|
||||
|
||||
@Serializable
|
||||
open class MobilePhoneTanMedium(
|
||||
mediumClass: TanMediumKlasse,
|
||||
status: TanMediumStatus,
|
||||
override val mediumName: String,
|
||||
val concealedPhoneNumber: String?,
|
||||
val phoneNumber: String?,
|
||||
val smsDebitAccount: KontoverbindungInternational? = null
|
||||
) : TanMedium(mediumClass, status, mediumName) {
|
||||
|
||||
val smsDebitAccount: BankAccountIdentifier? = null
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is MobilePhoneTanMedium) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (mediumName != other.mediumName) return false
|
||||
if (concealedPhoneNumber != other.concealedPhoneNumber) return false
|
||||
if (phoneNumber != other.phoneNumber) return false
|
||||
if (smsDebitAccount != other.smsDebitAccount) return false
|
||||
|
@ -27,17 +22,15 @@ open class MobilePhoneTanMedium(
|
|||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + (mediumName.hashCode())
|
||||
result = 31 * result + (concealedPhoneNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + (phoneNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + (smsDebitAccount?.hashCode() ?: 0)
|
||||
var result = concealedPhoneNumber.hashCode()
|
||||
result = 31 * result + phoneNumber.hashCode()
|
||||
result = 31 * result + smsDebitAccount.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString() + " $mediumName ${phoneNumber ?: concealedPhoneNumber ?: ""}"
|
||||
return phoneNumber ?: concealedPhoneNumber ?: ""
|
||||
}
|
||||
|
||||
}
|
|
@ -1,24 +1,21 @@
|
|||
package net.codinux.banking.fints.messages.datenelemente.implementierte.tan
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
open class TanGeneratorTanMedium(
|
||||
mediumClass: TanMediumKlasse,
|
||||
status: TanMediumStatus,
|
||||
val cardNumber: String,
|
||||
val cardSequenceNumber: String?,
|
||||
val cardType: Int?,
|
||||
val validFrom: LocalDate?,
|
||||
val validTo: LocalDate?,
|
||||
mediumName: String?
|
||||
) : TanMedium(mediumClass, status, mediumName) {
|
||||
val validTo: LocalDate?
|
||||
) {
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as TanGeneratorTanMedium
|
||||
|
||||
|
@ -27,25 +24,22 @@ open class TanGeneratorTanMedium(
|
|||
if (cardType != other.cardType) return false
|
||||
if (validFrom != other.validFrom) return false
|
||||
if (validTo != other.validTo) return false
|
||||
if (mediumName != other.mediumName) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + cardNumber.hashCode()
|
||||
var result = cardNumber.hashCode()
|
||||
result = 31 * result + cardSequenceNumber.hashCode()
|
||||
result = 31 * result + (cardType?.hashCode() ?: 0)
|
||||
result = 31 * result + (validFrom?.hashCode() ?: 0)
|
||||
result = 31 * result + (validTo?.hashCode() ?: 0)
|
||||
result = 31 * result + (mediumName?.hashCode() ?: 0)
|
||||
result = 31 * result + cardType.hashCode()
|
||||
result = 31 * result + validFrom.hashCode()
|
||||
result = 31 * result + validTo.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString() + " $mediumName $cardNumber (card sequence number: ${cardSequenceNumber ?: "-"})"
|
||||
return "$cardNumber (card sequence number: ${cardSequenceNumber ?: "-"})"
|
||||
}
|
||||
|
||||
}
|
|
@ -14,13 +14,20 @@ import kotlinx.serialization.Serializable
|
|||
open class TanMedium(
|
||||
open val mediumClass: TanMediumKlasse,
|
||||
open val status: TanMediumStatus,
|
||||
open val mediumName: String?
|
||||
open val mediumName: String?,
|
||||
open val tanGenerator: TanGeneratorTanMedium? = null,
|
||||
open val mobilePhone: MobilePhoneTanMedium? = null
|
||||
) {
|
||||
|
||||
|
||||
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
|
||||
|
@ -29,6 +36,9 @@ open class TanMedium(
|
|||
|
||||
if (mediumClass != other.mediumClass) return false
|
||||
if (status != other.status) return false
|
||||
if (mediumName != other.mediumName) return false
|
||||
if (tanGenerator != other.tanGenerator) return false
|
||||
if (mobilePhone != other.mobilePhone) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -36,6 +46,9 @@ open class TanMedium(
|
|||
override fun hashCode(): Int {
|
||||
var result = mediumClass.hashCode()
|
||||
result = 31 * result + status.hashCode()
|
||||
result = 31 * result + mediumName.hashCode()
|
||||
result = 31 * result + tanGenerator.hashCode()
|
||||
result = 31 * result + mobilePhone.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import net.codinux.banking.fints.messages.datenelemente.basisformate.Numerisches
|
|||
import net.codinux.banking.fints.messages.datenelemente.implementierte.DoNotPrintDatenelement
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.NotAllowedDatenelement
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.allCodes
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMediumKlasse
|
||||
import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.Segmentkopf
|
||||
import net.codinux.banking.fints.messages.datenelementgruppen.implementierte.account.Kontoverbindung
|
||||
|
@ -34,13 +34,13 @@ import net.codinux.banking.fints.response.segments.ChangeTanMediaParameters
|
|||
* chipTAN-Verfahren:
|
||||
* Steht beim chipTAN-Verfahren ein Kartenwechsel an, so kann der Kunde mit diesem Geschäftsvorfall seine Karte bzw.
|
||||
* Folgekarte aktivieren. Kann der Kunde mehrere Karten verwenden, dann kann mit diesem GV die Ummeldung auf eine
|
||||
* andere Karte erfolgen. Das Kreditinstitut entscheidet selbst, ob dieser GV TAN-pflichtig istoder nicht.
|
||||
* andere Karte erfolgen. Das Kreditinstitut entscheidet selbst, ob dieser GV TAN-pflichtig ist oder nicht.
|
||||
*/
|
||||
open class TanGeneratorTanMediumAnOderUmmelden(
|
||||
segmentVersion: Int,
|
||||
segmentNumber: Int,
|
||||
bank: BankData,
|
||||
newActiveTanMedium: TanGeneratorTanMedium,
|
||||
newActiveTanMedium: TanMedium,
|
||||
/**
|
||||
* Has to be set if „Eingabe von ATC und TAN erforderlich“ (BPD)=“J“
|
||||
*/
|
||||
|
@ -57,17 +57,17 @@ open class TanGeneratorTanMediumAnOderUmmelden(
|
|||
)
|
||||
: Segment(listOf(
|
||||
Segmentkopf(CustomerSegmentId.ChangeTanMedium, segmentVersion, segmentNumber),
|
||||
Code(TanMediumKlasse.TanGenerator, allCodes<TanMediumKlasse>(), Existenzstatus.Mandatory),
|
||||
AlphanumerischesDatenelement(newActiveTanMedium.cardNumber, Existenzstatus.Mandatory),
|
||||
AlphanumerischesDatenelement(newActiveTanMedium.cardSequenceNumber, if (parameters.enteringCardSequenceNumberRequired) Existenzstatus.Mandatory else Existenzstatus.NotAllowed),
|
||||
if (segmentVersion > 1) NumerischesDatenelement(newActiveTanMedium.cardType, 2, if (parameters.enteringCardTypeAllowed) Existenzstatus.Optional else Existenzstatus.NotAllowed) else DoNotPrintDatenelement(),
|
||||
Code(newActiveTanMedium.mediumClass, allCodes<TanMediumKlasse>(), Existenzstatus.Mandatory),
|
||||
AlphanumerischesDatenelement(newActiveTanMedium.tanGenerator?.cardNumber, if (newActiveTanMedium.mediumClass == TanMediumKlasse.TanGenerator) Existenzstatus.Mandatory else Existenzstatus.NotAllowed),
|
||||
AlphanumerischesDatenelement(newActiveTanMedium.tanGenerator?.cardSequenceNumber, if (newActiveTanMedium.mediumClass == TanMediumKlasse.TanGenerator && parameters.enteringCardSequenceNumberRequired) Existenzstatus.Mandatory else Existenzstatus.NotAllowed),
|
||||
if (segmentVersion > 1) NumerischesDatenelement(newActiveTanMedium.tanGenerator?.cardType, 2, if (newActiveTanMedium.mediumClass == TanMediumKlasse.TanGenerator && parameters.enteringCardTypeAllowed) Existenzstatus.Optional else Existenzstatus.NotAllowed) else DoNotPrintDatenelement(),
|
||||
if (segmentVersion == 2) Kontoverbindung(bank.accounts.first()) else DoNotPrintDatenelement(),
|
||||
if (segmentVersion >= 3 && parameters.accountInfoRequired) KontoverbindungInternational(bank.accounts.first(), bank) else DoNotPrintDatenelement(),
|
||||
if (segmentVersion >= 2) Datum(newActiveTanMedium.validFrom, Existenzstatus.Optional) else DoNotPrintDatenelement(),
|
||||
if (segmentVersion >= 2) Datum(newActiveTanMedium.validTo, Existenzstatus.Optional) else DoNotPrintDatenelement(),
|
||||
if (segmentVersion >= 3) AlphanumerischesDatenelement(iccsn, Existenzstatus.Optional, 19) else DoNotPrintDatenelement(),
|
||||
if (segmentVersion >= 2 && newActiveTanMedium.mediumClass == TanMediumKlasse.TanGenerator) Datum(newActiveTanMedium.tanGenerator?.validFrom, Existenzstatus.Optional) else DoNotPrintDatenelement(),
|
||||
if (segmentVersion >= 2 && newActiveTanMedium.mediumClass == TanMediumKlasse.TanGenerator) Datum(newActiveTanMedium.tanGenerator?.validTo, Existenzstatus.Optional) else DoNotPrintDatenelement(),
|
||||
if (segmentVersion >= 3 && newActiveTanMedium.mediumClass == TanMediumKlasse.TanGenerator) AlphanumerischesDatenelement(iccsn, Existenzstatus.Optional, 19) else DoNotPrintDatenelement(),
|
||||
NotAllowedDatenelement(), // TAN-Listennummer not supported anymore
|
||||
NumerischesDatenelement(atc, 5, if (parameters.enteringAtcAndTanRequired) Existenzstatus.Mandatory else Existenzstatus.NotAllowed),
|
||||
NumerischesDatenelement(atc, 5, if (newActiveTanMedium.mediumClass == TanMediumKlasse.TanGenerator && parameters.enteringAtcAndTanRequired) Existenzstatus.Mandatory else Existenzstatus.NotAllowed),
|
||||
AlphanumerischesDatenelement(tan, if (parameters.enteringAtcAndTanRequired) Existenzstatus.Mandatory else Existenzstatus.NotAllowed, 99)
|
||||
)) {
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -148,10 +148,10 @@ open class AccountTransaction(
|
|||
result = 31 * result + amount.hashCode()
|
||||
result = 31 * result + reference.hashCode()
|
||||
result = 31 * result + bookingDate.hashCode()
|
||||
result = 31 * result + (otherPartyName?.hashCode() ?: 0)
|
||||
result = 31 * result + (otherPartyBankId?.hashCode() ?: 0)
|
||||
result = 31 * result + (otherPartyAccountId?.hashCode() ?: 0)
|
||||
result = 31 * result + (postingText?.hashCode() ?: 0)
|
||||
result = 31 * result + otherPartyName.hashCode()
|
||||
result = 31 * result + otherPartyBankId.hashCode()
|
||||
result = 31 * result + otherPartyAccountId.hashCode()
|
||||
result = 31 * result + postingText.hashCode()
|
||||
result = 31 * result + valueDate.hashCode()
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package net.codinux.banking.fints.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.codinux.banking.fints.messages.datenelemente.abgeleiteteformate.Laenderkennzeichen
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.*
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion
|
||||
|
@ -7,9 +8,9 @@ import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMe
|
|||
import net.codinux.banking.fints.messages.segmente.id.ISegmentId
|
||||
import net.codinux.banking.fints.response.segments.ChangeTanMediaParameters
|
||||
import net.codinux.banking.fints.response.segments.JobParameters
|
||||
import net.codinux.banking.fints.response.segments.PinInfo
|
||||
|
||||
import net.codinux.banking.fints.serialization.BankDataSerializer
|
||||
|
||||
@Serializable(with = BankDataSerializer::class)
|
||||
open class BankData(
|
||||
open var bankCode: String,
|
||||
open var customerId: String,
|
||||
|
@ -30,7 +31,6 @@ open class BankData(
|
|||
open var selectedTanMethod: TanMethod = TanMethodNotSelected,
|
||||
open var tanMedia: List<TanMedium> = listOf(),
|
||||
open var selectedTanMedium: TanMedium? = null,
|
||||
open var changeTanMediumParameters: ChangeTanMediaParameters? = null,
|
||||
|
||||
open var supportedLanguages: List<Dialogsprache> = listOf(),
|
||||
open var selectedLanguage: Dialogsprache = Dialogsprache.Default,
|
||||
|
@ -44,7 +44,8 @@ open class BankData(
|
|||
open var countMaxJobsPerMessage: Int = 0,
|
||||
|
||||
open var supportedHbciVersions: List<HbciVersion> = listOf(),
|
||||
open var supportedJobs: List<JobParameters> = listOf()
|
||||
open var supportedJobs: List<JobParameters> = listOf(),
|
||||
open var jobsRequiringTan: Set<String> = emptySet()
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
@ -62,6 +63,11 @@ open class BankData(
|
|||
internal constructor() : this("", "", "", "", "") // for object deserializers
|
||||
|
||||
|
||||
open var pinInfo: PinInfo? = null
|
||||
|
||||
open val changeTanMediumParameters: ChangeTanMediaParameters?
|
||||
get() = supportedJobs.filterIsInstance<ChangeTanMediaParameters>().firstOrNull()
|
||||
|
||||
|
||||
protected open val _accounts = mutableListOf<AccountData>()
|
||||
|
||||
|
@ -82,16 +88,6 @@ open class BankData(
|
|||
}
|
||||
|
||||
|
||||
open var jobsRequiringTan: Set<String> = emptySet()
|
||||
protected set
|
||||
|
||||
open var pinInfo: PinInfo? = null
|
||||
set(value) {
|
||||
field = value
|
||||
// TODO: in case of null: actually in this case it's not allowed to execute job via PIN/TAN at all
|
||||
jobsRequiringTan = value?.jobTanConfiguration.orEmpty().filter { it.tanRequired }.map { it.segmentId }.toSet()
|
||||
}
|
||||
|
||||
open fun doesJobRequireTan(segmentId: ISegmentId): Boolean = doesJobRequireTan(segmentId.id)
|
||||
|
||||
open fun doesJobRequireTan(segmentId: String): Boolean =
|
||||
|
|
|
@ -11,6 +11,7 @@ open class MessageLogEntry(
|
|||
open val context: MessageContext,
|
||||
open val messageTrace: String,
|
||||
open val message: String,
|
||||
open val messageWithoutSensitiveData: String? = null,
|
||||
open val error: Throwable? = null,
|
||||
/**
|
||||
* Parsed received segments.
|
||||
|
@ -25,7 +26,7 @@ open class MessageLogEntry(
|
|||
get() = messageTrace + "\n" + message
|
||||
|
||||
override fun toString(): String {
|
||||
return "$type $message"
|
||||
return "$context $type $message"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package net.codinux.banking.fints.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
open class PinInfo(
|
||||
val minPinLength: Int?,
|
||||
val maxPinLength: Int?,
|
||||
val minTanLength: Int?,
|
||||
val userIdHint: String?,
|
||||
val customerIdHint: String?
|
||||
)
|
|
@ -11,6 +11,7 @@ import net.codinux.banking.fints.model.*
|
|||
import net.codinux.banking.fints.response.BankResponse
|
||||
import net.codinux.banking.fints.response.InstituteSegmentId
|
||||
import net.codinux.banking.fints.response.segments.*
|
||||
import net.codinux.banking.fints.response.segments.PinInfo
|
||||
|
||||
|
||||
open class ModelMapper(
|
||||
|
@ -34,7 +35,8 @@ open class ModelMapper(
|
|||
}
|
||||
|
||||
response.getFirstSegmentById<PinInfo>(InstituteSegmentId.PinInfo)?.let { pinInfo ->
|
||||
bank.pinInfo = pinInfo
|
||||
bank.pinInfo = net.codinux.banking.fints.model.PinInfo(pinInfo.minPinLength, pinInfo.maxPinLength, pinInfo.minTanLength, pinInfo.userIdHint, pinInfo.customerIdHint)
|
||||
bank.jobsRequiringTan = pinInfo.jobTanConfiguration.filter { it.tanRequired }.map { it.segmentId }.toHashSet()
|
||||
}
|
||||
|
||||
val tanInfos = response.getSegmentsById<TanInfo>(InstituteSegmentId.TanInfo)
|
||||
|
@ -54,10 +56,6 @@ open class ModelMapper(
|
|||
}
|
||||
}
|
||||
|
||||
response.getFirstSegmentById<ChangeTanMediaParameters>(InstituteSegmentId.ChangeTanMediaParameters)?.let { parameters ->
|
||||
bank.changeTanMediumParameters = parameters
|
||||
}
|
||||
|
||||
if (response.supportedJobs.isNotEmpty()) {
|
||||
bank.supportedJobs = response.supportedJobs
|
||||
}
|
||||
|
@ -154,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 }
|
||||
}
|
||||
|
||||
|
@ -175,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)
|
||||
}
|
||||
|
|
|
@ -41,6 +41,10 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
|
|||
|
||||
AccountTransactionsMt940Parameters(AccountTransactionsMt940.id + "S"),
|
||||
|
||||
AccountTransactionsCamt("HICAZ"),
|
||||
|
||||
AccountTransactionsCamtParameters(AccountTransactionsCamt.id + "S"),
|
||||
|
||||
CreditCardTransactions("DIKKU"),
|
||||
|
||||
CreditCardTransactionsParameters(CreditCardTransactions.id + "S"),
|
||||
|
|
|
@ -25,6 +25,7 @@ import net.codinux.banking.fints.response.segments.*
|
|||
import net.codinux.banking.fints.util.MessageUtils
|
||||
import net.codinux.banking.fints.extensions.getAllExceptionMessagesJoined
|
||||
import net.codinux.banking.fints.transactions.swift.Mt535Parser
|
||||
import net.dankito.banking.client.model.BankAccountIdentifier
|
||||
|
||||
|
||||
open class ResponseParser(
|
||||
|
@ -123,6 +124,9 @@ open class ResponseParser(
|
|||
InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups)
|
||||
InstituteSegmentId.AccountTransactionsMt940Parameters.id -> parseMt940AccountTransactionsParameters(segment, segmentId, dataElementGroups)
|
||||
|
||||
// InstituteSegmentId.AccountTransactionsCamt.id -> parseCamtAccountTransactions(segment, dataElementGroups)
|
||||
InstituteSegmentId.AccountTransactionsCamtParameters.id -> parseCamtAccountTransactionsParameters(segment, segmentId, dataElementGroups)
|
||||
|
||||
InstituteSegmentId.CreditCardTransactions.id -> parseCreditCardTransactions(segment, dataElementGroups)
|
||||
InstituteSegmentId.CreditCardTransactionsParameters.id -> parseCreditCardTransactionsParameters(segment, segmentId, dataElementGroups)
|
||||
|
||||
|
@ -613,34 +617,32 @@ open class ResponseParser(
|
|||
|
||||
val mediumName = if (hitabVersion < 2) null else parseStringToNullIfEmpty(remainingDataElements[10])
|
||||
|
||||
return when (mediumClass) {
|
||||
TanMediumKlasse.TanGenerator -> parseTanGeneratorTanMedium(mediumClass, status, mediumName, hitabVersion, remainingDataElements)
|
||||
TanMediumKlasse.MobiltelefonMitMobileTan -> parseMobilePhoneTanMedium(mediumClass, status, mediumName, hitabVersion, remainingDataElements)
|
||||
else -> TanMedium(mediumClass, status, mediumName) // Sparkasse sends for pushTan now class 'AlleMedien' -> set medium name and everything just works fine
|
||||
}
|
||||
val tanGenerator = if (mediumClass == TanMediumKlasse.TanGenerator) parseTanGeneratorTanMedium(hitabVersion, remainingDataElements)
|
||||
else null
|
||||
val mobilePhone = if (mediumClass == TanMediumKlasse.MobiltelefonMitMobileTan) parseMobilePhoneTanMedium(hitabVersion, remainingDataElements)
|
||||
else null
|
||||
|
||||
return TanMedium(mediumClass, status, mediumName, tanGenerator, mobilePhone) // Sparkasse sends for pushTan now class 'AlleMedien' -> set medium name and everything just works fine
|
||||
}
|
||||
|
||||
protected open fun parseTanGeneratorTanMedium(mediumClass: TanMediumKlasse, status: TanMediumStatus, mediumName: String?,
|
||||
hitabVersion: Int, dataElements: List<String>): TanGeneratorTanMedium {
|
||||
protected open fun parseTanGeneratorTanMedium(hitabVersion: Int, dataElements: List<String>): TanGeneratorTanMedium {
|
||||
|
||||
val cardType = if (hitabVersion < 2) null else parseNullableInt(dataElements[2])
|
||||
// TODO: may also parse account info
|
||||
val validFrom = if (hitabVersion < 2) null else parseNullableDate(dataElements[8])
|
||||
val validTo = if (hitabVersion < 2) null else parseNullableDate(dataElements[9])
|
||||
|
||||
return TanGeneratorTanMedium(mediumClass, status, parseString(dataElements[0]), parseStringToNullIfEmpty(dataElements[1]),
|
||||
cardType, validFrom, validTo, mediumName)
|
||||
return TanGeneratorTanMedium(parseString(dataElements[0]), parseStringToNullIfEmpty(dataElements[1]),
|
||||
cardType, validFrom, validTo)
|
||||
}
|
||||
|
||||
protected open fun parseMobilePhoneTanMedium(mediumClass: TanMediumKlasse, status: TanMediumStatus, mediumName: String?,
|
||||
hitabVersion: Int, dataElements: List<String>): MobilePhoneTanMedium {
|
||||
protected open fun parseMobilePhoneTanMedium(hitabVersion: Int, dataElements: List<String>): MobilePhoneTanMedium {
|
||||
|
||||
val concealedPhoneNumber = if (hitabVersion < 2) null else parseStringToNullIfEmpty(dataElements[11])
|
||||
val phoneNumber = if (hitabVersion < 2) null else parseStringToNullIfEmpty(dataElements[12])
|
||||
val smsDebitAccount: KontoverbindungInternational? = null // TODO: may parse 13th data element to KontoverbindungInternational
|
||||
val smsDebitAccount: BankAccountIdentifier? = null // TODO: may parse 13th data element to KontoverbindungInternational and map to BankAccountIdentifier
|
||||
|
||||
// mediumName should actually never be unset according to spec
|
||||
return MobilePhoneTanMedium(mediumClass, status, mediumName ?: "", concealedPhoneNumber, phoneNumber, smsDebitAccount)
|
||||
return MobilePhoneTanMedium(concealedPhoneNumber, phoneNumber, smsDebitAccount)
|
||||
}
|
||||
|
||||
|
||||
|
@ -767,6 +769,20 @@ open class ResponseParser(
|
|||
return RetrieveAccountTransactionsParameters(jobParameters, serverTransactionsRetentionDays, settingCountEntriesAllowed, settingAllAccountAllowed)
|
||||
}
|
||||
|
||||
protected open fun parseCamtAccountTransactionsParameters(segment: String, segmentId: String, dataElementGroups: List<String>): RetrieveAccountTransactionsParameters {
|
||||
val jobParameters = parseJobParameters(segment, segmentId, dataElementGroups)
|
||||
|
||||
val dataElements = getDataElements(dataElementGroups[4])
|
||||
|
||||
val serverTransactionsRetentionDays = parseInt(dataElements[0])
|
||||
val settingCountEntriesAllowed = parseBoolean(dataElements[1])
|
||||
val settingAllAccountAllowed = parseBoolean(dataElements[2])
|
||||
|
||||
val supportedCamtDataFormats = dataElements.subList(3, dataElements.size)
|
||||
|
||||
return RetrieveAccountTransactionsParameters(jobParameters, serverTransactionsRetentionDays, settingCountEntriesAllowed, settingAllAccountAllowed, supportedCamtDataFormats)
|
||||
}
|
||||
|
||||
|
||||
protected open fun parseCreditCardTransactions(segment: String, dataElementGroups: List<String>): ReceivedCreditCardTransactionsAndBalance {
|
||||
val balance = parseBalance(dataElementGroups[3])
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
||||
|
|
|
@ -5,9 +5,14 @@ open class RetrieveAccountTransactionsParameters(
|
|||
parameters: JobParameters,
|
||||
open val serverTransactionsRetentionDays: Int,
|
||||
open val settingCountEntriesAllowed: Boolean,
|
||||
open val settingAllAccountAllowed: Boolean
|
||||
open val settingAllAccountAllowed: Boolean,
|
||||
open val supportedCamtDataFormats: List<String> = emptyList()
|
||||
) : JobParameters(parameters) {
|
||||
|
||||
internal constructor() : this(JobParameters(), -1, false, false) // for object deserializers
|
||||
|
||||
// for languages not supporting default parameters
|
||||
constructor(parameters: JobParameters, serverTransactionsRetentionDays: Int, settingCountEntriesAllowed: Boolean, settingAllAccountAllowed: Boolean) :
|
||||
this(parameters, serverTransactionsRetentionDays, settingCountEntriesAllowed, settingAllAccountAllowed, emptyList())
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package net.codinux.banking.fints.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
|
||||
object BankDataSerializer : KSerializer<BankData> {
|
||||
|
||||
private val serializer = SerializedFinTsData.serializer()
|
||||
|
||||
private val mapper = SerializedFinTsDataMapper()
|
||||
|
||||
|
||||
override val descriptor: SerialDescriptor = serializer.descriptor
|
||||
|
||||
override fun serialize(encoder: Encoder, value: BankData) {
|
||||
val surrogate = mapper.map(value)
|
||||
|
||||
encoder.encodeSerializableValue(serializer, surrogate)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): BankData {
|
||||
val surrogate = decoder.decodeSerializableValue(serializer)
|
||||
|
||||
return mapper.map(surrogate)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
object FinTsModelSerializer {
|
||||
|
||||
private val json: Json by lazy {
|
||||
Json { this.ignoreUnknownKeys = true }
|
||||
}
|
||||
|
||||
private val prettyPrintJson by lazy {
|
||||
Json {
|
||||
this.ignoreUnknownKeys = true
|
||||
this.prettyPrint = true
|
||||
}
|
||||
}
|
||||
|
||||
private val mapper = SerializedFinTsDataMapper()
|
||||
|
||||
private val log by logger()
|
||||
|
||||
|
||||
fun serializeToJson(bank: BankData, prettyPrint: Boolean = false): String? {
|
||||
return try {
|
||||
val serializableData = mapper.map(bank)
|
||||
|
||||
val json = if (prettyPrint) prettyPrintJson else json
|
||||
|
||||
json.encodeToString(serializableData)
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not map fints4k model to JSON" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun deserializeFromJson(serializedFinTsData: String): BankData? = try {
|
||||
val serializedData = json.decodeFromString<SerializedFinTsData>(serializedFinTsData)
|
||||
|
||||
mapper.map(serializedData)
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not deserialize BankData from JSON:\n$serializedFinTsData"}
|
||||
null
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -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()
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -3,12 +3,12 @@ package net.codinux.banking.fints.tan
|
|||
|
||||
open class FlickerCode(
|
||||
val challengeHHD_UC: String,
|
||||
val parsedDataSet: String,
|
||||
val parsedDataSet: String? = null,
|
||||
val decodingError: Exception? = null
|
||||
) {
|
||||
|
||||
val decodingSuccessful: Boolean
|
||||
get() = decodingError == null
|
||||
get() = parsedDataSet != null
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
|
|
|
@ -45,7 +45,7 @@ open class FlickerCodeDecoder {
|
|||
} catch (e: Exception) {
|
||||
log.error(e) { "Could not decode challenge $challengeHHD_UC" }
|
||||
|
||||
return FlickerCode(challengeHHD_UC, "", e)
|
||||
return FlickerCode(challengeHHD_UC, null, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@ package net.codinux.banking.fints.tan
|
|||
|
||||
|
||||
open class TanImage(
|
||||
val mimeType: String,
|
||||
val imageBytes: ByteArray,
|
||||
val mimeType: String? = null,
|
||||
val imageBytes: ByteArray? = null,
|
||||
val decodingError: Exception? = null
|
||||
) {
|
||||
|
||||
val decodingSuccessful: Boolean
|
||||
get() = decodingError == null
|
||||
get() = mimeType != null && imageBytes != null
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
|
@ -16,7 +16,7 @@ open class TanImage(
|
|||
return "Decoding error: $decodingError"
|
||||
}
|
||||
|
||||
return "$mimeType ${imageBytes.size} bytes"
|
||||
return "$mimeType ${imageBytes?.size} bytes"
|
||||
}
|
||||
|
||||
}
|
|
@ -29,7 +29,7 @@ open class TanImageDecoder {
|
|||
} catch (e: Exception) {
|
||||
log.error(e) { "Could not decode challenge HHD_UC to TanImage: $challengeHHD_UC" }
|
||||
|
||||
return TanImage("", ByteArray(0), e)
|
||||
return TanImage(null, null, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ open class Mt535Parser(
|
|||
val (marketValue, pricingTime, totalCostPrice) = parseMarketValue(holdingBlock)
|
||||
|
||||
val balance = portfolioValue?.first ?: (if (balanceIsQuantity == false) Amount(totalBalance) else null)
|
||||
val quantity = if (balanceIsQuantity) totalBalance.replace(",", "").toIntOrNull() else null
|
||||
val quantity = if (balanceIsQuantity) totalBalance.replace(",", ".").toDoubleOrNull() else null
|
||||
|
||||
Holding(name, isin, wkn, buyingDate, quantity, averageCostPrice, balance, portfolioValue?.second ?: averageCostPriceCurrency, marketValue, pricingTime, totalCostPrice)
|
||||
} catch (e: Throwable) {
|
||||
|
|
|
@ -11,7 +11,7 @@ data class Holding(
|
|||
val isin: String?,
|
||||
val wkn: String?,
|
||||
val buyingDate: LocalDate?,
|
||||
val quantity: Int?,
|
||||
val quantity: Double?,
|
||||
/**
|
||||
* (Durchschnittlicher) Einstandspreis/-kurs einer Einheit des Wertpapiers
|
||||
*/
|
||||
|
|
|
@ -85,10 +85,10 @@ open class AccountTransaction(
|
|||
var result = amount.hashCode()
|
||||
result = 31 * result + reference.hashCode()
|
||||
result = 31 * result + bookingDate.hashCode()
|
||||
result = 31 * result + (otherPartyName?.hashCode() ?: 0)
|
||||
result = 31 * result + (otherPartyBankId?.hashCode() ?: 0)
|
||||
result = 31 * result + (otherPartyAccountId?.hashCode() ?: 0)
|
||||
result = 31 * result + (postingText?.hashCode() ?: 0)
|
||||
result = 31 * result + otherPartyName.hashCode()
|
||||
result = 31 * result + otherPartyBankId.hashCode()
|
||||
result = 31 * result + otherPartyAccountId.hashCode()
|
||||
result = 31 * result + postingText.hashCode()
|
||||
result = 31 * result + valueDate.hashCode()
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package net.dankito.banking.client.model.parameter
|
|||
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.TanMethodType
|
||||
import net.codinux.banking.fints.serialization.FinTsModelSerializer
|
||||
import net.dankito.banking.client.model.CustomerCredentials
|
||||
|
||||
|
||||
|
@ -15,5 +16,12 @@ open class FinTsClientParameter(
|
|||
open val tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
|
||||
open val preferredTanMedium: String? = null, // the ID of the medium
|
||||
open val abortIfTanIsRequired: Boolean = false,
|
||||
open val finTsModel: BankData? = null
|
||||
) : CustomerCredentials(bankCode, loginName, password)
|
||||
open val finTsModel: BankData? = null,
|
||||
open val serializedFinTsModel: String? = null
|
||||
) : CustomerCredentials(bankCode, loginName, password) {
|
||||
|
||||
open val finTsModelOrDeserialized: BankData? by lazy {
|
||||
finTsModel ?: serializedFinTsModel?.let { FinTsModelSerializer.deserializeFromJson(it) }
|
||||
}
|
||||
|
||||
}
|
|
@ -25,8 +25,9 @@ open class GetAccountDataParameter(
|
|||
preferredTanMedium: String? = null,
|
||||
abortIfTanIsRequired: Boolean = false,
|
||||
finTsModel: BankData? = null,
|
||||
serializedFinTsModel: String? = null,
|
||||
open val defaultBankValues: BankData? = null
|
||||
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, tanMethodsNotSupportedByApplication, preferredTanMedium, abortIfTanIsRequired, finTsModel) {
|
||||
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, tanMethodsNotSupportedByApplication, preferredTanMedium, abortIfTanIsRequired, finTsModel, serializedFinTsModel) {
|
||||
|
||||
open val retrieveOnlyAccountInfo: Boolean
|
||||
get() = retrieveBalance == false && retrieveTransactions == RetrieveTransactions.No
|
||||
|
|
|
@ -38,7 +38,8 @@ open class TransferMoneyParameter(
|
|||
preferredTanMedium: String? = null,
|
||||
abortIfTanIsRequired: Boolean = false,
|
||||
finTsModel: BankData? = null,
|
||||
serializedFinTsModel: String? = null,
|
||||
|
||||
open val selectAccountToUseForTransfer: ((List<AccountData>) -> AccountData?)? = null // TODO: use BankAccount instead of AccountData
|
||||
|
||||
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, tanMethodsNotSupportedByApplication, preferredTanMedium, abortIfTanIsRequired, finTsModel)
|
||||
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, tanMethodsNotSupportedByApplication, preferredTanMedium, abortIfTanIsRequired, finTsModel, serializedFinTsModel)
|
|
@ -2,13 +2,14 @@ package net.dankito.banking.client.model.response
|
|||
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
import net.codinux.banking.fints.model.MessageLogEntry
|
||||
import net.codinux.banking.fints.serialization.FinTsModelSerializer
|
||||
|
||||
|
||||
// TODO: rename to BankingClientResponse?
|
||||
open class FinTsClientResponse(
|
||||
open val error: ErrorCode?,
|
||||
open val errorMessage: String?,
|
||||
open val messageLogWithoutSensitiveData: List<MessageLogEntry>,
|
||||
open val messageLog: List<MessageLogEntry>,
|
||||
open val finTsModel: BankData? = null
|
||||
) {
|
||||
|
||||
|
@ -21,4 +22,7 @@ open class FinTsClientResponse(
|
|||
open val errorCodeAndMessage: String
|
||||
get() = "$error${errorMessage?.let { " $it" }}"
|
||||
|
||||
// save some CPU cycles, only serialize finTsModel if required
|
||||
open val serializedFinTsModel: String? by lazy { finTsModel?.let { FinTsModelSerializer.serializeToJson(it) } }
|
||||
|
||||
}
|
|
@ -9,9 +9,9 @@ open class GetAccountDataResponse(
|
|||
error: ErrorCode?,
|
||||
errorMessage: String?,
|
||||
open val customerAccount: CustomerAccount?,
|
||||
messageLogWithoutSensitiveData: List<MessageLogEntry>,
|
||||
messageLog: List<MessageLogEntry>,
|
||||
finTsModel: BankData? = null
|
||||
) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel) {
|
||||
) : FinTsClientResponse(error, errorMessage, messageLog, finTsModel) {
|
||||
|
||||
internal constructor() : this(null, null, null, listOf()) // for object deserializers
|
||||
|
||||
|
|
|
@ -7,6 +7,6 @@ import net.codinux.banking.fints.model.MessageLogEntry
|
|||
open class TransferMoneyResponse(
|
||||
error: ErrorCode?,
|
||||
errorMessage: String?,
|
||||
messageLogWithoutSensitiveData: List<MessageLogEntry>,
|
||||
messageLog: List<MessageLogEntry>,
|
||||
finTsModel: BankData? = null
|
||||
) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel)
|
||||
) : FinTsClientResponse(error, errorMessage, messageLog, finTsModel)
|
|
@ -62,13 +62,10 @@ abstract class FinTsTestBase {
|
|||
val ClientConfig = FinTsClientConfiguration(FinTsClientOptions(version = ProductVersion, productName = ProductName))
|
||||
|
||||
|
||||
init {
|
||||
Bank.changeTanMediumParameters = ChangeTanMediaParameters(JobParameters("", 1, 1, 1, ":0:0"), false, false, false, false, false, listOf())
|
||||
}
|
||||
|
||||
|
||||
fun createTestBank(): BankData {
|
||||
return BankData(BankCode, CustomerId, Pin, BankFinTsServerAddress, Bic, "", BankCountryCode, selectedTanMethod = TanMethod("chipTAN-optisch", SecurityFunction, TanMethodType.ChipTanFlickercode), selectedLanguage = Language)
|
||||
return BankData(BankCode, CustomerId, Pin, BankFinTsServerAddress, Bic, "", BankCountryCode, selectedTanMethod = TanMethod("chipTAN-optisch", SecurityFunction, TanMethodType.ChipTanFlickercode), selectedLanguage = Language, supportedJobs = listOf(
|
||||
ChangeTanMediaParameters(JobParameters("", 1, 1, 1, ":0:0"), false, false, false, false, false, listOf())
|
||||
))
|
||||
}
|
||||
|
||||
fun createTestAccount(): AccountData {
|
||||
|
@ -122,15 +119,15 @@ abstract class FinTsTestBase {
|
|||
createAllowedJob(CustomerSegmentId.CreditCardTransactions, 2),
|
||||
SepaAccountInfoParameters(createAllowedJob(CustomerSegmentId.SepaBankTransfer, 1), true, true, true, true, 35, listOf("pain.001.001.03")),
|
||||
SepaAccountInfoParameters(createAllowedJob(CustomerSegmentId.SepaRealTimeTransfer, 1), true, true, true, true, 35, listOf("pain.001.001.03")),
|
||||
ChangeTanMediaParameters(changeTanMediumJob, false, false, false, false, false, listOf())
|
||||
)
|
||||
bank.jobsRequiringTan = setOf(
|
||||
CustomerSegmentId.Balance.id,
|
||||
CustomerSegmentId.AccountTransactionsMt940.id,
|
||||
CustomerSegmentId.CreditCardTransactions.id,
|
||||
CustomerSegmentId.SepaBankTransfer.id,
|
||||
CustomerSegmentId.SepaRealTimeTransfer.id
|
||||
)
|
||||
bank.pinInfo = PinInfo(getTransactionsJob, null, null, null, null, null, listOf(
|
||||
JobTanConfiguration(CustomerSegmentId.Balance.id, true),
|
||||
JobTanConfiguration(CustomerSegmentId.AccountTransactionsMt940.id, true),
|
||||
JobTanConfiguration(CustomerSegmentId.CreditCardTransactions.id, true),
|
||||
JobTanConfiguration(CustomerSegmentId.SepaBankTransfer.id, true),
|
||||
JobTanConfiguration(CustomerSegmentId.SepaRealTimeTransfer.id, true)
|
||||
))
|
||||
bank.changeTanMediumParameters = ChangeTanMediaParameters(changeTanMediumJob, false, false, false, false, false, listOf())
|
||||
|
||||
val checkingAccount = AccountData(CustomerId, null, BankCountryCode, BankCode, "ABCDDEBBXXX", CustomerId, AccountType.Girokonto, "EUR", "", null, null, bank.supportedJobs.map { it.jobName }, bank.supportedJobs)
|
||||
bank.addAccount(checkingAccount)
|
||||
|
@ -149,7 +146,7 @@ abstract class FinTsTestBase {
|
|||
3 -> messageBuilder.createInitDialogMessageWithoutStrongCustomerAuthentication(context, null)
|
||||
4 -> messageBuilder.createSynchronizeCustomerSystemIdMessage(context)
|
||||
5 -> messageBuilder.createGetTanMediaListMessage(context)
|
||||
6 -> messageBuilder.createChangeTanMediumMessage(context, TanGeneratorTanMedium(TanMediumKlasse.TanGenerator, TanMediumStatus.Aktiv, "", null, null, null, null, null), null, null)
|
||||
6 -> messageBuilder.createChangeTanMediumMessage(context, TanMedium(TanMediumKlasse.TanGenerator, TanMediumStatus.Aktiv, null, TanGeneratorTanMedium("", null, null, null, null)), null, null)
|
||||
7 -> messageBuilder.createGetBalanceMessage(context, account)
|
||||
8 -> messageBuilder.createGetTransactionsMessage(context, GetAccountTransactionsParameter(bank, account, true))
|
||||
9 -> messageBuilder.createGetTransactionsMessage(context, GetAccountTransactionsParameter(bank, bank.accounts[1], true))
|
||||
|
|
|
@ -164,7 +164,7 @@ class MessageBuilderTest : FinTsTestBase() {
|
|||
// given
|
||||
val getTransactionsJob = RetrieveAccountTransactionsParameters(JobParameters(CustomerSegmentId.AccountTransactionsMt940.id, 1, 1, null, "HIKAZS:73:5"), 180, true, false)
|
||||
bank.supportedJobs = listOf(getTransactionsJob)
|
||||
bank.pinInfo = PinInfo(getTransactionsJob, null, null, null, null, null, listOf(JobTanConfiguration(CustomerSegmentId.AccountTransactionsMt940.id, true)))
|
||||
bank.jobsRequiringTan = setOf(CustomerSegmentId.AccountTransactionsMt940.id)
|
||||
val account = AccountData(CustomerId, null, BankCountryCode, BankCode, null, CustomerId, AccountType.Girokonto, "EUR", "", null, null, listOf(getTransactionsJob.jobName), listOf(getTransactionsJob))
|
||||
bank.addAccount(account)
|
||||
|
||||
|
@ -198,7 +198,7 @@ class MessageBuilderTest : FinTsTestBase() {
|
|||
// given
|
||||
val getTransactionsJob = RetrieveAccountTransactionsParameters(JobParameters(CustomerSegmentId.AccountTransactionsMt940.id, 1, 1, null, "HIKAZS:73:5"), 180, true, false)
|
||||
bank.supportedJobs = listOf(getTransactionsJob)
|
||||
bank.pinInfo = PinInfo(getTransactionsJob, null, null, null, null, null, listOf(JobTanConfiguration(CustomerSegmentId.AccountTransactionsMt940.id, true)))
|
||||
bank.jobsRequiringTan = setOf(CustomerSegmentId.AccountTransactionsMt940.id)
|
||||
val account = AccountData(CustomerId, null, BankCountryCode, BankCode, null, CustomerId, AccountType.Girokonto, "EUR", "", null, null, listOf(getTransactionsJob.jobName), listOf(getTransactionsJob))
|
||||
bank.addAccount(account)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package net.codinux.banking.fints.messages.segmente.implementierte.tan
|
|||
|
||||
import net.codinux.banking.fints.FinTsTestBase
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMediumKlasse
|
||||
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMediumStatus
|
||||
import net.codinux.banking.fints.response.segments.ChangeTanMediaParameters
|
||||
|
@ -25,7 +26,7 @@ class TanGeneratorTanMediumAnOderUmmeldenTest: FinTsTestBase() {
|
|||
|
||||
private const val SegmentNumber = 3
|
||||
|
||||
private val NewActiveTanMedium = TanGeneratorTanMedium(TanMediumKlasse.TanGenerator, TanMediumStatus.Verfuegbar, CardNumber, CardSequenceNumber, CardType, null, null, "EC-Card")
|
||||
private val NewActiveTanMedium = TanMedium(TanMediumKlasse.TanGenerator, TanMediumStatus.Verfuegbar, "EC-Card", TanGeneratorTanMedium(CardNumber, CardSequenceNumber, CardType, null, null))
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1021,8 +1021,8 @@ class ResponseParserTest : FinTsTestBase() {
|
|||
result.getFirstSegmentById<TanMediaList>(InstituteSegmentId.TanMediaList)?.let { segment ->
|
||||
assertEquals(TanEinsatzOption.KundeKannGenauEinMediumZuEinerZeitNutzen, segment.usageOption)
|
||||
assertContainsExactly(segment.tanMedia,
|
||||
TanGeneratorTanMedium(TanMediumKlasse.TanGenerator, TanMediumStatus.AktivFolgekarte, oldCardNumber, cardSequenceNumber, null, null, null, mediaName),
|
||||
TanGeneratorTanMedium(TanMediumKlasse.TanGenerator, TanMediumStatus.Verfuegbar, cardSequenceNumber, null, null, null, null, mediaName)
|
||||
TanMedium(TanMediumKlasse.TanGenerator, TanMediumStatus.AktivFolgekarte, mediaName, TanGeneratorTanMedium(oldCardNumber, cardSequenceNumber, null, null, null)),
|
||||
TanMedium(TanMediumKlasse.TanGenerator, TanMediumStatus.Verfuegbar, mediaName, TanGeneratorTanMedium(cardSequenceNumber, null, null, null, null))
|
||||
)
|
||||
}
|
||||
?: run { fail("No segment of type TanMediaList found in ${result.receivedSegments}") }
|
||||
|
@ -1226,6 +1226,24 @@ class ResponseParserTest : FinTsTestBase() {
|
|||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun parseAccountTransactionsCamtParameters() {
|
||||
val result = underTest.parse("HICAZS:56:1:3+1+1+1+740:N:N:urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.02:urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.08'")
|
||||
|
||||
// then
|
||||
assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountTransactionsCamtParameters, 56, 1, 3)
|
||||
|
||||
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.AccountTransactionsCamtParameters)?.let { segment ->
|
||||
assertEquals(740, segment.serverTransactionsRetentionDays)
|
||||
assertFalse(segment.settingCountEntriesAllowed)
|
||||
assertFalse(segment.settingAllAccountAllowed)
|
||||
|
||||
assertContainsExactly(segment.supportedCamtDataFormats, "urn:iso:std:iso:20022:tech:xsd:camt.052.001.02", "urn:iso:std:iso:20022:tech:xsd:camt.052.001.08")
|
||||
}
|
||||
?: run { fail("No segment of type AccountTransactionsCamtParameters found in ${result.receivedSegments}") }
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun parseCreditCardAccountTransactions() {
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
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.banking.fints.test.TestDataGenerator
|
||||
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 BankDataSerializerTest {
|
||||
|
||||
private val serializedBankData = TestDataGenerator.serializedBankData
|
||||
|
||||
private val json = Json {
|
||||
prettyPrint = true
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun serializeToJson() {
|
||||
val bank = TestDataGenerator.generateBankDataForSerialization()
|
||||
|
||||
val result = json.encodeToString(bank)
|
||||
|
||||
assertEquals(serializedBankData, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserializeFromJson() {
|
||||
val result = json.decodeFromString<BankData>(serializedBankData)
|
||||
|
||||
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(serializedBankData, json.encodeToString(result))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package net.codinux.banking.fints.serialization
|
||||
|
||||
import net.codinux.banking.fints.test.TestDataGenerator
|
||||
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 {
|
||||
|
||||
private val serializedBankData = TestDataGenerator.serializedBankData
|
||||
|
||||
|
||||
@Test
|
||||
fun serializeToJson() {
|
||||
val bank = TestDataGenerator.generateBankDataForSerialization()
|
||||
|
||||
val result = FinTsModelSerializer.serializeToJson(bank, true)
|
||||
|
||||
assertEquals(serializedBankData, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserializeFromJson() {
|
||||
val result = FinTsModelSerializer.deserializeFromJson(serializedBankData)
|
||||
|
||||
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(serializedBankData, FinTsModelSerializer.serializeToJson(result, true))
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,539 @@
|
|||
package net.codinux.banking.fints.test
|
||||
|
||||
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
|
||||
|
||||
object TestDataGenerator {
|
||||
|
||||
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
|
||||
|
||||
|
||||
fun generateBankDataForSerialization(): 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val serializedBankData = """
|
||||
{
|
||||
"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()
|
||||
|
||||
}
|
|
@ -22,8 +22,19 @@ class Mt535ParserTest {
|
|||
|
||||
assertSize(2, statement.holdings)
|
||||
|
||||
assertHolding(statement.holdings.first(), "MUL AMUN MSCI WLD ETF ACC MUL Amundi MSCI World V", "LU1781541179", null, LocalDate(2024, 6, 3), 1693, "16,828250257", "18531,08")
|
||||
assertHolding(statement.holdings[1], "NVIDIA CORP. DL-,001 NVIDIA Corp.", "US67066G1040", null, LocalDate(2024, 8, 5), 214, "92,86", "22846,64")
|
||||
assertHolding(statement.holdings.first(), "MUL AMUN MSCI WLD ETF ACC MUL Amundi MSCI World V", "LU1781541179", null, LocalDate(2024, 6, 3), 1693.0, "16,828250257", "18531,08")
|
||||
assertHolding(statement.holdings[1], "NVIDIA CORP. DL-,001 NVIDIA Corp.", "US67066G1040", null, LocalDate(2024, 8, 5), 214.0, "92,86", "22846,64")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun quantityIsFloatingPointNumber() {
|
||||
val result = underTest.parseMt535String(QuantityIsFloatingPointNumber)
|
||||
|
||||
val statement = assertStatement(result, "70033100", "0123456789", "21480,38", LocalDate(2024, 10, 15), LocalDate(2024, 10, 15))
|
||||
|
||||
assertSize(1, statement.holdings)
|
||||
|
||||
assertHolding(statement.holdings.first(), "SAP SE O.N. SAP SE", "DE0007164600", null, LocalDate(2024, 10, 4), 100.446, "199,090257451", "21480,38")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -46,8 +57,8 @@ class Mt535ParserTest {
|
|||
|
||||
assertSize(3, statement.holdings)
|
||||
|
||||
assertHolding(statement.holdings.first(), "/DE/123456 Mustermann AG, Stammaktien", "DE0123456789", null, LocalDate(1999, 8, 15), 100, "68,5", "5270,")
|
||||
assertHolding(statement.holdings[1], "/DE/123457 Mustermann AG, Vorzugsaktien", "DE0123456790", null, LocalDate(1998, 10, 13), 100, "42,75", "5460,")
|
||||
assertHolding(statement.holdings.first(), "/DE/123456 Mustermann AG, Stammaktien", "DE0123456789", null, LocalDate(1999, 8, 15), 100.0, "68,5", "5270,")
|
||||
assertHolding(statement.holdings[1], "/DE/123457 Mustermann AG, Vorzugsaktien", "DE0123456790", null, LocalDate(1998, 10, 13), 100.0, "42,75", "5460,")
|
||||
// TODO: these values are not correct. Implement taking foreign currencies into account to fix this
|
||||
assertHolding(statement.holdings[2], "Australian Domestic Bonds 1993 (2003) Ser. 10", "AU9876543210", null, LocalDate(1999, 3, 15), null, "31", "6294,65")
|
||||
}
|
||||
|
@ -71,7 +82,7 @@ class Mt535ParserTest {
|
|||
return statement
|
||||
}
|
||||
|
||||
private fun assertHolding(holding: Holding, name: String, isin: String?, wkn: String?, buyingDate: LocalDate?, quantity: Int?, averagePrice: String?, balance: String?, currency: String? = "EUR") {
|
||||
private fun assertHolding(holding: Holding, name: String, isin: String?, wkn: String?, buyingDate: LocalDate?, quantity: Double?, averagePrice: String?, balance: String?, currency: String? = "EUR") {
|
||||
assertEquals(name, holding.name)
|
||||
|
||||
assertEquals(isin, holding.isin)
|
||||
|
@ -140,6 +151,41 @@ class Mt535ParserTest {
|
|||
-
|
||||
""".trimIndent()
|
||||
|
||||
private val QuantityIsFloatingPointNumber = """
|
||||
:16R:GENL
|
||||
:28E:1/ONLY
|
||||
:20C::SEME//NONREF
|
||||
:23G:NEWM
|
||||
:98A::PREP//20241015
|
||||
:98A::STAT//20241015
|
||||
:22F::STTY//CUST
|
||||
:97A::SAFE//70033100/0123456789
|
||||
:17B::ACTI//Y
|
||||
:16S:GENL
|
||||
|
||||
:16R:FIN
|
||||
:35B:ISIN DE0007164600
|
||||
SAP SE O.N.
|
||||
SAP SE
|
||||
:93B::AGGR//UNIT/100,446
|
||||
:16R:SUBBAL
|
||||
:93C::TAVI//UNIT/AVAI/100,446
|
||||
:70C::SUBB//1 SAP SE O.N.
|
||||
2
|
||||
3 EDE 213.850000000EUR 2024-10-15T09:03:25.4
|
||||
4 19997.82EUR DE0007164600, 1/SHS
|
||||
:16S:SUBBAL
|
||||
:19A::HOLD//EUR21480,38
|
||||
:70E::HOLD//1STK++++20241004+
|
||||
2199,090257451+EUR
|
||||
:16S:FIN
|
||||
|
||||
:16R:ADDINFO
|
||||
:19A::HOLP//EUR21480,38
|
||||
:16S:ADDINFO
|
||||
-'
|
||||
""".trimIndent()
|
||||
|
||||
/**
|
||||
* See Anlage_3_Datenformate_V3.8, S. 317ff
|
||||
*
|
||||
|
|
|
@ -31,7 +31,7 @@ class ResponseParserTestJvm : FinTsTestBaseJvm() {
|
|||
|
||||
val decodedChallengeHhdUc = TanImageDecoder().decodeChallenge(result.tanResponse?.challengeHHD_UC ?: "")
|
||||
assertThat(decodedChallengeHhdUc.decodingSuccessful).isTrue()
|
||||
assertThat(decodedChallengeHhdUc.imageBytes.size).isEqualTo(3664)
|
||||
assertThat(decodedChallengeHhdUc.imageBytes?.size).isEqualTo(3664)
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue