commit daf79284cff57b8138c0efef5fe865ca6eb4509b
parent 2bc9b85d82aabb656fc9894430eaad53ab22f2ce
Author: Antoine A <>
Date: Mon, 6 May 2024 17:21:24 +0900
nexus: parse GLS instant transaction notifications
Diffstat:
4 files changed, 128 insertions(+), 6 deletions(-)
diff --git a/nexus/sample/platform/gls_camt054.xml b/nexus/sample/platform/gls_camt054.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.054.001.08">
+ <BkToCstmrDbtCdtNtfctn>
+ <GrpHdr>
+ <MsgId>IS11PGENODEFF2DA8899900378806</MsgId>
+ </GrpHdr>
+ <Ntfctn>
+ <Ntry>
+ <Amt Ccy="EUR">2.50</Amt>
+ <CdtDbtInd>CRDT</CdtDbtInd>
+ <Sts>
+ <Cd>BOOK</Cd>
+ </Sts>
+ <ValDt>
+ <Dt>2024-05-05</Dt>
+ </ValDt>
+ <BkTxCd>
+ <Domn>
+ <Cd>PMNT</Cd>
+ <Fmly>
+ <Cd>RRCT</Cd>
+ <SubFmlyCd>ESCT</SubFmlyCd>
+ </Fmly>
+ </Domn>
+ </BkTxCd>
+ <NtryDtls>
+ <TxDtls>
+ <Refs>
+ <EndToEndId>NOTPROVIDED</EndToEndId>
+ <TxId>IS11PGENODEFF2DA8899900378806</TxId>
+ </Refs>
+ <RltdPties>
+ <Dbtr>
+ <Nm>Mr Test</Nm>
+ </Dbtr>
+ <DbtrAcct>
+ <Id>
+ <IBAN>DE84500105177118117964</IBAN>
+ </Id>
+ </DbtrAcct>
+ <Cdtr>
+ <Nm>John Smith</Nm>
+ </Cdtr>
+ <CdtrAcct>
+ <Id>
+ <IBAN>DE20500105172419259181</IBAN>
+ </Id>
+ </CdtrAcct>
+ </RltdPties>
+ <RltdAgts>
+ <DbtrAgt>
+ <FinInstnId>
+ <BICFI>BYLADEM1WOR</BICFI>
+ </FinInstnId>
+ </DbtrAgt>
+ <CdtrAgt>
+ <FinInstnId>
+ <BICFI>GENODEM1GLS</BICFI>
+ </FinInstnId>
+ </CdtrAgt>
+ </RltdAgts>
+ <RmtInf>
+ <Ustrd>Test ICT</Ustrd>
+ </RmtInf>
+ </TxDtls>
+ </NtryDtls>
+ </Ntry>
+ </Ntfctn>
+ </BkToCstmrDbtCdtNtfctn>
+</Document>
+\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -343,8 +343,10 @@ fun parseTx(
}
}
- fun XmlDestructor.bookDate() =
- one("BookgDt").one("Dt").date().atStartOfDay().toInstant(ZoneOffset.UTC)
+ fun XmlDestructor.executionDate(): Instant {
+ // Value date if present else booking date
+ return (opt("ValDt") ?: one("BookgDt")).one("Dt").date().atStartOfDay().toInstant(ZoneOffset.UTC)
+ }
fun XmlDestructor.nexusId(): String? =
opt("Refs") { opt("EndToEndId")?.textProvided() ?: opt("MsgId")?.text() }
@@ -392,7 +394,7 @@ fun parseTx(
each("Ntry") {
val entryRef = opt("AcctSvcrRef")?.text()
assertBooked(entryRef)
- val bookDate = bookDate()
+ val bookDate = executionDate()
val kind = one("CdtDbtInd").enum<Kind>()
val amount = amount(acceptedCurrency)
one("NtryDtls").one("TxDtls") { // TODO handle batches
@@ -446,6 +448,36 @@ fun parseTx(
opt("BkToCstmrStmt")?.each("Stmt") { // Camt.053
parseGlsInner()
}
+ opt("BkToCstmrDbtCdtNtfctn")?.each("Ntfctn") { // Camt.054
+ opt("Acct") {
+ // Sanity check on currency and IBAN ?
+ }
+ each("Ntry") {
+ val entryRef = opt("AcctSvcrRef")?.text()
+ assertBooked(entryRef)
+ val bookDate = executionDate()
+ val kind = one("CdtDbtInd").enum<Kind>()
+ val amount = amount(acceptedCurrency)
+ if (!isReversalCode()) {
+ one("NtryDtls").one("TxDtls") {
+ val txRef = one("Refs").opt("AcctSvcrRef")?.text()
+ val subject = opt("RmtInf")?.map("Ustrd") { text() }?.joinToString("")
+ if (kind == Kind.CRDT) {
+ val bankId = one("Refs").opt("TxId")?.text()
+ val debtorPayto = opt("RltdPties") { payto("Dbtr") }
+ txsInfo.add(TxInfo.Credit(
+ ref = bankId ?: txRef ?: entryRef,
+ bookDate = bookDate,
+ bankId = bankId,
+ amount = amount,
+ subject = subject,
+ debtorPayto = debtorPayto
+ ))
+ }
+ }
+ }
+ }
+ }
}
Dialect.postfinance -> {
opt("BkToCstmrStmt")?.each("Stmt") { // Camt.053
@@ -455,7 +487,7 @@ fun parseTx(
each("Ntry") {
val entryRef = opt("AcctSvcrRef")?.text()
assertBooked(entryRef)
- val bookDate = bookDate()
+ val bookDate = executionDate()
if (isReversalCode()) {
one("NtryDtls").one("TxDtls") {
val kind = one("CdtDbtInd").enum<Kind>()
@@ -481,7 +513,7 @@ fun parseTx(
each("Ntry") {
val entryRef = opt("AcctSvcrRef")?.text()
assertBooked(entryRef)
- val bookDate = bookDate()
+ val bookDate = executionDate()
if (!isReversalCode()) {
one("NtryDtls").each("TxDtls") {
val kind = one("CdtDbtInd").enum<Kind>()
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt
@@ -62,11 +62,12 @@ enum class Dialect {
}
}
}
+ // TODO for GLS we might have to fetch the same kind of files from multiple orders
gls -> when (doc) {
SupportedDocument.PAIN_002 -> EbicsOrder.V3("BTD", "REP", "DE", "pain.002", null, "ZIP", "SCT")
SupportedDocument.CAMT_052 -> EbicsOrder.V3("BTD", "STM", "DE", "camt.052", null, "ZIP")
SupportedDocument.CAMT_053 -> EbicsOrder.V3("BTD", "EOP", "DE", "camt.053", null, "ZIP")
- SupportedDocument.CAMT_054 -> EbicsOrder.V3("BTD", "STM", "DE", "camt.054", null, "ZIP")
+ SupportedDocument.CAMT_054 -> EbicsOrder.V3("BTD", "STM", "DE", "camt.054", null, "ZIP", "SCI")
SupportedDocument.PAIN_002_LOGS -> EbicsOrder.V3("HAC")
}
}
diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt
@@ -163,4 +163,22 @@ class Iso20022Test {
txs
)
}
+
+ @Test
+ fun gls_camt054() {
+ val content = Files.newInputStream(Path("sample/platform/gls_camt054.xml"))
+ val txs = parseTx(content, "EUR", Dialect.gls)
+ assertEquals(
+ listOf(
+ IncomingPayment(
+ bankId = "IS11PGENODEFF2DA8899900378806",
+ amount = TalerAmount("EUR:2.5"),
+ wireTransferSubject = "Test ICT",
+ executionTime = instant("2024-05-05"),
+ debitPaytoUri = "payto://iban/DE84500105177118117964?receiver-name=Mr+Test"
+ )
+ ),
+ txs
+ )
+ }
}
\ No newline at end of file