Implemented parsing SepaAccountInfoParameters (HISPAS) and setting SEPA descriptor URN in SepaEinzelueberweisung

This commit is contained in:
dankl 2019-10-20 19:32:47 +02:00 committed by dankito
parent af0e4e923b
commit c08dd1379a
10 changed files with 207 additions and 30 deletions

View File

@ -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<Int>): MessageBuilderResult {
protected open fun getSupportedVersionsOfJob(segmentId: CustomerSegmentId, customer: CustomerData,
supportedVersions: List<Int>): 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<SepaAccountInfoParameters>()
.sortedByDescending { it.segmentVersion }
.flatMap { it.supportedSepaFormats }
.firstOrNull { it.contains(sepaDataFormat) }
}
protected open fun getAllowedJobs(segmentId: CustomerSegmentId, customer: CustomerData): List<JobParameters> {
customer.accounts.firstOrNull()?.let { account -> // TODO: find a better solution / make more generic
return account.allowedJobs.filter { it.jobName == segmentId.id }
}
return listOf()
}
}

View File

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

View File

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

View File

@ -23,6 +23,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
SepaAccountInfo("HISPA"),
SepaAccountInfoParameters("HISPAS"),
TanInfo("HITANS"),
Tan("HITAN"),

View File

@ -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<String>): 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<String>): JobParameters {
var jobName = segmentId.substring(0, 5) // cut off last 'S' (which stands for 'parameter')

View File

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

View File

@ -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<String>
)
: JobParameters(parameters) {
companion object {
const val CountReservedUsageLengthNotSet = 0
}
}

View File

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

View File

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

View File

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