[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated: implement improved ISO20022 camt parsi
From: |
gnunet |
Subject: |
[libeufin] branch master updated: implement improved ISO20022 camt parsing |
Date: |
Thu, 11 Jun 2020 17:59:30 +0200 |
This is an automated email from the git hooks/post-receive script.
dold pushed a commit to branch master
in repository libeufin.
The following commit(s) were added to refs/heads/master by this push:
new 9437d03 implement improved ISO20022 camt parsing
9437d03 is described below
commit 9437d034203690d0f536e428a0025f0416186a5b
Author: Florian Dold <florian.dold@gmail.com>
AuthorDate: Thu Jun 11 21:29:03 2020 +0530
implement improved ISO20022 camt parsing
---
.../src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 1 -
.../main/kotlin/tech/libeufin/nexus/Iso20022.kt | 321 ++++++++++
nexus/src/test/kotlin/Iso20022Test.kt | 25 +
.../camt.053.001.02.gesamtbeispiel.xml | 701 +++++++++++++++++++++
util/src/main/kotlin/XmlCombinators.kt | 81 ++-
5 files changed, 1124 insertions(+), 5 deletions(-)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
index 377c875..062f1d3 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
@@ -193,7 +193,6 @@ fun ingestBankMessagesIntoAccount(
}
acct.highestSeenBankMessageId = lastId
}
-
}
/**
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
new file mode 100644
index 0000000..154c402
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -0,0 +1,321 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2020 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.nexus
+
+/**
+ * Parse ISO 20022 messages
+ */
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import org.w3c.dom.Document
+import tech.libeufin.util.XmlElementDestructor
+import tech.libeufin.util.destructXml
+
+enum class CreditDebitIndicator {
+ DBIT, CRDT
+}
+
+enum class TransactionStatus {
+ BOOK, PENDING
+}
+
+data class TransactionDetails(
+ /**
+ * Related parties as JSON.
+ */
+ val relatedParties: RelatedParties,
+ val amountDetails: AmountDetails,
+ val references: References,
+ /**
+ * Unstructured remittance information (=subject line) of the transaction,
+ * or the empty string if missing.
+ */
+ val unstructuredRemittanceInformation: String
+)
+
+data class BankTransaction(
+ val accountIdentifier: String,
+ /**
+ * Scheme used for the account identifier.
+ */
+ val accountScheme: String,
+ val currency: String,
+ val amount: String,
+ /**
+ * Booked, pending, etc.
+ */
+ val status: TransactionStatus,
+ /**
+ * Is this transaction debiting or crediting the account
+ * it is reported for?
+ */
+ val creditDebitIndicator: CreditDebitIndicator,
+ /**
+ * Code that describes the type of bank transaction
+ * in more detail
+ */
+ val bankTransactionCode: BankTransactionCode,
+ /**
+ * Is this a batch booking?
+ */
+ val isBatch: Boolean,
+ val details: List<TransactionDetails>
+)
+
+abstract class TypedEntity(val type: String)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class Agent(
+ val name: String?,
+ val bic: String
+) : TypedEntity("agent")
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class Party(
+ val name: String?
+) : TypedEntity("party")
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class Account(
+ val iban: String?
+) : TypedEntity("party")
+
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class BankTransactionCode(
+ /**
+ * Standardized bank transaction code, as "$domain/$family/$subfamily"
+ */
+ val iso: String?,
+
+ /**
+ * Proprietary code, as "$issuer/$code".
+ */
+ val proprietary: String?
+)
+
+data class AmountAndCurrencyExchangeDetails(
+ val amount: String,
+ val currency: String
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class AmountDetails(
+ val instructedAmount: AmountAndCurrencyExchangeDetails?,
+ val transactionAmount: AmountAndCurrencyExchangeDetails?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class References(
+ val endToEndIdentification: String?
+)
+
+/**
+ * This structure captures both "TransactionParties6" and "TransactionAgents5"
+ * of ISO 20022.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class RelatedParties(
+ val debtor: Party?,
+ val debtorAccount: Account?,
+ val debtorAgent: Agent?,
+ val creditor: Party?,
+ val creditorAccount: Account?,
+ val creditorAgent: Agent?
+)
+
+class CamtParsingError(msg: String) : Exception(msg)
+
+private fun XmlElementDestructor.extractAgent(): Agent {
+ return Agent(
+ name = maybeUniqueChildNamed("FinInstnId") {
+ maybeUniqueChildNamed("Nm") {
+ it.textContent
+ }
+ },
+ bic = requireUniqueChildNamed("FinInstnId") {
+ requireUniqueChildNamed("BIC") {
+ it.textContent
+ }
+ }
+ )
+}
+
+private fun XmlElementDestructor.extractAccount(): Account {
+ return Account(
+ iban = requireUniqueChildNamed("Id") {
+ maybeUniqueChildNamed("IBAN") {
+ it.textContent
+ }
+ }
+ )
+}
+
+private fun XmlElementDestructor.extractParty(): Party {
+ return Party(
+ name = maybeUniqueChildNamed("Nm") { it.textContent }
+ )
+}
+
+private fun XmlElementDestructor.extractPartiesAndAgents(): RelatedParties {
+ return RelatedParties(
+ debtor = maybeUniqueChildNamed("RltdPties") {
+ maybeUniqueChildNamed("Dbtr") {
+ extractParty()
+ }
+ },
+ creditor = maybeUniqueChildNamed("RltdPties") {
+ maybeUniqueChildNamed("Cdtr") {
+ extractParty()
+ }
+ },
+ creditorAccount = maybeUniqueChildNamed("RltdPties") {
+ maybeUniqueChildNamed("CdtrAcct") {
+ extractAccount()
+ }
+ },
+ debtorAccount = maybeUniqueChildNamed("RltdPties") {
+ maybeUniqueChildNamed("DbtrAcct") {
+ extractAccount()
+ }
+ },
+ creditorAgent = maybeUniqueChildNamed("RltdAgts") {
+ maybeUniqueChildNamed("CdtrAgt") {
+ extractAgent()
+ }
+ },
+ debtorAgent = maybeUniqueChildNamed("RltdAgts") {
+ maybeUniqueChildNamed("DbtrAgt") {
+ extractAgent()
+ }
+ }
+ )
+}
+
+private fun XmlElementDestructor.extractAmountAndCurrencyExchangeDetails():
AmountAndCurrencyExchangeDetails {
+ return AmountAndCurrencyExchangeDetails(
+ amount = requireUniqueChildNamed("Amt") { it.textContent},
+ currency = requireUniqueChildNamed("Amt") { it.getAttribute("Ccy") }
+ )
+}
+
+private fun XmlElementDestructor.extractTransactionDetails():
List<TransactionDetails> {
+ return requireUniqueChildNamed("NtryDtls") {
+ mapEachChildNamed("TxDtls") {
+ TransactionDetails(
+ relatedParties = extractPartiesAndAgents(),
+ amountDetails = maybeUniqueChildNamed("AmtDtls") {
+ AmountDetails(
+ instructedAmount = maybeUniqueChildNamed("InstrAmt") {
extractAmountAndCurrencyExchangeDetails() },
+ transactionAmount = maybeUniqueChildNamed("TxAmt") {
extractAmountAndCurrencyExchangeDetails() }
+ )
+ } ?: AmountDetails(null, null),
+ references = maybeUniqueChildNamed("Refs") {
+ References(
+ endToEndIdentification =
maybeUniqueChildNamed("EndToEndId") { it.textContent }
+ )
+ } ?: References(null),
+ unstructuredRemittanceInformation =
maybeUniqueChildNamed("RmtInf") {
+ requireUniqueChildNamed("Ustrd") { it.textContent }
+ } ?: ""
+ )
+ }
+ }
+}
+
+private fun XmlElementDestructor.extractInnerTransactions():
List<BankTransaction> {
+ val iban = requireUniqueChildNamed("Acct") {
+ requireUniqueChildNamed("Id") {
+ requireUniqueChildNamed("IBAN") {
+ it.textContent
+ }
+ }
+ }
+
+ return mapEachChildNamed("Ntry") {
+ val amount = requireUniqueChildNamed("Amt") { it.textContent }
+ val currency = requireUniqueChildNamed("Amt") { it.getAttribute("Ccy")
}
+ val status = requireUniqueChildNamed("Sts") { it.textContent }.let {
+ TransactionStatus.valueOf(it)
+ }
+ val creditDebitIndicator = requireUniqueChildNamed("CdtDbtInd") {
it.textContent }.let {
+ CreditDebitIndicator.valueOf(it)
+ }
+ val btc = requireUniqueChildNamed("BkTxCd") {
+ BankTransactionCode(
+ proprietary = maybeUniqueChildNamed("Prtry") {
+ val cd = requireUniqueChildNamed("Cd") { it.textContent }
+ val issr = requireUniqueChildNamed("Issr") {
it.textContent }
+ "$issr:$cd"
+ },
+ iso = maybeUniqueChildNamed("Domn") {
+ val cd = requireUniqueChildNamed("Cd") { it.textContent }
+ val r = requireUniqueChildNamed("Fmly") {
+ object {
+ val fmlyCd = requireUniqueChildNamed("Cd") {
it.textContent }
+ val subFmlyCd =
requireUniqueChildNamed("SubFmlyCd") { it.textContent }
+ }
+ }
+ "$cd/${r.fmlyCd}/${r.subFmlyCd}"
+ }
+ )
+ }
+ val details = extractTransactionDetails()
+ BankTransaction(
+ accountIdentifier = iban,
+ accountScheme = "iban",
+ amount = amount,
+ currency = currency,
+ status = status,
+ creditDebitIndicator = creditDebitIndicator,
+ bankTransactionCode = btc,
+ details = details,
+ isBatch = details.size > 1
+ )
+ }
+}
+
+/**
+ * Extract a list of transactions from an ISO20022 camt.052 / camt.053 message.
+ */
+fun getTransactions(doc: Document): List<BankTransaction> {
+ return destructXml(doc) {
+ requireRootElement("Document") {
+ // Either bank to customer statement or report
+ requireOnlyChild() {
+ when (it.localName) {
+ "BkToCstmrAcctRpt" -> {
+ mapEachChildNamed("Rpt") {
+ extractInnerTransactions()
+ }
+ }
+ "BkToCstmrStmt" -> {
+ mapEachChildNamed("Stmt") {
+ extractInnerTransactions()
+ }
+ }
+ else -> {
+ throw CamtParsingError("expected statement or report")
+ }
+ }
+ }
+ }
+ }.flatten()
+}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/Iso20022Test.kt
b/nexus/src/test/kotlin/Iso20022Test.kt
new file mode 100644
index 0000000..377c977
--- /dev/null
+++ b/nexus/src/test/kotlin/Iso20022Test.kt
@@ -0,0 +1,25 @@
+package tech.libeufin.nexus
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import org.junit.Test
+import org.w3c.dom.Document
+import tech.libeufin.util.XMLUtil
+
+
+fun loadXmlResource(name: String): Document {
+ val classLoader = ClassLoader.getSystemClassLoader()
+ val res = classLoader.getResource(name)
+ if (res == null) {
+ throw Exception("resource $name not found");
+ }
+ return XMLUtil.parseStringIntoDom(res.readText())
+}
+
+class Iso20022Test {
+ @Test
+ fun testTransactionsImport() {
+ val camt53 =
loadXmlResource("iso20022-samples/camt.053.001.02.gesamtbeispiel.xml")
+ val txs = getTransactions(camt53)
+
println(jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(txs))
+ }
+}
\ No newline at end of file
diff --git
a/nexus/src/test/resources/iso20022-samples/camt.053.001.02.gesamtbeispiel.xml
b/nexus/src/test/resources/iso20022-samples/camt.053.001.02.gesamtbeispiel.xml
new file mode 100644
index 0000000..109acee
--- /dev/null
+++
b/nexus/src/test/resources/iso20022-samples/camt.053.001.02.gesamtbeispiel.xml
@@ -0,0 +1,701 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Mit XMLSpy v2008 rel. 2 sp2 (http://www.altova.com) im Mai 2016 von der
SIZ GmbH (Wenzel) angepasst hinsichtlich Anlage 3, Version 3.0: -->
+<!-- 1. BkTxCd Pflicht auf Entryebene, 2. Issuer nun "DK" (Statt "ZKA"), 3.
Mapping GVC auf Domn, 4. Nichtdrehen bei R-Transaktionen illustriert 5.
Schluss-Saldo angepasst (Löschung der DTAUS-Umsätze)-->
+<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02
camt.053.001.02.xsd">
+ <BkToCstmrStmt>
+ <GrpHdr>
+ <MsgId>27632364572</MsgId>
+ <CreDtTm>2016-05-11T19:30:47.0+01:00</CreDtTm>
+ <MsgRcpt>
+ <Id>
+ <OrgId>
+ <Othr>
+ <Id>BCS45678</Id>
+ </Othr>
+ </OrgId>
+ </Id>
+ </MsgRcpt>
+ <MsgPgntn>
+ <PgNb>1</PgNb>
+ <LastPgInd>true</LastPgInd>
+ </MsgPgntn>
+ </GrpHdr>
+ <Stmt>
+ <Id>2736482736482</Id>
+ <ElctrncSeqNb>101</ElctrncSeqNb>
+ <!--Folgendes Feld ist optional und könnte die
Papier-KAZ enthalten-->
+ <LglSeqNb>32</LglSeqNb>
+ <CreDtTm>2016-05-11T17:30:47.0+01:00</CreDtTm>
+ <Acct>
+ <Id>
+ <IBAN>DE62210500001234567890</IBAN>
+ </Id>
+ <Ccy>EUR</Ccy>
+ <Ownr>
+ <Nm>Name Kontoinhaber</Nm>
+ </Ownr>
+ <Svcr>
+ <FinInstnId>
+ <BIC>BANKDEFFXXX</BIC>
+ <Othr>
+ <Id>DE123456789</Id>
+ <Issr>UmsStId</Issr>
+ </Othr>
+ </FinInstnId>
+ </Svcr>
+ </Acct>
+ <Bal>
+ <Tp>
+ <CdOrPrtry>
+ <Cd>PRCD</Cd>
+ </CdOrPrtry>
+ </Tp>
+ <Amt Ccy="EUR">112.72</Amt>
+ <CdtDbtInd>CRDT</CdtDbtInd>
+ <Dt>
+ <Dt>2016-05-11</Dt>
+ </Dt>
+ </Bal>
+ <Bal>
+ <Tp>
+ <CdOrPrtry>
+ <Cd>CLBD</Cd>
+ </CdOrPrtry>
+ </Tp>
+ <Amt Ccy="EUR">158530.32</Amt>
+ <CdtDbtInd>CRDT</CdtDbtInd>
+ <Dt>
+ <Dt>2016-05-11</Dt>
+ </Dt>
+ </Bal>
+ <!-- Beispiel 1: SEPA-Zahlungen (Ueberweisung,
Lastschrift, R-Nachricht -->
+ <!--Gutschrift aufgrund eines
SEPA-Ueberweisungseinganges-->
+ <Ntry>
+ <Amt Ccy="EUR">100.00</Amt>
+ <CdtDbtInd>CRDT</CdtDbtInd>
+ <Sts>BOOK</Sts>
+ <BookgDt>
+ <Dt>2016-05-11</Dt>
+ </BookgDt>
+ <ValDt>
+ <Dt>2016-05-11</Dt>
+ </ValDt>
+ <AcctSvcrRef>Bankreferenz</AcctSvcrRef>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>RCDT</Cd>
+ <SubFmlyCd>ESCT</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+ <Cd>166</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <NtryDtls>
+ <TxDtls>
+ <Refs>
+
<EndToEndId>Ende-zu-Ende-Id des Ueberweisenden</EndToEndId>
+ </Refs>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>RCDT</Cd>
+
<SubFmlyCd>ESCT</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NTRF+166</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <RltdPties>
+ <Dbtr>
+ <Nm>Herr
Ueberweisender</Nm>
+ </Dbtr>
+ <DbtrAcct>
+ <Id>
+
<IBAN>DE21500500001234567897</IBAN>
+ </Id>
+ </DbtrAcct>
+ <UltmtDbtr>
+ <Nm>Herr Debtor
Reference Party</Nm>
+ </UltmtDbtr>
+ <Cdtr>
+ <Nm>Herr
Kontoinhaber</Nm>
+ </Cdtr>
+ <UltmtCdtr>
+ <Nm>Herr
Creditor Reference Party</Nm>
+ </UltmtCdtr>
+ </RltdPties>
+ <Purp>
+ <Cd>GDDS</Cd>
+ </Purp>
+ <RmtInf>
+ <Ustrd>Rechnungsnr.
4711 vom 20.04.2016</Ustrd>
+ </RmtInf>
+ </TxDtls>
+ </NtryDtls>
+ <AddtlNtryInf>SEPA GUTSCHRIFT</AddtlNtryInf>
+ </Ntry>
+ <!--Gutschrift aufgrund einer zurueckgekommenen
SEPA-Ueberweisung-->
+ <Ntry>
+ <Amt Ccy="EUR">200.00</Amt>
+ <CdtDbtInd>CRDT</CdtDbtInd>
+ <Sts>BOOK</Sts>
+ <BookgDt>
+ <Dt>2016-05-11</Dt>
+ </BookgDt>
+ <ValDt>
+ <Dt>2016-05-11</Dt>
+ </ValDt>
+ <AcctSvcrRef>Bankreferenz</AcctSvcrRef>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>ICDT</Cd>
+ <SubFmlyCd>RRTN</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+ <Cd>159</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <NtryDtls>
+ <TxDtls>
+ <Refs>
+ <EndToEndId>Urspr.
E2E-Id der Hintransaktion</EndToEndId>
+ </Refs>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>ICDT</Cd>
+
<SubFmlyCd>RRTN</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NRTI+159++901</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <RmtInf>
+ <Ustrd>Angabe des
urspruenglichen Verwendungszweckes</Ustrd>
+ </RmtInf>
+ <!--Informationen zur
Originaltransaktion. Da die Belegung von Domain optional ist, kann auch nur
Prtry (GVC) vorhanden sein-->
+ <RtrInf>
+ <OrgnlBkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+
<Cd>ICDT</Cd>
+
<SubFmlyCd>ESCT</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NTRF+116</Cd>
+
<Issr>DK</Issr>
+ </Prtry>
+ </OrgnlBkTxCd>
+ <Orgtr>
+ <Id>
+ <OrgId>
+
<BICOrBEI>BANKDEHH</BICOrBEI>
+ </OrgId>
+ </Id>
+ </Orgtr>
+ <Rsn>
+ <Cd>AC01</Cd>
+ </Rsn>
+ <AddtlInf>IBAN
FEHLERHAFT</AddtlInf>
+ </RtrInf>
+ </TxDtls>
+ </NtryDtls>
+ <AddtlNtryInf>SEPA RUECKBUCHUNG</AddtlNtryInf>
+ </Ntry>
+ <!--Belastung aufgrund einer SEPA-Lastschrift-->
+ <Ntry>
+ <Amt Ccy="EUR">50.00</Amt>
+ <CdtDbtInd>DBIT</CdtDbtInd>
+ <Sts>BOOK</Sts>
+ <BookgDt>
+ <Dt>2016-05-11</Dt>
+ </BookgDt>
+ <ValDt>
+ <Dt>2016-05-11</Dt>
+ </ValDt>
+ <AcctSvcrRef>Bankreferenz</AcctSvcrRef>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>RDDT</Cd>
+ <SubFmlyCd>ESDD</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+ <Cd>105</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <NtryDtls>
+ <TxDtls>
+ <Refs>
+ <EndToEndId>E2E-Id
vergeben vom Glaeubiger</EndToEndId>
+ <MndtId>Ref. des
SEPA-Lastschriftmandats</MndtId>
+ </Refs>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>RDDT</Cd>
+
<SubFmlyCd>ESDD</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NDDT+105</Cd>
+ <Issr>DKA</Issr>
+ </Prtry>
+ </BkTxCd>
+ <RltdPties>
+ <Dbtr>
+ <Nm>Herr
Zahlungspflichtiger</Nm>
+ </Dbtr>
+ <UltmtDbtr>
+ <Nm>Herr Debtor
Reference Party</Nm>
+ </UltmtDbtr>
+ <Cdtr>
+
<Nm>Glaeubigerfirma</Nm>
+ <Id>
+ <PrvtId>
+
<Othr>
+
<Id>Cdtr-Id des Glaeubigers</Id>
+
</Othr>
+
</PrvtId>
+ </Id>
+ </Cdtr>
+ </RltdPties>
+ <Purp>
+ <Cd>PHON</Cd>
+ </Purp>
+ <RmtInf>
+ <Ustrd>Telefonrechnung
April 2016, Vertragsnummer 3536456345</Ustrd>
+ </RmtInf>
+ </TxDtls>
+ </NtryDtls>
+ <AddtlNtryInf>SEPA LASTSCHRIFT</AddtlNtryInf>
+ </Ntry>
+ <!-- Beispiel 2: DTAUS-Zahlungen (Ueberweisung,
Lastschrift, Rueckgabe) BEISPIEL ENTFERNT -->
+ <!-- Beispiel 3a: Sammlerdarstellung mit Aufloesung
innerhalb der Nachricht -->
+ <!--Belastung aufgrund von SEPA-Lastschriftrueckgaben
(Sammelbuchung) mit Sammleraufloesung unter Transaction Details-->
+ <Ntry>
+ <Amt Ccy="EUR">276</Amt>
+ <CdtDbtInd>DBIT</CdtDbtInd>
+ <Sts>BOOK</Sts>
+ <BookgDt>
+ <Dt>2016-05-11</Dt>
+ </BookgDt>
+ <ValDt>
+ <Dt>2016-05-11</Dt>
+ </ValDt>
+ <AcctSvcrRef>Bankreferenz</AcctSvcrRef>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>IDDT</Cd>
+ <SubFmlyCd>UPDD</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+ <Cd>109</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <NtryDtls>
+ <Btch>
+ <NbOfTxs>3</NbOfTxs>
+ </Btch>
+ <TxDtls>
+ <!-- Ab hier Aufloesung des
Sammlers bestehend aus 3 Einzelumsaetzen -->
+ <Refs>
+
<EndToEndId>79892</EndToEndId>
+ <MndtId>10001</MndtId>
+ </Refs>
+ <AmtDtls>
+ <TxAmt>
+ <Amt
Ccy="EUR">76</Amt>
+ </TxAmt>
+ </AmtDtls>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>IDDT</Cd>
+
<SubFmlyCd>UPDD</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NRTI+109++901</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <!--Weil bei R-Transaktionen
nicht gedreht wird, stehen hier die Originals-->
+ <RltdPties>
+ <Dbtr>
+ <Nm>Herr
Zahlungspflichtiger 1</Nm>
+ </Dbtr>
+ <DbtrAcct>
+ <Id>
+
<IBAN>DE83700202707777777777</IBAN>
+ </Id>
+ </DbtrAcct>
+ <Cdtr>
+
<Nm>Telefongesellschaft ABC</Nm>
+ <Id>
+ <PrvtId>
+
<Othr>
+
<Id>CdtrId des SEPA-Lastschrifteinr.</Id>
+
</Othr>
+
</PrvtId>
+ </Id>
+ </Cdtr>
+ <CdtrAcct>
+ <Id>
+
<IBAN>DE62210500001234567890</IBAN>
+ </Id>
+ </CdtrAcct>
+ </RltdPties>
+ <Purp>
+ <Cd>PHON</Cd>
+ </Purp>
+ <RmtInf>
+ <Ustrd>Telefonrechnung
April 2016, Vertragsnummer 3536456345</Ustrd>
+ </RmtInf>
+ <RtrInf>
+ <Rsn>
+ <Cd>AC01</Cd>
+ </Rsn>
+
<AddtlInf>RUECKLASTSCHRIFT IBAN FEHLERHAFT</AddtlInf>
+ </RtrInf>
+ </TxDtls>
+ <TxDtls>
+ <Refs>
+
<EndToEndId>768768</EndToEndId>
+ <MndtId>10002</MndtId>
+ </Refs>
+ <AmtDtls>
+ <TxAmt>
+ <Amt
Ccy="EUR">80</Amt>
+ </TxAmt>
+ </AmtDtls>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>IDDT</Cd>
+
<SubFmlyCd>UPDD</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NRTI+109++901</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <RltdPties>
+ <Dbtr>
+ <Nm>Herr
Zahlungspflichtiger 2</Nm>
+ </Dbtr>
+ <DbtrAcct>
+ <Id>
+
<IBAN>DE83700202704444444444</IBAN>
+ </Id>
+ </DbtrAcct>
+ <Cdtr>
+
<Nm>Telefongesellschaft ABC</Nm>
+ <Id>
+ <PrvtId>
+
<Othr>
+
<Id>CdtrId des SEPA-Lastschrifteinr.</Id>
+
</Othr>
+
</PrvtId>
+ </Id>
+ </Cdtr>
+ <CdtrAcct>
+ <Id>
+
<IBAN>DE62210500001234567890</IBAN>
+ </Id>
+ </CdtrAcct>
+ </RltdPties>
+ <Purp>
+ <Cd>PHON</Cd>
+ </Purp>
+ <RmtInf>
+ <Ustrd>Telefonrechnung
April 2016, Vertragsnummer 3536456888</Ustrd>
+ </RmtInf>
+ <RtrInf>
+ <Rsn>
+ <Cd>AC01</Cd>
+ </Rsn>
+
<AddtlInf>RUECKLASTSCHRIFT IBAN FEHLERHAFT</AddtlInf>
+ </RtrInf>
+ </TxDtls>
+ <TxDtls>
+ <Refs>
+
<EndToEndId>45456465</EndToEndId>
+ <MndtId>10003</MndtId>
+ </Refs>
+ <AmtDtls>
+ <TxAmt>
+ <Amt
Ccy="EUR">120</Amt>
+ </TxAmt>
+ </AmtDtls>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>IDDT</Cd>
+
<SubFmlyCd>UPDD</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NRTI+109++901</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <RltdPties>
+ <Dbtr>
+ <Nm>Herr
Zahlungspflichtiger 3</Nm>
+ </Dbtr>
+ <DbtrAcct>
+ <Id>
+
<IBAN>DE83700202703333333333</IBAN>
+ </Id>
+ </DbtrAcct>
+ <Cdtr>
+
<Nm>Telefongesellschaft ABC</Nm>
+ <Id>
+ <PrvtId>
+
<Othr>
+
<Id>CdtrId des SEPA-Lastschrifteinr.</Id>
+
</Othr>
+
</PrvtId>
+ </Id>
+ </Cdtr>
+ <CdtrAcct>
+ <Id>
+
<IBAN>DE62210500001234567890</IBAN>
+ </Id>
+ </CdtrAcct>
+ </RltdPties>
+ <Purp>
+ <Cd>PHON</Cd>
+ </Purp>
+ <RmtInf>
+ <Ustrd>Telefonrechnung
April 2016, Vertragsnummer 3536456345</Ustrd>
+ </RmtInf>
+ <RtrInf>
+ <Rsn>
+ <Cd>AC01</Cd>
+ </Rsn>
+
<AddtlInf>RUECKLASTSCHRIFT IBAN FEHLERHAFT</AddtlInf>
+ </RtrInf>
+ </TxDtls>
+ </NtryDtls>
+ </Ntry>
+ <!-- Beispiel 3b: Sammlerdarstellung mit Verweis auf
pain-Nachricht und separate camt.054.001.01-Nachricht -->
+ <!--Belastung aufgrund einer SEPA-Ueberweisung
(Sammler) mit Verweis auf Original pain-Nachricht-->
+ <Ntry>
+ <Amt Ccy="EUR">100876.00</Amt>
+ <CdtDbtInd>DBIT</CdtDbtInd>
+ <Sts>BOOK</Sts>
+ <BookgDt>
+ <Dt>2016-05-11</Dt>
+ </BookgDt>
+ <ValDt>
+ <Dt>2016-05-11</Dt>
+ </ValDt>
+ <AcctSvcrRef>Bankreferenz</AcctSvcrRef>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>ICDT</Cd>
+ <SubFmlyCd>ESCT</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+ <Cd>191</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <NtryDtls>
+ <Btch>
+ <MsgId>MsgId der
pain-Nachricht</MsgId>
+ <PmtInfId>Sammler-Id dieser
pain-Nachricht</PmtInfId>
+ </Btch>
+ <TxDtls>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>ICDT</Cd>
+
<SubFmlyCd>ESCT</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NTRF+191</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ </TxDtls>
+ </NtryDtls>
+ <AddtlNtryInf>SEPA Credit Transfer
(Sammler-Soll)</AddtlNtryInf>
+ </Ntry>
+ <!--Belastung aufgrund von SEPA-Lastschriftrueckgaben
(Sammelbuchung) mit Verweis auf separate camt.054.001.01-Nachricht-->
+ <Ntry>
+ <Amt Ccy="EUR">276.00</Amt>
+ <CdtDbtInd>DBIT</CdtDbtInd>
+ <Sts>BOOK</Sts>
+ <BookgDt>
+ <Dt>2016-05-11</Dt>
+ </BookgDt>
+ <ValDt>
+ <Dt>2016-05-11</Dt>
+ </ValDt>
+ <AcctSvcrRef>Bankreferenz</AcctSvcrRef>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>IDDT</Cd>
+ <SubFmlyCd>UPDD</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+ <Cd>109</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <AddtlInfInd>
+ <MsgNmId>camt.054.001.01</MsgNmId>
+ <MsgId>054-20160511-00034</MsgId>
+ <!-- siehe Bsp. camt54 Bsp 3b -->
+ </AddtlInfInd>
+ <NtryDtls>
+ <TxDtls>
+ <BkTxCd>
+ <Domn>
+ <Cd>PNMT</Cd>
+ <Fmly>
+ <Cd>IDDT</Cd>
+
<SubFmlyCd>UPDD</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NRTI+109++901</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ </TxDtls>
+ </NtryDtls>
+ </Ntry>
+ <!-- Beispiel 4: USD-Zahlung mit Gutschrift auf einem
EUR-Konto -->
+ <!-- USD-Zahlung mit Gutschrift auf einem EUR-Konto -->
+ <Ntry>
+ <Amt Ccy="EUR">259595.60</Amt>
+ <CdtDbtInd>CRDT</CdtDbtInd>
+ <Sts>BOOK</Sts>
+ <BookgDt>
+ <Dt>2016-05-11</Dt>
+ </BookgDt>
+ <ValDt>
+ <Dt>2016-05-11</Dt>
+ </ValDt>
+ <AcctSvcrRef>Bankreferenz</AcctSvcrRef>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>RCDT</Cd>
+
<SubFmlyCd>XBCT</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NTRF+202</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <NtryDtls>
+ <TxDtls>
+ <AmtDtls>
+ <InstdAmt>
+ <Amt
Ccy="USD">360873.97</Amt>
+ </InstdAmt>
+ <TxAmt>
+ <Amt
Ccy="EUR">259595.60</Amt>
+ </TxAmt>
+ <CntrValAmt>
+ <Amt
Ccy="EUR">259621.56</Amt>
+ <CcyXchg>
+
<SrcCcy>USD</SrcCcy>
+
<TrgtCcy>EUR</TrgtCcy>
+
<XchgRate>1.39</XchgRate>
+ </CcyXchg>
+ </CntrValAmt>
+ </AmtDtls>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>RCDT</Cd>
+
<SubFmlyCd>XBCT</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+
<Cd>NTRF+202</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <Chrgs>
+ <Amt
Ccy="EUR">25.96</Amt>
+ </Chrgs>
+ <RltdPties>
+ <Dbtr>
+ <Nm>West Coast
Ltd.</Nm>
+ <PstlAdr>
+
<Ctry>US</Ctry>
+
<AdrLine>52, Main Street</AdrLine>
+
<AdrLine>3733 San Francisco</AdrLine>
+ </PstlAdr>
+ </Dbtr>
+ <DbtrAcct>
+ <Id>
+ <Othr>
+
<Id>546237687</Id>
+ </Othr>
+ </Id>
+ </DbtrAcct>
+ </RltdPties>
+ <RltdAgts>
+ <DbtrAgt>
+ <FinInstnId>
+
<BIC>BANKUSNY</BIC>
+ </FinInstnId>
+ </DbtrAgt>
+ </RltdAgts>
+ <RmtInf>
+ <Ustrd>Invoice No.
4545</Ustrd>
+ </RmtInf>
+ </TxDtls>
+ </NtryDtls>
+
<AddtlNtryInf>AZV-UEBERWEISUNGSGUTSCHRIFT</AddtlNtryInf>
+ </Ntry>
+ </Stmt>
+ </BkToCstmrStmt>
+</Document>
diff --git a/util/src/main/kotlin/XmlCombinators.kt
b/util/src/main/kotlin/XmlCombinators.kt
index d799c57..1c49521 100644
--- a/util/src/main/kotlin/XmlCombinators.kt
+++ b/util/src/main/kotlin/XmlCombinators.kt
@@ -1,6 +1,8 @@
package tech.libeufin.util
import com.sun.xml.txw2.output.IndentingXMLStreamWriter
+import org.w3c.dom.Document
+import org.w3c.dom.Element
import java.io.StringWriter
import javax.xml.stream.XMLOutputFactory
import javax.xml.stream.XMLStreamWriter
@@ -49,6 +51,7 @@ class XmlDocumentBuilder {
fun namespace(uri: String) {
writer.setDefaultNamespace(uri)
}
+
fun namespace(prefix: String, uri: String) {
writer.setPrefix(prefix, uri)
}
@@ -85,10 +88,80 @@ fun constructXml(indent: Boolean = false, f:
XmlDocumentBuilder.() -> Unit): Str
return "<?xml version=\"1.0\" encoding=\"UTF-8\"
standalone=\"yes\"?>\n${stream.buffer.toString()}"
}
-class XmlDocumentDestructor {
+class DestructionError(m: String) : Exception(m)
+
+private fun Element.getChildElements(ns: String, tag: String): List<Element> {
+ val elements = mutableListOf<Element>()
+ for (i in 0..this.childNodes.length) {
+ val el = this.childNodes.item(i)
+ if (el !is Element) {
+ continue
+ }
+ if (ns != "*" && el.namespaceURI != ns) {
+ continue
+ }
+ if (tag != "*" && el.localName != tag) {
+ continue
+ }
+ elements.add(el)
+ }
+ return elements
+}
+
+class XmlElementDestructor internal constructor(val d: Document, val e:
Element) {
+ fun <T> requireOnlyChild(f: XmlElementDestructor.(e: Element) -> T): T {
+ val child =
+ e.getChildElements("*", "*").elementAtOrNull(0)
+ ?: throw DestructionError("expected singleton child tag")
+ val destr = XmlElementDestructor(d, child)
+ return f(destr, child)
+ }
+
+ fun <T> mapEachChildNamed(s: String, f: XmlElementDestructor.(e: Element)
-> T): List<T> {
+ val res = mutableListOf<T>()
+ val els = e.getChildElements("*", s)
+ for (child in els) {
+ val destr = XmlElementDestructor(d, child)
+ res.add(f(destr, child))
+ }
+ return res
+ }
+
+ fun <T> requireUniqueChildNamed(s: String, f: XmlElementDestructor.(e:
Element) -> T): T {
+ val cl = e.getChildElements("*", s)
+ if (cl.size != 1) {
+ throw DestructionError("expected exactly one unique $s child, got
${cl.size} instead")
+ }
+ val el = cl[0]
+ val destr = XmlElementDestructor(d, el)
+ return f(destr, el)
+ }
+
+ fun <T> maybeUniqueChildNamed(s: String, f: XmlElementDestructor.(e:
Element) -> T): T? {
+ val cl = e.getChildElements("*", s)
+ if (cl.size > 1) {
+ throw DestructionError("expected at most one unique $s child, got
${cl.size} instead")
+ }
+ if (cl.size == 1) {
+ val el = cl[0]
+ val destr = XmlElementDestructor(d, el)
+ println("found child $s")
+ return f(destr, el)
+ }
+ return null
+ }
+}
+
+class XmlDocumentDestructor internal constructor(val d: Document) {
+ fun <T> requireRootElement(name: String, f: XmlElementDestructor.(e:
Element) -> T): T {
+ if (this.d.documentElement.tagName != name) {
+ throw DestructionError("expected '$name' tag")
+ }
+ val destr = XmlElementDestructor(d, d.documentElement)
+ return f(destr, this.d.documentElement)
+ }
}
-fun <T> destructXml(f: XmlDocumentDestructor.() -> T): T {
- val d = XmlDocumentDestructor()
- return f(d)
+fun <T> destructXml(d: Document, f: XmlDocumentDestructor.() -> T): T {
+ return f(XmlDocumentDestructor(d))
}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated: implement improved ISO20022 camt parsing,
gnunet <=