Implemented parsing SepaAccountInfoParameters (HISPAS) and setting SEPA descriptor URN in SepaEinzelueberweisung
This commit is contained in:
parent
af0e4e923b
commit
c08dd1379a
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -23,6 +23,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
|
|||
|
||||
SepaAccountInfo("HISPA"),
|
||||
|
||||
SepaAccountInfoParameters("HISPAS"),
|
||||
|
||||
TanInfo("HITANS"),
|
||||
|
||||
Tan("HITAN"),
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue