diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/MessageBuilder.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/MessageBuilder.kt index dad92178..6088e36b 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/MessageBuilder.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/MessageBuilder.kt @@ -15,6 +15,8 @@ import net.dankito.fints.messages.segmente.implementierte.umsaetze.Kontoumsaetze import net.dankito.fints.messages.segmente.implementierte.umsaetze.KontoumsaetzeZeitraumMt940Version7 import net.dankito.fints.messages.segmente.implementierte.umsaetze.Saldenabfrage import net.dankito.fints.model.* +import net.dankito.fints.response.segments.JobParameters +import net.dankito.fints.response.segments.SepaAccountInfoParameters import net.dankito.fints.util.FinTsUtils import kotlin.random.Random @@ -97,7 +99,7 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg open fun createGetTransactionsMessage(parameter: GetTransactionsParameter, bank: BankData, customer: CustomerData, product: ProductData, dialogData: DialogData): MessageBuilderResult { - val result = getSupportedVersionOfJob(CustomerSegmentId.AccountTransactionsMt940, customer, listOf(5, 6, 7)) + val result = getSupportedVersionsOfJob(CustomerSegmentId.AccountTransactionsMt940, customer, listOf(5, 6, 7)) if (result.isJobVersionSupported) { val transactionsJob = if (result.isAllowed(7)) KontoumsaetzeZeitraumMt940Version7(generator.resetSegmentNumber(2), parameter, bank, customer) @@ -116,7 +118,7 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg open fun createGetBalanceMessage(bank: BankData, customer: CustomerData, product: ProductData, dialogData: DialogData): MessageBuilderResult { - val result = getSupportedVersionOfJob(CustomerSegmentId.Balance, customer, listOf(5)) + val result = getSupportedVersionsOfJob(CustomerSegmentId.Balance, customer, listOf(5)) if (result.isJobVersionSupported) { return MessageBuilderResult(createSignedMessage(bank, customer, dialogData, listOf( @@ -131,14 +133,18 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg open fun createBankTransferMessage(bankTransferData: BankTransferData, bank: BankData, customer: CustomerData, dialogData: DialogData): MessageBuilderResult { - val result = getSupportedVersionOfJob(CustomerSegmentId.SepaBankTransfer, customer, listOf(1)) + val result = getSupportedVersionsOfJob(CustomerSegmentId.SepaBankTransfer, customer, listOf(1)) if (result.isJobVersionSupported) { - return MessageBuilderResult(createSignedMessage(bank, customer, dialogData, listOf( - SepaEinzelueberweisung(generator.resetSegmentNumber(2), customer, bank.bic!!, bankTransferData), - ZweiSchrittTanEinreichung(generator.getNextSegmentNumber(), TanProcess.TanProcess4, CustomerSegmentId.SepaBankTransfer) - ))) + getSepaUrnFor(CustomerSegmentId.SepaAccountInfoParameters, customer, "pain.001.001.03")?.let { urn -> + return MessageBuilderResult(createSignedMessage(bank, customer, dialogData, listOf( + SepaEinzelueberweisung(generator.resetSegmentNumber(2), urn, customer, bank.bic!!, bankTransferData), // TODO: get rid of '!!' + ZweiSchrittTanEinreichung(generator.getNextSegmentNumber(), TanProcess.TanProcess4, CustomerSegmentId.SepaBankTransfer) + ))) + } + + return MessageBuilderResult(true, false, result.allowedVersions, result.supportedVersions, null) // TODO: how to tell that we don't support required SEPA pain version? } return result @@ -217,11 +223,13 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg } - protected open fun getSupportedVersionOfJob(segmentId: CustomerSegmentId, customer: CustomerData, - supportedVersions: List): MessageBuilderResult { + protected open fun getSupportedVersionsOfJob(segmentId: CustomerSegmentId, customer: CustomerData, + supportedVersions: List): MessageBuilderResult { - customer.accounts.firstOrNull()?.let { account -> // TODO: find a better solution / make more generic - val allowedVersions = account.allowedJobs.filter { it.jobName == segmentId.id } + val allowedJobs = getAllowedJobs(segmentId, customer) + + if (allowedJobs.isNotEmpty()) { + val allowedVersions = allowedJobs .map { it.segmentVersion } .sortedDescending() @@ -232,4 +240,22 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg return MessageBuilderResult(false) } + protected open fun getSepaUrnFor(segmentId: CustomerSegmentId, customer: CustomerData, sepaDataFormat: String): String? { + + return getAllowedJobs(segmentId, customer) + .filterIsInstance() + .sortedByDescending { it.segmentVersion } + .flatMap { it.supportedSepaFormats } + .firstOrNull { it.contains(sepaDataFormat) } + } + + protected open fun getAllowedJobs(segmentId: CustomerSegmentId, customer: CustomerData): List { + + customer.accounts.firstOrNull()?.let { account -> // TODO: find a better solution / make more generic + return account.allowedJobs.filter { it.jobName == segmentId.id } + } + + return listOf() + } + } \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/id/CustomerSegmentId.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/id/CustomerSegmentId.kt index 125cad2c..8eee646c 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/id/CustomerSegmentId.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/id/CustomerSegmentId.kt @@ -17,6 +17,8 @@ enum class CustomerSegmentId(override val id: String) : ISegmentId { AccountTransactionsMt940("HKKAZ"), - SepaBankTransfer("HKCCS") + SepaBankTransfer("HKCCS"), + + SepaAccountInfoParameters("HKSPA") // not implemented, retrieved automatically with UPD } \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisung.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisung.kt index 143e74f0..ea986426 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisung.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisung.kt @@ -7,6 +7,7 @@ import net.dankito.fints.model.CustomerData open class SepaEinzelueberweisung( segmentNumber: Int, + sepaDescriptorUrn: String, debitor: CustomerData, debitorBic: String, data: BankTransferData, @@ -16,7 +17,7 @@ open class SepaEinzelueberweisung( segmentNumber, CustomerSegmentId.SepaBankTransfer, 1, - "urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03", // TODO: read from HISPAS + sepaDescriptorUrn, "pain.001.001.03.xml", data.creditorIban, data.creditorBic, diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt index 7ed585a1..e4344008 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt @@ -23,6 +23,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId { SepaAccountInfo("HISPA"), + SepaAccountInfoParameters("HISPAS"), + TanInfo("HITANS"), Tan("HITAN"), diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt index 827bcce6..5bc234d3 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt @@ -87,6 +87,7 @@ open class ResponseParser @JvmOverloads constructor( InstituteSegmentId.UserParameters.id -> parseUserParameters(segment, dataElementGroups) InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups) InstituteSegmentId.SepaAccountInfo.id -> parseSepaAccountInfo(segment, dataElementGroups) + InstituteSegmentId.SepaAccountInfoParameters.id -> parseSepaAccountInfoParameters(segment, segmentId, dataElementGroups) InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, segmentId, dataElementGroups) InstituteSegmentId.Tan.id -> parseTanResponse(segment, dataElementGroups) @@ -265,6 +266,24 @@ open class ResponseParser @JvmOverloads constructor( ) } + protected open fun parseSepaAccountInfoParameters(segment: String, segmentId: String, dataElementGroups: List): SepaAccountInfoParameters { + val jobParameters = parseJobParameters(segment, segmentId, dataElementGroups) + val segmentVersion = jobParameters.segmentVersion + + val parametersDataElements = getDataElements(dataElementGroups[4]) + val supportedSepaFormatsBeginIndex = if (segmentVersion == 1) 3 else if (segmentVersion == 2) 4 else 5 + + return SepaAccountInfoParameters( + jobParameters, + parseBoolean(parametersDataElements[0]), + parseBoolean(parametersDataElements[1]), + parseBoolean(parametersDataElements[2]), + if (segmentVersion >= 2) parseBoolean(parametersDataElements[3]) else false, + if (segmentVersion >= 3) parseInt(parametersDataElements[4]) else SepaAccountInfoParameters.CountReservedUsageLengthNotSet, + parametersDataElements.subList(supportedSepaFormatsBeginIndex, parametersDataElements.size) + ) + } + protected open fun parseJobParameters(segment: String, segmentId: String, dataElementGroups: List): JobParameters { var jobName = segmentId.substring(0, 5) // cut off last 'S' (which stands for 'parameter') diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/JobParameters.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/JobParameters.kt index 857be2c5..825ab0f8 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/JobParameters.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/JobParameters.kt @@ -11,6 +11,11 @@ open class JobParameters( : ReceivedSegment(segmentString) { + constructor(parameters: JobParameters) + : this(parameters.jobName, parameters.maxCountJobs, parameters.minimumCountSignatures, + parameters.securityClass, parameters.segmentString) + + override fun toString(): String { return "$jobName $segmentVersion" } diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/SepaAccountInfoParameters.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/SepaAccountInfoParameters.kt new file mode 100644 index 00000000..6fa38e22 --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/SepaAccountInfoParameters.kt @@ -0,0 +1,73 @@ +package net.dankito.fints.response.segments + + +/** + * TODO: some translations: + * + * - payee: Zahlungsempfänger + * - note to payee: Verwendungszweck + * - alternative payee: abweichender Zahlungsempfänger + */ + +open class SepaAccountInfoParameters( + parameters: JobParameters, + + /** + * Über das DE „Einzelkontoabruf erlaubt“ legt das Kreditinstitut fest, ob es möglich ist, einzelne + * Kontoverbindungen gezielt abzurufen oder ob nur alle relevanten Konten insgesamt bereitgestellt werden. + */ + val retrieveSingleAccountAllowed: Boolean, + + /** + * Über das DE „Nationale Kontoverbindung erlaubt“ legt das Kreditinstitut fest, ob im Rahmen einer + * SEPA-Kontoverbindung auch die nationalen Elemente Kreditinstitutskennung, Konto-/Depotnummer und + * Unterkontomerkmal zugelassen sind. Bei „N“ dürfen nur IBAN und BIC verwendet werden. + */ + val nationalAccountRelationshipAllowed: Boolean, + + /** + * Über diese Information legt das Kreditinstitut fest, ob bei SEPA-Zahlungsverkehrsinstrumenten die Verwendung + * von strukturierten Verwendungszweckinformationen („StructuredRemittanceInformation“) erlaubt ist oder nicht. + */ + val structuredUsageAllowed: Boolean, + + /** + * Kennzeichen dafür, ob die Belegung des Feldes „Maximale Anzahl Einträge“ im Kundenauftrag zugelassen ist. + * Falls ja, kann das Kundenprodukt die Anzahl der maximal rückzumeldenden Buchungspositionen beschränken. + * + * Über das DE „Eingabe Anzahl Einträge erlaubt“ legt das Kreditinstitut fest, ob es kundenseitig möglich ist, + * bei Aufträgen die Anzahl von Einträgen in derKreditinstitutsantwort zu beschränken. Ist die Option nicht + * zugelassen, gelten die syntaktischen Maximalwerte. + */ + val settingMaxAllowedEntriesAllowed: Boolean, + + /** + * Anzahl der Stellen im SEPA Verwendungszweck (CreditorReferenceInformationSCT, insgesamt 4 x 35 = 140 Stellen), + * die für interne Verwendung – z. B. Andrucken von Datum, Uhrzeit und verwendeter TAN – durch das Institut + * reserviert sind. Diese Stellen dürfen vom Kundenprodukt nicht für andere Zwecke verwendet werden. Die Anzahl + * wird vom Ende des letzten SEPA-Elementes aus gezählt und darf den Wert 35 nicht überschreiten. + */ + val countReservedUsageLength: Int, + + /** + * Dieses DE beschreibt Ort, Name und Version einer SEPA pain message als URN. Die korrekte Bezeichnung des URN + * ist der Anlage 3 des DFÜ-Abkommens zu entnehmen (vgl. [DFÜ-Abkommen]). + * + * Für die pain messages der ersten Generation ("pain.00x.001.0y.xsd") sind weiterhin die bisherigen Regelungen + * (Angabe der URI bzw. "sepade.pain.00x.001.0y.xsd") zugelassen. Bestehende, lauffähige Implementierungen für + * diese erste Schema-Generation müssen somit nicht angepasst werden. + * + * Werden in den Bankparameterdaten eines bestimmten Geschäftsvorfalls explizit unterstützte SEPA-Datenformate + * genannt, so sind die laut HISPAS global mitgeteilten unterstützten SEPA pain messages für den betreffenden + * Geschäftsvorfall nicht relevant. Es gelten lediglich die laut den Bankparameterdaten des Geschäftsvorfalls + * zugelassenen SEPA pain messages. + */ + val supportedSepaFormats: List +) + : JobParameters(parameters) { + + companion object { + const val CountReservedUsageLengthNotSet = 0 + } + +} \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/TanInfo.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/TanInfo.kt index 446c8260..4f82d808 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/TanInfo.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/TanInfo.kt @@ -2,18 +2,7 @@ package net.dankito.fints.response.segments open class TanInfo( - jobName: String, - maxCountJobs: Int, - minimumCountSignatures: Int, - securityClass: Int?, - val tanProcedureParameters: TwoStepTanProcedureParameters, - - segmentString: String + parameters: JobParameters, + val tanProcedureParameters: TwoStepTanProcedureParameters ) - : JobParameters(jobName, maxCountJobs, minimumCountSignatures, securityClass, segmentString) { - - constructor(parameters: JobParameters, tanProcedureParameters: TwoStepTanProcedureParameters) - : this(parameters.jobName, parameters.maxCountJobs, parameters.minimumCountSignatures, - parameters.securityClass, tanProcedureParameters, parameters.segmentString) - -} \ No newline at end of file + : JobParameters(parameters) \ No newline at end of file diff --git a/fints4javaLib/src/test/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisungTest.kt b/fints4javaLib/src/test/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisungTest.kt index 247e5e51..e8568a61 100644 --- a/fints4javaLib/src/test/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisungTest.kt +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisungTest.kt @@ -22,6 +22,7 @@ class SepaEinzelueberweisungTest { val usage = "What should Mahatma Gandhi want with money?" val underTest = SepaEinzelueberweisung(segmentNumber, + "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03", CustomerData("", "", "", debitorName, debitorIban), debitorBic, BankTransferData(creditorName, creditorIban, creditorBic, amount, usage) @@ -34,6 +35,6 @@ class SepaEinzelueberweisungTest { // then assertThat(result).contains(debitorName, debitorIban, debitorBic, creditorName, creditorIban, creditorBic, - amount.toString(), usage) + amount.toString(), usage, "urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03") } } \ No newline at end of file diff --git a/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt b/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt index 483ba75a..88b380a3 100644 --- a/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt @@ -447,6 +447,41 @@ class ResponseParserTest : FinTsTestBase() { ?: run { Assert.fail("No segment of type SepaAccountInfo found in ${result.receivedSegments}") } } + @Test + fun parseSepaAccountInfoParameters() { + + // when + val result = underTest.parse("HISPAS:147:1:3+1+1+1+J:N:N:sepade.pain.001.001.02.xsd:sepade.pain.001.002.02.xsd:sepade.pain.001.002.03.xsd:sepade.pain.008.002.02.xsd: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:148:2:3+1+1+1+J:N:N:N:sepade.pain.001.001.02.xsd:sepade.pain.001.002.02.xsd:sepade.pain.001.002.03.xsd:sepade.pain.008.002.02.xsd: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'") + + // then + assertCouldParseResponse(result) + + val sepaAccountInfoParameters = result.getSegmentsById(InstituteSegmentId.SepaAccountInfoParameters) + assertThat(sepaAccountInfoParameters).hasSize(2) + + assertCouldParseJobParametersSegment(sepaAccountInfoParameters[0], InstituteSegmentId.SepaAccountInfoParameters, 147, 1, 3, "HKSPA", 1, 1, 1) + assertCouldParseJobParametersSegment(sepaAccountInfoParameters[1], InstituteSegmentId.SepaAccountInfoParameters, 148, 2, 3, "HKSPA", 1, 1, 1) + + for (segment in sepaAccountInfoParameters) { + assertThat(segment.retrieveSingleAccountAllowed).isTrue() + assertThat(segment.nationalAccountRelationshipAllowed).isFalse() + assertThat(segment.structuredUsageAllowed).isFalse() + assertThat(segment.settingMaxAllowedEntriesAllowed).isFalse() + assertThat(segment.countReservedUsageLength).isEqualTo(SepaAccountInfoParameters.CountReservedUsageLengthNotSet) + assertThat(segment.supportedSepaFormats).containsExactlyInAnyOrder( + "sepade.pain.001.001.02.xsd", + "sepade.pain.001.002.02.xsd", + "sepade.pain.001.002.03.xsd", + "sepade.pain.008.002.02.xsd", + "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" + ) + } + } + @Test fun parseSupportedJobs() { @@ -666,13 +701,17 @@ class ResponseParserTest : FinTsTestBase() { private fun assertSuccessfullyParsedSegment(result: Response, segmentId: ISegmentId, segmentNumber: Int, segmentVersion: Int, referenceSegmentNumber: Int? = null) { + assertCouldParseResponse(result) + + assertCouldParseSegment(result, segmentId, segmentNumber, segmentVersion, referenceSegmentNumber) + } + + private fun assertCouldParseResponse(result: Response) { assertThat(result.successful).isTrue() assertThat(result.responseContainsErrors).isFalse() assertThat(result.exception).isNull() assertThat(result.errorsToShowToUser).isEmpty() assertThat(result.receivedResponse).isNotNull() - - assertCouldParseSegment(result, segmentId, segmentNumber, segmentVersion, referenceSegmentNumber) } private fun assertCouldParseSegment(result: Response, segmentId: ISegmentId, segmentNumber: Int, @@ -680,6 +719,12 @@ class ResponseParserTest : FinTsTestBase() { val segment = result.getFirstSegmentById(segmentId) + assertCouldParseSegment(segment, segmentId, segmentNumber, segmentVersion, referenceSegmentNumber) + } + + private fun assertCouldParseSegment(segment: ReceivedSegment?, segmentId: ISegmentId, segmentNumber: Int, + segmentVersion: Int, referenceSegmentNumber: Int?) { + assertThat(segment).isNotNull() segment?.let { @@ -690,4 +735,18 @@ class ResponseParserTest : FinTsTestBase() { } } + private fun assertCouldParseJobParametersSegment(segment: JobParameters?, segmentId: ISegmentId, segmentNumber: Int, + segmentVersion: Int, referenceSegmentNumber: Int?, jobName: String, + maxCountJobs: Int, minimumCountSignatures: Int, securityClass: Int?) { + + assertCouldParseSegment(segment, segmentId, segmentNumber, segmentVersion, referenceSegmentNumber) + + segment?.let { + assertThat(segment.jobName).isEqualTo(jobName) + assertThat(segment.maxCountJobs).isEqualTo(maxCountJobs) + assertThat(segment.minimumCountSignatures).isEqualTo(minimumCountSignatures) + assertThat(segment.securityClass).isEqualTo(securityClass) + } + } + } \ No newline at end of file