[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated: iso20022
From: |
gnunet |
Subject: |
[libeufin] branch master updated: iso20022 |
Date: |
Mon, 06 Jul 2020 12:56:15 +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 94215e3 iso20022
94215e3 is described below
commit 94215e32b2333d507ebbb61a8bfc7f498fd3d187
Author: Florian Dold <florian.dold@gmail.com>
AuthorDate: Mon Jul 6 16:26:02 2020 +0530
iso20022
---
.../main/kotlin/tech/libeufin/nexus/Iso20022.kt | 184 +++++++++++++++++----
nexus/src/test/kotlin/Iso20022Test.kt | 42 +++--
.../camt.053/de.camt.053.001.02.xml | 171 ++++++++++++++++++-
3 files changed, 349 insertions(+), 48 deletions(-)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
index 95d3506..16fcf84 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -67,6 +67,7 @@ data class CamtReport(
data class GenericId(
val id: String,
val schemeName: String?,
+ val proprietarySchemeName: String?,
val issuer: String?
)
@@ -78,8 +79,19 @@ data class CashAccount(
val otherId: GenericId?
)
+data class Balance(
+ val type: String?,
+ val subtype: String?,
+ val proprietaryType: String?,
+ val proprietarySubtype: String?,
+ val date: String,
+ val creditDebitIndicator: CreditDebitIndicator,
+ val amount: CurrencyAmount
+)
+
data class CamtParseResult(
val reports: List<CamtReport>,
+ val balances: List<Balance>,
val messageId: String,
/**
* Message type in form of the ISO 20022 message name.
@@ -88,13 +100,36 @@ data class CamtParseResult(
val creationDateTime: String
)
-enum class PartyType(@get:JsonValue val jsonName: String) {
- PRIVATE("private"), ORGANIZATION("organization")
-}
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class PrivateIdentification(
+ val birthDate: String?,
+ val provinceOfBirth: String?,
+ val cityOfBirth: String?,
+ val countryOfBirth: String?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class OrganizationIdentification(
+ val bic: String?,
+ val lei: String?
+)
+/**
+ * Identification of a party, which can be a private party
+ * or an organiation.
+ *
+ * Mapping of ISO 20022 PartyIdentification135.
+ */
@JsonInclude(JsonInclude.Include.NON_NULL)
data class PartyIdentification(
val name: String?,
+ val countryOfResidence: String?,
+ val privateId: PrivateIdentification?,
+ val organizationId: OrganizationIdentification?,
+
+ /**
+ * Identification that applies to both private parties and organizations.
+ */
val otherId: GenericId?
)
@@ -144,7 +179,17 @@ data class TransactionInfo(
* Unstructured remittance information (=subject line) of the transaction,
* or the empty string if missing.
*/
- val unstructuredRemittanceInformation: String
+ val unstructuredRemittanceInformation: String,
+ val returnInfo: ReturnInfo?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class ReturnInfo(
+ val originalBankTransactionCode: BankTransactionCode?,
+ val originator: PartyIdentification?,
+ val reason: String?,
+ val proprietaryReason: String?,
+ val additionalInfo: String?
)
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -349,6 +394,19 @@ private fun XmlElementDestructor.extractAgent():
AgentIdentification {
)
}
+private fun XmlElementDestructor.extractGenericId(): GenericId {
+ return GenericId(
+ id = requireUniqueChildNamed("Id") { it.textContent },
+ schemeName = maybeUniqueChildNamed("SchmeNm") {
+ maybeUniqueChildNamed("Cd") { it.textContent }
+ },
+ issuer = maybeUniqueChildNamed("Issr") { it.textContent },
+ proprietarySchemeName = maybeUniqueChildNamed("SchmeNm") {
+ maybeUniqueChildNamed("Prtry") { it.textContent }
+ }
+ )
+}
+
private fun XmlElementDestructor.extractAccount(): CashAccount {
var iban: String? = null
var otherId: GenericId? = null
@@ -361,11 +419,7 @@ private fun XmlElementDestructor.extractAccount():
CashAccount {
iban = it.textContent
}
"Othr" -> {
- otherId = GenericId(
- id = requireUniqueChildNamed("Id") { it.textContent },
- schemeName = maybeUniqueChildNamed("SchmeNm") {
it.textContent },
- issuer = maybeUniqueChildNamed("Issr") {
it.textContent }
- )
+ otherId = extractGenericId()
}
else -> throw Error("invalid account identification")
}
@@ -375,9 +429,43 @@ private fun XmlElementDestructor.extractAccount():
CashAccount {
}
private fun XmlElementDestructor.extractParty(): PartyIdentification {
+ val otherId: GenericId? = maybeUniqueChildNamed("Id") {
+ (maybeUniqueChildNamed("PrvtId") { it } ?:
maybeUniqueChildNamed("OrgId") { it })?.run {
+ maybeUniqueChildNamed("Othr") {
+ extractGenericId()
+ }
+ }
+ }
+
+ val privateId = maybeUniqueChildNamed("Id") {
+ maybeUniqueChildNamed("PrvtId") {
+ maybeUniqueChildNamed("DtAndPlcOfBirth") {
+ PrivateIdentification(
+ birthDate = maybeUniqueChildNamed("BirthDt") {
it.textContent},
+ cityOfBirth = maybeUniqueChildNamed("CityOfBirth") {
it.textContent},
+ countryOfBirth = maybeUniqueChildNamed("CtryOfBirth") {
it.textContent},
+ provinceOfBirth = maybeUniqueChildNamed("PrvcOfBirth") {
it.textContent}
+ )
+ }
+ }
+ }
+
+ val organizationId = maybeUniqueChildNamed("Id") {
+ maybeUniqueChildNamed("OrgId") {
+ OrganizationIdentification(
+ bic = maybeUniqueChildNamed("BICOrBEI") { it.textContent} ?:
maybeUniqueChildNamed("AnyBIC") { it.textContent},
+ lei = maybeUniqueChildNamed("LEI") { it.textContent}
+ )
+ }
+ }
+
+
return PartyIdentification(
name = maybeUniqueChildNamed("Nm") { it.textContent },
- otherId = null
+ otherId = otherId,
+ privateId = privateId,
+ organizationId = organizationId,
+ countryOfResidence = maybeUniqueChildNamed("CtryOfRes") {
it.textContent }
)
}
@@ -486,7 +574,7 @@ private fun XmlElementDestructor.extractTransactionInfos(
if (chunks.isEmpty()) {
null
} else {
- chunks.joinToString()
+ chunks.joinToString(separator = "")
}
} ?: "",
creditorAgent = maybeUniqueChildNamed("CdtrAgt") {
extractAgent() },
@@ -494,12 +582,45 @@ private fun XmlElementDestructor.extractTransactionInfos(
debtorAccount = maybeUniqueChildNamed("DbtrAgt") {
extractAccount() },
creditorAccount = maybeUniqueChildNamed("CdtrAgt") {
extractAccount() },
debtor = maybeUniqueChildNamed("Dbtr") { extractParty() },
- creditor = maybeUniqueChildNamed("Cdtr") { extractParty() }
+ creditor = maybeUniqueChildNamed("Cdtr") { extractParty() },
+ returnInfo = maybeUniqueChildNamed("RtrInf") {
+ ReturnInfo(
+ originalBankTransactionCode =
maybeUniqueChildNamed("OrgnlBkTxCd") {
+ extractInnerBkTxCd()
+ },
+ originator = maybeUniqueChildNamed("Orgtr") {
extractParty() },
+ reason = maybeUniqueChildNamed("Rsn") {
maybeUniqueChildNamed("Cd") { it.textContent } },
+ proprietaryReason = maybeUniqueChildNamed("Rsn") {
maybeUniqueChildNamed("Prtry") { it.textContent } },
+ additionalInfo = maybeUniqueChildNamed("AddtlInf") {
it.textContent }
+ )
+ }
)
}
}
}
+private fun XmlElementDestructor.extractInnerBkTxCd(): BankTransactionCode {
+ return BankTransactionCode(
+ domain = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Cd") {
it.textContent } },
+ family = maybeUniqueChildNamed("Domn") {
+ maybeUniqueChildNamed("Fmly") {
+ maybeUniqueChildNamed("Cd") { it.textContent }
+ }
+ },
+ subfamily = maybeUniqueChildNamed("Domn") {
+ maybeUniqueChildNamed("Fmly") {
+ maybeUniqueChildNamed("SubFmlyCd") { it.textContent }
+ }
+ },
+ proprietaryCode = maybeUniqueChildNamed("Prtry") {
+ maybeUniqueChildNamed("Cd") { it.textContent }
+ },
+ proprietaryIssuer = maybeUniqueChildNamed("Prtry") {
+ maybeUniqueChildNamed("Issr") { it.textContent }
+ }
+ )
+}
+
private fun XmlElementDestructor.extractInnerTransactions(): CamtReport {
val account = requireUniqueChildNamed("Acct") { extractAccount() }
val entries = mapEachChildNamed("Ntry") {
@@ -512,25 +633,7 @@ private fun
XmlElementDestructor.extractInnerTransactions(): CamtReport {
CreditDebitIndicator.valueOf(it)
}
val btc = requireUniqueChildNamed("BkTxCd") {
- BankTransactionCode(
- domain = maybeUniqueChildNamed("Domn") {
maybeUniqueChildNamed("Cd") { it.textContent } },
- family = maybeUniqueChildNamed("Domn") {
- maybeUniqueChildNamed("Fmly") {
- maybeUniqueChildNamed("Cd") { it.textContent }
- }
- },
- subfamily = maybeUniqueChildNamed("Domn") {
- maybeUniqueChildNamed("Fmly") {
- maybeUniqueChildNamed("SubFmlyCd") { it.textContent }
- }
- },
- proprietaryCode = maybeUniqueChildNamed("Prtry") {
- maybeUniqueChildNamed("Cd") { it.textContent }
- },
- proprietaryIssuer = maybeUniqueChildNamed("Prtry") {
- maybeUniqueChildNamed("Issr") { it.textContent }
- }
- )
+ extractInnerBkTxCd()
}
val acctSvcrRef = maybeUniqueChildNamed("AcctSvcrRef") {
it.textContent }
val entryRef = maybeUniqueChildNamed("NtryRef") { it.textContent }
@@ -575,6 +678,23 @@ fun parseCamtMessage(doc: Document): CamtParseResult {
}
}
}
+
+ val balances = requireOnlyChild {
+ mapEachChildNamed("Bal") {
+ Balance(
+ type = maybeUniqueChildNamed("Tp") {
maybeUniqueChildNamed("Cd") { it.textContent } },
+ proprietaryType = maybeUniqueChildNamed("Tp") {
maybeUniqueChildNamed("Prtry") { it.textContent } },
+ date = extractDateOrDateTime(),
+ creditDebitIndicator =
requireUniqueChildNamed("CdtDbtInd") { it.textContent }.let {
+ CreditDebitIndicator.valueOf(it)
+ },
+ subtype = maybeUniqueChildNamed("SubTp") {
maybeUniqueChildNamed("Cd") { it.textContent } },
+ proprietarySubtype = maybeUniqueChildNamed("SubTp") {
maybeUniqueChildNamed("Prtry") { it.textContent } },
+ amount = extractCurrencyAmount()
+ )
+ }
+ }
+
val messageId = requireOnlyChild {
requireUniqueChildNamed("GrpHdr") {
requireUniqueChildNamed("MsgId") { it.textContent }
@@ -594,7 +714,7 @@ fun parseCamtMessage(doc: Document): CamtParseResult {
}
}
}
- CamtParseResult(reports, messageId, messageType, creationDateTime)
+ CamtParseResult(reports, balances, messageId, messageType,
creationDateTime)
}
}
}
diff --git a/nexus/src/test/kotlin/Iso20022Test.kt
b/nexus/src/test/kotlin/Iso20022Test.kt
index 24553be..f655414 100644
--- a/nexus/src/test/kotlin/Iso20022Test.kt
+++ b/nexus/src/test/kotlin/Iso20022Test.kt
@@ -19,21 +19,33 @@ class Iso20022Test {
fun testTransactionsImport() {
val camt53 =
loadXmlResource("iso20022-samples/camt.053/de.camt.053.001.02.xml")
val r = parseCamtMessage(camt53)
- assertEquals(r.messageId, "msg-001")
- assertEquals(r.creationDateTime, "2020-07-03T12:44:40+05:30")
- assertEquals(r.messageType, CashManagementResponseType.Statement)
- assertEquals(r.reports.size, 1)
- assertEquals(r.reports[0].entries[0].entryAmount.amount, "100.00")
- assertEquals(r.reports[0].entries[0].entryAmount.currency, "EUR")
- assertEquals(r.reports[0].entries[0].status, EntryStatus.BOOK)
- assertEquals(r.reports[0].entries[0].entryRef, null)
- assertEquals(r.reports[0].entries[0].accountServicerRef,
"acctsvcrref-001")
- assertEquals(r.reports[0].entries[0].bankTransactionCode.domain,
"PMNT")
- assertEquals(r.reports[0].entries[0].bankTransactionCode.family,
"RCDT")
- assertEquals(r.reports[0].entries[0].bankTransactionCode.subfamily,
"ESCT")
-
assertEquals(r.reports[0].entries[0].bankTransactionCode.proprietaryCode, "166")
-
assertEquals(r.reports[0].entries[0].bankTransactionCode.proprietaryIssuer,
"DK")
- assertEquals(r.reports[0].entries[0].transactionInfos.size, 1)
+ assertEquals("msg-001", r.messageId)
+ assertEquals("2020-07-03T12:44:40+05:30", r.creationDateTime)
+ assertEquals(CashManagementResponseType.Statement, r.messageType)
+ assertEquals(1, r.reports.size)
+
+ // First Entry
+ assertEquals("100.00", r.reports[0].entries[0].entryAmount.amount)
+ assertEquals("EUR", r.reports[0].entries[0].entryAmount.currency)
+ assertEquals(CreditDebitIndicator.CRDT,
r.reports[0].entries[0].creditDebitIndicator)
+ assertEquals(EntryStatus.BOOK, r.reports[0].entries[0].status)
+ assertEquals(null, r.reports[0].entries[0].entryRef)
+ assertEquals("acctsvcrref-001",
r.reports[0].entries[0].accountServicerRef)
+ assertEquals("PMNT",
r.reports[0].entries[0].bankTransactionCode.domain)
+ assertEquals("RCDT",
r.reports[0].entries[0].bankTransactionCode.family)
+ assertEquals("ESCT",
r.reports[0].entries[0].bankTransactionCode.subfamily)
+ assertEquals("166",
r.reports[0].entries[0].bankTransactionCode.proprietaryCode)
+ assertEquals("DK",
r.reports[0].entries[0].bankTransactionCode.proprietaryIssuer)
+ assertEquals(1, r.reports[0].entries[0].transactionInfos.size)
+ assertEquals("EUR",
r.reports[0].entries[0].transactionInfos[0].amount.currency)
+ assertEquals("100.00",
r.reports[0].entries[0].transactionInfos[0].amount.amount)
+ assertEquals(CreditDebitIndicator.CRDT,
r.reports[0].entries[0].transactionInfos[0].creditDebitIndicator)
+ assertEquals("unstructured info one",
r.reports[0].entries[0].transactionInfos[0].unstructuredRemittanceInformation)
+
+ // Second Entry
+ assertEquals("unstructured info across lines",
r.reports[0].entries[1].transactionInfos[0].unstructuredRemittanceInformation)
+
+ // Third Entry
// Make sure that round-tripping of entry CamtBankAccountEntry JSON
works
for (entry in r.reports.flatMap { it.entries }) {
diff --git
a/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml
b/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml
index 6030482..d297f47 100644
--- a/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml
+++ b/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml
@@ -28,6 +28,8 @@
<Dt>2020-07-03</Dt>
</Dt>
</Bal>
+
+ <!-- Credit due to incoming SCT -->
<Ntry>
<Amt Ccy="EUR">100.00</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
@@ -52,7 +54,6 @@
<Issr>DK</Issr>
</Prtry>
</BkTxCd>
- <!-- Credit due to incoming SCT -->
<NtryDtls>
<TxDtls>
<Refs>
@@ -100,6 +101,174 @@
</NtryDtls>
<AddtlNtryInf>SEPA GUTSCHRIFT</AddtlNtryInf>
</Ntry>
+
+ <!-- Entry to illustrate multiple ustrd elements -->
+ <Ntry>
+ <Amt Ccy="EUR">50.00</Amt>
+ <CdtDbtInd>CRDT</CdtDbtInd>
+ <Sts>BOOK</Sts>
+ <BookgDt>
+ <Dt>2020-07-02</Dt>
+ </BookgDt>
+ <ValDt>
+ <Dt>2020-07-04</Dt>
+ </ValDt>
+ <AcctSvcrRef>acctsvcrref-002</AcctSvcrRef>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>RCDT</Cd>
+ <SubFmlyCd>ESCT</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+ <Cd>166</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <!-- Credit due to incoming SCT -->
+ <NtryDtls>
+ <TxDtls>
+ <Refs>
+ <EndToEndId>e2e-002</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>Debtor One</Nm>
+ </Dbtr>
+ <DbtrAcct>
+ <Id>
+ <IBAN>DE52123456789473323175</IBAN>
+ </Id>
+ </DbtrAcct>
+ <Cdtr>
+ <Nm>Creditor One</Nm>
+ </Cdtr>
+ </RltdPties>
+ <RmtInf>
+ <Ustrd>unstructured </Ustrd>
+ <Ustrd>info </Ustrd>
+ <Ustrd>across </Ustrd>
+ <Ustrd>lines</Ustrd>
+ </RmtInf>
+ </TxDtls>
+ </NtryDtls>
+ </Ntry>
+
+ <!--
+ Credit due to a return resulting from a batch payment initiation
where only one payment failed.
+ This data was obtained by doing a transaction on a GLS Bank
account, but we've replaced
+ the account's IBAN with a random one.
+ Note how the original creditor and debtor are preserved and not
flipped.
+ Unfortunately the original payment didn't have an end-to-end ID,
so it would be harder
+ to correlate this message to the original payment initiation -->
+ <Ntry>
+ <Amt Ccy="EUR">1.12</Amt>
+ <CdtDbtInd>CRDT</CdtDbtInd>
+ <Sts>BOOK</Sts>
+ <BookgDt>
+ <Dt>2020-06-30</Dt>
+ </BookgDt>
+ <ValDt>
+ <Dt>2020-06-30</Dt>
+ </ValDt>
+ <AcctSvcrRef>2020063011423362000</AcctSvcrRef>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>ICDT</Cd>
+ <SubFmlyCd>RRTN</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+ <Cd>NRTI+159+00931</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <NtryDtls>
+ <TxDtls>
+ <Refs>
+ <EndToEndId>NOTPROVIDED</EndToEndId>
+ </Refs>
+ <AmtDtls>
+ <TxAmt>
+ <Amt Ccy="EUR">1.12</Amt>
+ </TxAmt>
+ </AmtDtls>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>ICDT</Cd>
+ <SubFmlyCd>RRTN</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ <Prtry>
+ <Cd>NRTI+159+00931</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </BkTxCd>
+ <RltdPties>
+ <Dbtr>
+ <Nm>Account Owner</Nm>
+ </Dbtr>
+ <DbtrAcct>
+ <Id>
+ <IBAN>DE54123456784713474163</IBAN>
+ </Id>
+ </DbtrAcct>
+ <Cdtr>
+ <Nm>Nonexistant Creditor</Nm>
+ </Cdtr>
+ <CdtrAcct>
+ <Id>
+ <IBAN>DE24500105177398216438</IBAN>
+ </Id>
+ </CdtrAcct>
+ </RltdPties>
+ <RmtInf>
+ <Ustrd>Retoure SEPA Ueberweisung vom 29.06.2020,
Rueckgabegrund: AC01 IBAN fehlerhaft und ungültig SVWZ: RETURN, Sammelposten
Nummer Zwei IBAN: DE2</Ustrd>
+ <Ustrd>4500105177398216438 BIC: INGDDEFFXXX</Ustrd>
+ </RmtInf>
+ <RtrInf>
+ <OrgnlBkTxCd>
+ <Prtry>
+ <Cd>116</Cd>
+ <Issr>DK</Issr>
+ </Prtry>
+ </OrgnlBkTxCd>
+ <Orgtr>
+ <Id>
+ <OrgId>
+ <BICOrBEI>GENODEM1GLS</BICOrBEI>
+ </OrgId>
+ </Id>
+ </Orgtr>
+ <Rsn>
+ <Cd>AC01</Cd>
+ </Rsn>
+ <AddtlInf>IBAN fehlerhaft und ungültig</AddtlInf>
+ </RtrInf>
+ </TxDtls>
+ </NtryDtls>
+ <AddtlNtryInf>Retouren</AddtlNtryInf>
+ </Ntry>
+
<!-- Credit due to incoming USD transfer -->
<Ntry>
<Amt Ccy="EUR">1000</Amt>
--
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: iso20022,
gnunet <=