diff options
12 files changed, 214 insertions, 52 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt index a63e6503..0886e177 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -37,6 +37,7 @@ import startServer import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData import tech.libeufin.nexus.iso20022.createPain001document import tech.libeufin.nexus.iso20022.parseCamtMessage +import tech.libeufin.nexus.server.EbicsDialects import tech.libeufin.nexus.server.client import tech.libeufin.nexus.server.nexusApp import tech.libeufin.util.* @@ -137,15 +138,15 @@ class ParseCamt : CliktCommand("Parse camt.05x file, outputs JSON in libEufin in private val logLevel by option( help = "Set the log level to: 'off', 'error', 'warn', 'info', 'debug', 'trace', 'all'" ) - private val withC54 by option( - help = "Treats the input as camt.054. Without this option, the" + - " parser expects a camt.052 or camt.053 and handles them equally." + private val withPfDialect by option( + help = "Set the dialect to 'pf' (PostFinance). If not given, it defaults to GLS." ).flag(default = false) private val filename by argument("FILENAME", "File in CAMT format") override fun run() { setLogLevel(logLevel) val camtText = File(filename).readText(Charsets.UTF_8) - val res = parseCamtMessage(XMLUtil.parseStringIntoDom(camtText)) + val dialect = if (withPfDialect) EbicsDialects.POSTFINANCE.dialectName else null + val res = parseCamtMessage(XMLUtil.parseStringIntoDom(camtText), dialect) println(jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(res)) } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt index a1576a05..8f1f6ca5 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt @@ -29,7 +29,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.nexus.* import tech.libeufin.nexus.iso20022.* import tech.libeufin.nexus.server.* -import tech.libeufin.nexus.xlibeufinbank.processXLibeufinBankMessage +import tech.libeufin.nexus.xlibeufinbank.ingestXLibeufinBankMessage import tech.libeufin.util.XMLUtil import tech.libeufin.util.internalServerError import java.time.Instant @@ -204,9 +204,13 @@ fun ingestBankMessagesIntoAccount( }.orderBy( Pair(NexusBankMessagesTable.id, SortOrder.ASC) ).forEach { - val processingResult: IngestedTransactionsCount = when(BankConnectionType.parseBankConnectionType(conn.type)) { + val ingestionResult: IngestedTransactionsCount = when(BankConnectionType.parseBankConnectionType(conn.type)) { BankConnectionType.EBICS -> { val camtString = it.message.bytes.toString(Charsets.UTF_8) + /** + * NOT validating _again_ the camt document because it was + * already validate before being stored into the database. + */ val doc = XMLUtil.parseStringIntoDom(camtString) /** * Calling the CaMt handler. After its return, all the Neuxs-meaningful @@ -214,7 +218,7 @@ fun ingestBankMessagesIntoAccount( * processed by any facade OR simply be communicated to the CLI via JSON. */ try { - processCamtMessage( + ingestCamtMessageIntoAccount( bankAccountId, doc, it.fetchLevel, @@ -234,7 +238,7 @@ fun ingestBankMessagesIntoAccount( " be parsed into JSON by the x-libeufin-bank ingestion.") throw internalServerError("Could not ingest x-libeufin-bank messages.") } - processXLibeufinBankMessage( + ingestXLibeufinBankMessage( bankAccountId, jMessage ) @@ -246,13 +250,13 @@ fun ingestBankMessagesIntoAccount( * (1) flagged, (2) skipped when this function will run again, and (3) * NEVER deleted from the database. */ - if (processingResult.newTransactions == -1) { + if (ingestionResult.newTransactions == -1) { it.errors = true lastId = it.id.value return@forEach } - totalNew += processingResult.newTransactions - downloadedTransactions += processingResult.downloadedTransactions + totalNew += ingestionResult.newTransactions + downloadedTransactions += ingestionResult.downloadedTransactions /** * Disk-space conservative check: only store if "yes" was * explicitly set into the environment variable. Any other diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt index 382aefc8..0d3aa67b 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt @@ -78,7 +78,14 @@ private suspend inline fun HttpClient.postToBank(url: String, body: String): Str sealed class EbicsDownloadResult class EbicsDownloadSuccessResult( - val orderData: ByteArray + val orderData: ByteArray, + /** + * This value points at the EBICS transaction that carried + * the order data contained in this structure. That makes + * possible to log the EBICS transaction that carried one + * invalid order data, for example. + */ + val transactionID: String? = null ) : EbicsDownloadResult() class EbicsDownloadEmptyResult( @@ -244,7 +251,7 @@ suspend fun doEbicsDownloadTransaction( } } logger.debug("Bank acknowledges EBICS download receipt. Transaction ID: $transactionID.") - return EbicsDownloadSuccessResult(respPayload) + return EbicsDownloadSuccessResult(respPayload, transactionID) } // Currently only 1-segment requests. diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt index e84e9846..50578894 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -90,18 +90,31 @@ private fun getFetchLevelFromEbicsOrder(ebicsHistoryType: String): FetchLevel { } } -fun storeCamt( +// Validate and store the received document for later ingestion. +private fun validateAndStoreCamt( bankConnectionId: String, camt: String, - fetchLevel: FetchLevel + fetchLevel: FetchLevel, + transactionID: String? = null // the EBICS transaction that carried this camt. ) { - val camt53doc = XMLUtil.parseStringIntoDom(camt) - val msgId = camt53doc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId") + val camtDoc = try { + XMLUtil.parseStringIntoDom(camt) + } + catch (e: Exception) { + throw badGateway("Could not parse camt document from EBICS transaction $transactionID") + } + if (!XMLUtil.validateFromDom(camtDoc)) + throw badGateway("Camt document from EBICS transaction $transactionID is invalid") + + val msgId = camtDoc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId") logger.info("Camt document '$msgId' received via $fetchLevel.") transaction { val conn = NexusBankConnectionEntity.findByName(bankConnectionId) if (conn == null) { - throw NexusError(HttpStatusCode.InternalServerError, "bank connection missing") + throw NexusError( + HttpStatusCode.InternalServerError, + "bank connection missing" + ) } val oldMsg = NexusBankMessageEntity.find { NexusBankMessagesTable.messageId eq msgId }.firstOrNull() if (oldMsg == null) { @@ -164,10 +177,11 @@ private suspend fun fetchEbicsC5x( is EbicsDownloadSuccessResult -> { response.orderData.unzipWithLambda { // logger.debug("Camt entry (filename (in the Zip archive): ${it.first}): ${it.second}") - storeCamt( + validateAndStoreCamt( bankConnectionId, it.second, - getFetchLevelFromEbicsOrder(historyType) + getFetchLevelFromEbicsOrder(historyType), + transactionID = response.transactionID ) } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt index b11818f9..14e24485 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt @@ -30,6 +30,7 @@ import CashAccount import CreditDebitIndicator import CurrencyAmount import CurrencyExchange +import EntryStatus import GenericId import OrganizationIdentification import PartyIdentification @@ -581,7 +582,7 @@ private fun XmlElementDestructor.extractInnerBkTxCd(creditDebitIndicator: Credit return "XTND-NTAV-NTAV" } -private fun XmlElementDestructor.extractInnerTransactions(): CamtReport { +private fun XmlElementDestructor.extractInnerTransactions(dialect: String? = null): CamtReport { val account = requireUniqueChildNamed("Acct") { extractAccount() } val balances = mapEachChildNamed("Bal") { @@ -613,8 +614,16 @@ private fun XmlElementDestructor.extractInnerTransactions(): CamtReport { // multiple money transactions *within* one Ntry element. val entries = mapEachChildNamed("Ntry") { val amount = extractCurrencyAmount() - val status = requireUniqueChildNamed("Sts") { focusElement.textContent }.let { - EntryStatus.valueOf(it) + val status = requireUniqueChildNamed("Sts") { + val textContent = if (dialect == EbicsDialects.POSTFINANCE.dialectName) { + requireUniqueChildNamed("Cd") { + focusElement.textContent + } + } else + focusElement.textContent + textContent.let { + EntryStatus.valueOf(it) + } } val creditDebitIndicator = requireUniqueChildNamed("CdtDbtInd") { focusElement.textContent }.let { CreditDebitIndicator.valueOf(it) @@ -677,7 +686,7 @@ private fun XmlElementDestructor.extractInnerTransactions(): CamtReport { * Extract a list of transactions from * an ISO20022 camt.052 / camt.053 message. */ -fun parseCamtMessage(doc: Document): CamtParseResult { +fun parseCamtMessage(doc: Document, dialect: String? = null): CamtParseResult { return destructXml(doc) { requireRootElement("Document") { // Either bank to customer statement or report @@ -685,17 +694,17 @@ fun parseCamtMessage(doc: Document): CamtParseResult { when (focusElement.localName) { "BkToCstmrAcctRpt" -> { mapEachChildNamed("Rpt") { - extractInnerTransactions() + extractInnerTransactions(dialect) } } "BkToCstmrStmt" -> { mapEachChildNamed("Stmt") { - extractInnerTransactions() + extractInnerTransactions(dialect) } } "BkToCstmrDbtCdtNtfctn" -> { mapEachChildNamed("Ntfctn") { - extractInnerTransactions() + extractInnerTransactions(dialect) } } else -> { @@ -846,7 +855,7 @@ fun extractPaymentUidFromSingleton( * case of DBIT transaction. * - returns a IngestedTransactionCount object. */ -fun processCamtMessage( +fun ingestCamtMessageIntoAccount( bankAccountId: String, camtDoc: Document, fetchLevel: FetchLevel, @@ -866,7 +875,7 @@ fun processCamtMessage( if (acct == null) { throw NexusError(HttpStatusCode.NotFound, "user not found") } - val res = try { parseCamtMessage(camtDoc) } catch (e: CamtParsingError) { + val res = try { parseCamtMessage(camtDoc, dialect) } catch (e: CamtParsingError) { logger.warn("Invalid CAMT received from bank: ${e.message}") newTransactions = -1 return@transaction diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt index fca81b51..21614fbe 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt @@ -44,7 +44,7 @@ import org.slf4j.event.Level import tech.libeufin.nexus.* import tech.libeufin.nexus.bankaccount.* import tech.libeufin.nexus.ebics.* -import tech.libeufin.nexus.iso20022.processCamtMessage +import tech.libeufin.nexus.iso20022.ingestCamtMessageIntoAccount import tech.libeufin.util.* import java.net.URLEncoder @@ -437,7 +437,7 @@ val nexusApp: Application.() -> Unit = { defaultConn.dialect } else null val msgType = ensureNonNull(call.parameters["type"]) - processCamtMessage( + ingestCamtMessageIntoAccount( ensureNonNull(accountId), XMLUtil.parseStringIntoDom(call.receiveText()), when(msgType) { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt index bfa52208..cd37862a 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt @@ -19,12 +19,10 @@ import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.util.* import io.ktor.util.* -import io.ktor.util.date.* import org.jetbrains.exposed.sql.statements.api.ExposedBlob import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.nexus.* import tech.libeufin.nexus.bankaccount.* -import tech.libeufin.nexus.iso20022.* import tech.libeufin.nexus.server.* import tech.libeufin.util.* import java.net.MalformedURLException @@ -328,7 +326,7 @@ class XlibeufinBankConnectionProtocol : BankConnectionProtocol { * status, since Sandbox has only one (unnamed) transaction state and * all transactions are asked as reports. */ -fun processXLibeufinBankMessage( +fun ingestXLibeufinBankMessage( bankAccountId: String, data: JsonNode ): IngestedTransactionsCount { diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt index 58777cba..bc5d7379 100644 --- a/nexus/src/test/kotlin/Iso20022Test.kt +++ b/nexus/src/test/kotlin/Iso20022Test.kt @@ -9,7 +9,7 @@ import org.junit.Ignore import org.junit.Test import org.w3c.dom.Document import poFiCamt052 -import poFiCamt054 +import poFiCamt054_2019 import prepNexusDb import tech.libeufin.nexus.bankaccount.getBankAccount import tech.libeufin.nexus.iso20022.* @@ -21,13 +21,6 @@ import tech.libeufin.util.DestructionError import tech.libeufin.util.XMLUtil import tech.libeufin.util.destructXml import withTestDatabase -import java.math.BigDecimal -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.util.TimeZone import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -125,15 +118,15 @@ class Iso20022Test { @Test fun parsePoFiCamt054() { - val doc = XMLUtil.parseStringIntoDom(poFiCamt054) - parseCamtMessage(doc) + val doc = XMLUtil.parseStringIntoDom(poFiCamt054_2019) + parseCamtMessage(doc, dialect = "pf") } @Test fun ingestPoFiCamt054() { - val doc = XMLUtil.parseStringIntoDom(poFiCamt054) + val doc = XMLUtil.parseStringIntoDom(poFiCamt054_2019) withTestDatabase { prepNexusDb() - processCamtMessage( + ingestCamtMessageIntoAccount( "foo", doc, FetchLevel.NOTIFICATION, diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt index c64d20e8..f3c71a11 100644 --- a/nexus/src/test/kotlin/MakeEnv.kt +++ b/nexus/src/test/kotlin/MakeEnv.kt @@ -6,7 +6,6 @@ import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.nexus.* import tech.libeufin.nexus.dbCreateTables import tech.libeufin.nexus.dbDropTables -import tech.libeufin.nexus.iso20022.* import tech.libeufin.nexus.server.BankConnectionType import tech.libeufin.nexus.server.FetchLevel import tech.libeufin.nexus.server.FetchSpecAllJson @@ -495,7 +494,143 @@ fun genNexusIncomingCamt( ) ) -val poFiCamt054: String = """ +// Comes from a "mit Sammelbuchung" sample. +// "mit Einzelbuchung" sample didn't have the "Ustrd" +// See: https://www.postfinance.ch/de/support/services/dokumente/musterfiles-fuer-geschaeftskunden.html +val poFiCamt054_2019: String = """ +<?xml version="1.0" encoding="UTF-8"?> +<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.054.001.08" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.054.001.08 file:///C:/Users/burkhalterl/Documents/Musterfiles%20ISOV19/Schemen/camt.054.001.08.xsd"> + <BkToCstmrDbtCdtNtfctn> + <GrpHdr> + <MsgId>20200618375204295372463</MsgId> + <CreDtTm>2022-03-08T23:31:31</CreDtTm> + <MsgPgntn> + <PgNb>1</PgNb> + <LastPgInd>true</LastPgInd> + </MsgPgntn> + <AddtlInf>SPS/2.0/PROD</AddtlInf> + </GrpHdr> + <Ntfctn> + <Id>20200618375204295372465</Id> + <CreDtTm>2022-03-08T23:31:31</CreDtTm> + <FrToDt> + <FrDtTm>2022-03-08T00:00:00</FrDtTm> + <ToDtTm>2022-03-08T23:59:59</ToDtTm> + </FrToDt> + <Acct> + <Id> + <IBAN>${FOO_USER_IBAN}</IBAN> + </Id> + <Ccy>CHF</Ccy> + <Ownr> + <Nm>Robert Schneider SA Grands magasins Biel/Bienne</Nm> + </Ownr> + </Acct> + <Ntry> + <NtryRef>CH2909000000250094239</NtryRef> + <Amt Ccy="CHF">501.05</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <RvslInd>false</RvslInd> + <Sts> + <Cd>BOOK</Cd> + </Sts> + <BookgDt> + <Dt>2022-03-08</Dt> + </BookgDt> + <ValDt> + <Dt>2022-03-08</Dt> + </ValDt> + <AcctSvcrRef>1000000000000000</AcctSvcrRef> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>RCDT</Cd> + <SubFmlyCd>AUTT</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <NtryDtls> + <Btch> + <NbOfTxs>1</NbOfTxs> + </Btch> + <TxDtls> + <Refs> + <AcctSvcrRef>2000000000000000</AcctSvcrRef> + <InstrId>1006265-25bbb3b1a</InstrId> + <EndToEndId>NOTPROVIDED</EndToEndId> + <UETR>b009c997-97b3-4a9c-803c-d645a7276b0</UETR> + <Prtry> + <Tp>00</Tp> + <Ref>00000000000000000000020</Ref> + </Prtry> + </Refs> + <Amt Ccy="CHF">501.05</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>RCDT</Cd> + <SubFmlyCd>AUTT</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <RltdPties> + <Dbtr> + <Pty> + <Nm>Bernasconi Maria</Nm> + <PstlAdr> + <AdrLine>Place de la Gare 12</AdrLine> + <AdrLine>2502 Biel/Bienne</AdrLine> + </PstlAdr> + </Pty> + </Dbtr> + <DbtrAcct> + <Id> + <IBAN>CH5109000000250092291</IBAN> + </Id> + </DbtrAcct> + <CdtrAcct> + <Id> + <IBAN>CH2909000000250094239</IBAN> + </Id> + </CdtrAcct> + </RltdPties> + <RltdAgts> + <DbtrAgt> + <FinInstnId> + <BICFI>POFICHBEXXX</BICFI> + <Nm>POSTFINANCE AG</Nm> + <PstlAdr> + <AdrLine>MINGERSTRASSE , 20</AdrLine> + <AdrLine>3030 BERN</AdrLine> + </PstlAdr> + </FinInstnId> + </DbtrAgt> + </RltdAgts> + <RmtInf> + <Ustrd>Muster</Ustrd> + <Ustrd> Musterfile</Ustrd> + <Strd> + <AddtlRmtInf>?REJECT?0</AddtlRmtInf> + <AddtlRmtInf>?ERROR?000</AddtlRmtInf> + </Strd> + </RmtInf> + <RltdDts> + <AccptncDtTm>2022-03-08T20:00:00</AccptncDtTm> + </RltdDts> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>SAMMELGUTSCHRIFT FÜR KONTO: CH2909000000250094239 VERARBEITUNG VOM 08.03.2022 PAKET ID: 200000000000XXX</AddtlNtryInf> + </Ntry> + </Ntfctn> + </BkToCstmrDbtCdtNtfctn> +</Document> + +""".trimIndent() + +val poFiCamt054_2013: String = """ <?xml version="1.0" encoding="UTF-8"?> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.054.001.04" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.054.001.04 camt.054.001.04.xsd"> <BkToCstmrDbtCdtNtfctn> diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt index d79d0ab2..fc240963 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -1017,8 +1017,8 @@ private fun ensureEbicsHost(requestHostID: String): EbicsHostPublicInfo { } fun receiveEbicsXmlInternal(xmlData: String): Document { // logger.debug("Data received: $xmlData") - val requestDocument: Document? = XMLUtil.parseStringIntoDom(xmlData) - if (requestDocument == null || (!XMLUtil.validateFromDom(requestDocument))) { + val requestDocument: Document = XMLUtil.parseStringIntoDom(xmlData) + if (!XMLUtil.validateFromDom(requestDocument)) { println("Problematic document was: $requestDocument") throw EbicsInvalidXmlError() } diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt index 737039a6..da814d5d 100644 --- a/util/src/main/kotlin/Ebics.kt +++ b/util/src/main/kotlin/Ebics.kt @@ -264,7 +264,6 @@ fun createEbicsRequestForUploadInitialization( return XMLUtil.convertDomToString(doc) } - fun createEbicsRequestForDownloadInitialization( subscriberDetails: EbicsClientSubscriberDetails, orderType: String, diff --git a/util/src/main/kotlin/XMLUtil.kt b/util/src/main/kotlin/XMLUtil.kt index e9074cb4..aa96e00f 100644 --- a/util/src/main/kotlin/XMLUtil.kt +++ b/util/src/main/kotlin/XMLUtil.kt @@ -225,6 +225,8 @@ class XMLUtil private constructor() { } } } + + // FIXME: need here the "2019" Swiss versions of camt and pain. val schemaInputs: Array<Source> = listOf( "xsd/ebics_H004.xsd", "xsd/ebics_hev.xsd", @@ -232,7 +234,7 @@ class XMLUtil private constructor() { "xsd/camt.053.001.02.xsd", "xsd/camt.054.001.02.xsd", "xsd/pain.001.001.03.xsd", - "xsd/pain.001.001.03.ch.02.xsd" + "xsd/pain.001.001.03.ch.02.xsd" // Swiss 2013 version. ).map { val stream = classLoader.getResourceAsStream(it) ?: throw FileNotFoundException("Schema file $it not found.") |