libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit daf79284cff57b8138c0efef5fe865ca6eb4509b
parent 2bc9b85d82aabb656fc9894430eaad53ab22f2ce
Author: Antoine A <>
Date:   Mon,  6 May 2024 17:21:24 +0900

nexus: parse GLS instant transaction notifications

Diffstat:
Anexus/sample/platform/gls_camt054.xml | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt | 42+++++++++++++++++++++++++++++++++++++-----
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt | 3++-
Mnexus/src/test/kotlin/Iso20022Test.kt | 18++++++++++++++++++
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