commit e84bca5fbf145f7ef0d7807634a0f0160f33f788 parent f3a93726fbc2e1b6ba95131cf472dcc3166a863f Author: Antoine A <> Date: Thu, 30 Oct 2025 12:57:22 +0100 nexus: add valiant dialect Diffstat:
13 files changed, 553 insertions(+), 68 deletions(-)
diff --git a/nexus/sample/platform/valiant_pain002.xml b/nexus/sample/platform/pain002_accp.xml diff --git a/nexus/sample/platform/pain002.xml b/nexus/sample/platform/pain002_part.xml diff --git a/nexus/sample/platform/valiant_camt052.xml b/nexus/sample/platform/valiant_camt052.xml @@ -0,0 +1,384 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.052.001.04"> + <BkToCstmrAcctRpt> + <Rpt> + <Acct> + <Id> + <IBAN>CH9289144596463965762</IBAN> + </Id> + <Ccy>CHF</Ccy> + </Acct> + <Ntry> + <Amt Ccy="CHF">.1</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <RvslInd>false</RvslInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2025-10-30</Dt> + </BookgDt> + <ValDt> + <Dt>2025-10-30</Dt> + </ValDt> + <AcctSvcrRef>ZV20251030/511372</AcctSvcrRef> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>ICDT</Cd> + <SubFmlyCd>CHRG</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <NtryDtls> + <Btch> + <NbOfTxs>1</NbOfTxs> + <TtlAmt Ccy="CHF">.1</TtlAmt> + <CdtDbtInd>DBIT</CdtDbtInd> + </Btch> + <TxDtls> + <Refs> + <MsgId>MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L</MsgId> + <AcctSvcrRef>ZV20251030/511372/1</AcctSvcrRef> + <PmtInfId>NOTPROVIDED</PmtInfId> + <InstrId>4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE</InstrId> + <EndToEndId>4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE</EndToEndId> + </Refs> + <Amt Ccy="CHF">.1</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">.1</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">.1</Amt> + </TxAmt> + </AmtDtls> + <RltdPties> + <Cdtr> + <Nm>Grothoff Hans</Nm> + </Cdtr> + <CdtrAcct> + <Id> + <IBAN>CH7389144832588726658</IBAN> + </Id> + </CdtrAcct> + </RltdPties> + <RmtInf> + <Ustrd>single 2025-10-30T09:46:04.55293090 9Z</Ustrd> + </RmtInf> + <AddtlTxInf>Vergütung</AddtlTxInf> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>Vergütung</AddtlNtryInf> + </Ntry> + <Ntry> + <Amt Ccy="CHF">.46</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <RvslInd>false</RvslInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2025-10-30</Dt> + </BookgDt> + <ValDt> + <Dt>2025-10-30</Dt> + </ValDt> + <AcctSvcrRef>ZV20251030/511373</AcctSvcrRef> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>ICDT</Cd> + <SubFmlyCd>CHRG</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <NtryDtls> + <Btch> + <NbOfTxs>4</NbOfTxs> + <TtlAmt Ccy="CHF">.46</TtlAmt> + <CdtDbtInd>DBIT</CdtDbtInd> + </Btch> + <TxDtls> + <Refs> + <MsgId>5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U</MsgId> + <AcctSvcrRef>ZV20251030/511373/1</AcctSvcrRef> + <PmtInfId>NOTPROVIDED</PmtInfId> + <InstrId>SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8</InstrId> + <EndToEndId>SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8</EndToEndId> + </Refs> + <Amt Ccy="CHF">.1</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">.1</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">.1</Amt> + </TxAmt> + </AmtDtls> + <RltdPties> + <Cdtr> + <Nm>Grothoff Hans</Nm> + </Cdtr> + <CdtrAcct> + <Id> + <IBAN>CH7389144832588726658</IBAN> + </Id> + </CdtrAcct> + </RltdPties> + <RmtInf> + <Ustrd>multi 0 2025-10-30T09:46:10.3877961 30Z</Ustrd> + </RmtInf> + <AddtlTxInf>Vergütung</AddtlTxInf> + </TxDtls> + <TxDtls> + <Refs> + <MsgId>5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U</MsgId> + <AcctSvcrRef>ZV20251030/511373/2</AcctSvcrRef> + <PmtInfId>NOTPROVIDED</PmtInfId> + <InstrId>RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN</InstrId> + <EndToEndId>RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN</EndToEndId> + </Refs> + <Amt Ccy="CHF">.11</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">.11</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">.11</Amt> + </TxAmt> + </AmtDtls> + <RltdPties> + <Cdtr> + <Nm>Grothoff Hans</Nm> + </Cdtr> + <CdtrAcct> + <Id> + <IBAN>CH7389144832588726658</IBAN> + </Id> + </CdtrAcct> + </RltdPties> + <RmtInf> + <Ustrd>multi 1 2025-10-30T09:46:10.3877961 30Z</Ustrd> + </RmtInf> + <AddtlTxInf>Vergütung</AddtlTxInf> + </TxDtls> + <TxDtls> + <Refs> + <MsgId>5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U</MsgId> + <AcctSvcrRef>ZV20251030/511373/3</AcctSvcrRef> + <PmtInfId>NOTPROVIDED</PmtInfId> + <InstrId>GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7</InstrId> + <EndToEndId>GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7</EndToEndId> + </Refs> + <Amt Ccy="CHF">.12</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">.12</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">.12</Amt> + </TxAmt> + </AmtDtls> + <RltdPties> + <Cdtr> + <Nm>Grothoff Hans</Nm> + </Cdtr> + <CdtrAcct> + <Id> + <IBAN>CH7389144832588726658</IBAN> + </Id> + </CdtrAcct> + </RltdPties> + <RmtInf> + <Ustrd>multi 2 2025-10-30T09:46:10.3877961 30Z</Ustrd> + </RmtInf> + <AddtlTxInf>Vergütung</AddtlTxInf> + </TxDtls> + <TxDtls> + <Refs> + <MsgId>5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U</MsgId> + <AcctSvcrRef>ZV20251030/511373/4</AcctSvcrRef> + <PmtInfId>NOTPROVIDED</PmtInfId> + <InstrId>PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28</InstrId> + <EndToEndId>PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28</EndToEndId> + </Refs> + <Amt Ccy="CHF">.13</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">.13</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">.13</Amt> + </TxAmt> + </AmtDtls> + <RltdPties> + <Cdtr> + <Nm>Grothoff Hans</Nm> + </Cdtr> + <CdtrAcct> + <Id> + <IBAN>CH7389144832588726658</IBAN> + </Id> + </CdtrAcct> + </RltdPties> + <RmtInf> + <Ustrd>multi 3 2025-10-30T09:46:10.3877961 30Z</Ustrd> + </RmtInf> + <AddtlTxInf>Vergütung</AddtlTxInf> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>Vergütung</AddtlNtryInf> + </Ntry> + <Ntry> + <Amt Ccy="CHF">.85</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <RvslInd>false</RvslInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2025-10-30</Dt> + </BookgDt> + <ValDt> + <Dt>2025-10-30</Dt> + </ValDt> + <AcctSvcrRef>ZV20251030/514778</AcctSvcrRef> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>RCDT</Cd> + <SubFmlyCd>OTHR</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <NtryDtls> + <Btch> + <NbOfTxs>1</NbOfTxs> + <TtlAmt Ccy="CHF">.85</TtlAmt> + <CdtDbtInd>CRDT</CdtDbtInd> + </Btch> + <TxDtls> + <Refs> + <AcctSvcrRef>ZV20251030/514778/1</AcctSvcrRef> + <EndToEndId>NOTPROVIDED</EndToEndId> + <TxId>51030655601.0001</TxId> + </Refs> + <Amt Ccy="CHF">.85</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">.85</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">.85</Amt> + </TxAmt> + </AmtDtls> + <RltdPties> + <Dbtr> + <Nm>Grothoff Hans</Nm> + </Dbtr> + <DbtrAcct> + <Id> + <IBAN>CH7389144832588726658</IBAN> + </Id> + </DbtrAcct> + </RltdPties> + <RltdAgts> + <DbtrAgt> + <FinInstnId> + <ClrSysMmbId> + <ClrSysId> + <Cd>CHSIC</Cd> + </ClrSysId> + <MmbId>087042</MmbId> + </ClrSysMmbId> + </FinInstnId> + </DbtrAgt> + </RltdAgts> + <RmtInf> + <Ustrd>fun stuff</Ustrd> + </RmtInf> + <AddtlTxInf>Vergütung</AddtlTxInf> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>Vergütung</AddtlNtryInf> + </Ntry> + <Ntry> + <Amt Ccy="CHF">.95</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <RvslInd>false</RvslInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2025-10-30</Dt> + </BookgDt> + <ValDt> + <Dt>2025-10-30</Dt> + </ValDt> + <AcctSvcrRef>ZV20251030/514779</AcctSvcrRef> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>RCDT</Cd> + <SubFmlyCd>OTHR</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <NtryDtls> + <Btch> + <NbOfTxs>1</NbOfTxs> + <TtlAmt Ccy="CHF">.95</TtlAmt> + <CdtDbtInd>CRDT</CdtDbtInd> + </Btch> + <TxDtls> + <Refs> + <AcctSvcrRef>ZV20251030/514779/1</AcctSvcrRef> + <EndToEndId>NOTPROVIDED</EndToEndId> + <TxId>51030655601.0002</TxId> + </Refs> + <Amt Ccy="CHF">.95</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">.95</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">.95</Amt> + </TxAmt> + </AmtDtls> + <RltdPties> + <Dbtr> + <Nm>Grothoff Hans</Nm> + </Dbtr> + <DbtrAcct> + <Id> + <IBAN>CH7389144832588726658</IBAN> + </Id> + </DbtrAcct> + </RltdPties> + <RltdAgts> + <DbtrAgt> + <FinInstnId> + <ClrSysMmbId> + <ClrSysId> + <Cd>CHSIC</Cd> + </ClrSysId> + <MmbId>087042</MmbId> + </ClrSysMmbId> + </FinInstnId> + </DbtrAgt> + </RltdAgts> + <RmtInf> + <Ustrd>Taler PC2MKG0B7CK32K1T7DP08P6E1B7FHB6HY6R Q0PT3VTPBPRPYM1B0</Ustrd> + </RmtInf> + <AddtlTxInf>Vergütung</AddtlTxInf> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>Vergütung</AddtlNtryInf> + </Ntry> + </Rpt> + </BkToCstmrAcctRpt> +</Document> +\ No newline at end of file diff --git a/nexus/sample/platform/valiant_pain001.xml b/nexus/sample/platform/valiant_pain001.xml @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.ch.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>BIC</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> +\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt @@ -91,10 +91,11 @@ class NexusEbicsConfig( /** Bank account payto */ val payto = IbanPayto.build(account.iban, account.bic, account.name) - val dialect = sect.map("bank_dialect", "dialect", mapOf( + val dialect = sect.map("bank_dialect", "bank dialect", mapOf( "postfinance" to Dialect.postfinance, "gls" to Dialect.gls, "maerki_baumann" to Dialect.maerki_baumann, + "valiant" to Dialect.valiant, )).require() /** Path where we store the bank public keys */ diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt @@ -219,7 +219,7 @@ suspend fun registerTxs( xml: InputStream ): Int { var nbTx: Int = 0 - parseTx(xml, cfg.ebics.dialect).forEach { accountTx -> + parseTx(xml).forEach { accountTx -> if (accountTx.iban == cfg.ebics.account.iban) { require(accountTx.currency == null || accountTx.currency == cfg.currency) { "Expected transactions of currency ${cfg.currency} got ${accountTx.currency}" } accountTx.txs.forEach { tx -> diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt @@ -147,63 +147,84 @@ enum class OrderDoc { } } +/** Supported EBICS standard */ +enum class Standard { + /// Swiss Payment Standards + SIX, + /// German Banking Industry Committee + GBIC; + + fun downloadDoc(doc: OrderDoc, ebics2: Boolean): List<EbicsOrder> = when (this) { + SIX -> { + if (ebics2) { + when (doc) { + OrderDoc.acknowledgement -> listOf(EbicsOrder.V2_5("HAC", "DZHNN")) + OrderDoc.status -> listOf(EbicsOrder.V2_5("Z01", "DZHNN")) + OrderDoc.report -> listOf(EbicsOrder.V2_5("Z52", "DZHNN")) + OrderDoc.statement -> listOf(EbicsOrder.V2_5("Z53", "DZHNN")) + OrderDoc.notification -> listOf(EbicsOrder.V2_5("Z54", "DZHNN")) + } + } else { + when (doc) { + OrderDoc.acknowledgement -> listOf(EbicsOrder.V3.HAC) + OrderDoc.status -> listOf(EbicsOrder.V3("BTD", "PSR", "CH", "pain.002", "10", "ZIP")) + OrderDoc.report -> listOf(EbicsOrder.V3("BTD", "STM", "CH", "camt.052", "08", "ZIP")) + OrderDoc.statement -> listOf(EbicsOrder.V3("BTD", "EOP", "CH", "camt.053", "08", "ZIP")) + OrderDoc.notification -> listOf(EbicsOrder.V3("BTD", "REP", "CH", "camt.054", "08", "ZIP")) + } + } + } + GBIC -> when (doc) { + OrderDoc.acknowledgement -> listOf(EbicsOrder.V3.HAC) + OrderDoc.status -> listOf( + EbicsOrder.V3("BTD", "REP", "DE", "pain.002", null, "ZIP", "SCI"), + EbicsOrder.V3("BTD", "REP", "DE", "pain.002", null, "ZIP", "SCT") + ) + OrderDoc.report -> listOf(EbicsOrder.V3("BTD", "STM", "DE", "camt.052", null, "ZIP")) + OrderDoc.statement -> listOf(EbicsOrder.V3("BTD", "EOP", "DE", "camt.053", null, "ZIP")) + OrderDoc.notification -> listOf( + EbicsOrder.V3("BTD", "STM", "DE", "camt.054", null, "ZIP"), + EbicsOrder.V3("BTD", "STM", "DE", "camt.054", null, "ZIP", "SCI") + ) + } + } + + fun directDebit(): EbicsOrder = when (this) { + SIX -> EbicsOrder.V3("BTU", "MCT", "CH", "pain.001", "09") + GBIC -> EbicsOrder.V3("BTU", "SCT", null, "pain.001") + } + + fun instantDirectDebit(): EbicsOrder? = when (this) { + SIX -> null + GBIC -> EbicsOrder.V3("BTU", "SCI", "DE", "pain.001") + } +} + +/** Supported bank dialects */ enum class Dialect { + valiant, postfinance, gls, maerki_baumann; + fun standard(): Standard = when (this) { + valiant, postfinance, maerki_baumann -> Standard.SIX + gls -> Standard.GBIC + } + fun downloadDoc(doc: OrderDoc, ebics2: Boolean): List<EbicsOrder> { - return when (this) { - postfinance -> { - if (ebics2) { - when (doc) { - OrderDoc.acknowledgement -> listOf(EbicsOrder.V2_5("HAC", "DZHNN")) - OrderDoc.status -> listOf(EbicsOrder.V2_5("Z01", "DZHNN")) - OrderDoc.report -> listOf(EbicsOrder.V2_5("Z52", "DZHNN")) - OrderDoc.statement -> listOf(EbicsOrder.V2_5("Z53", "DZHNN")) - OrderDoc.notification -> listOf(EbicsOrder.V2_5("Z54", "DZHNN")) - } - } else { - when (doc) { - OrderDoc.acknowledgement -> listOf(EbicsOrder.V3.HAC) - OrderDoc.status -> listOf(EbicsOrder.V3("BTD", "PSR", "CH", "pain.002", "10", "ZIP")) - OrderDoc.report -> listOf(EbicsOrder.V3("BTD", "STM", "CH", "camt.052", "08", "ZIP")) - OrderDoc.statement -> listOf(EbicsOrder.V3("BTD", "EOP", "CH", "camt.053", "08", "ZIP")) - OrderDoc.notification -> listOf(EbicsOrder.V3("BTD", "REP", "CH", "camt.054", "08", "ZIP")) - } - } - } - gls -> when (doc) { - OrderDoc.acknowledgement -> listOf(EbicsOrder.V3.HAC) - OrderDoc.status -> listOf( - EbicsOrder.V3("BTD", "REP", "DE", "pain.002", null, "ZIP", "SCI"), - EbicsOrder.V3("BTD", "REP", "DE", "pain.002", null, "ZIP", "SCT") - ) - OrderDoc.report -> listOf(EbicsOrder.V3("BTD", "STM", "DE", "camt.052", null, "ZIP")) - OrderDoc.statement -> listOf(EbicsOrder.V3("BTD", "EOP", "DE", "camt.053", null, "ZIP")) - OrderDoc.notification -> listOf( - EbicsOrder.V3("BTD", "STM", "DE", "camt.054", null, "ZIP"), - EbicsOrder.V3("BTD", "STM", "DE", "camt.054", null, "ZIP", "SCI") - ) - } - maerki_baumann -> throw IllegalArgumentException("Maerki Baumann does not have EBICS access") - } + if (this == maerki_baumann) throw IllegalArgumentException("Maerki Baumann does not have EBICS access") + return this.standard().downloadDoc(doc, ebics2) } fun directDebit(): EbicsOrder { - return when (this) { - postfinance -> EbicsOrder.V3("BTU", "MCT", "CH", "pain.001", "09") - gls -> EbicsOrder.V3("BTU", "SCT", null, "pain.001") - maerki_baumann -> throw IllegalArgumentException("Maerki Baumann does not have EBICS access") - } + if (this == maerki_baumann) throw IllegalArgumentException("Maerki Baumann does not have EBICS access") + return this.standard().directDebit() } fun instantDirectDebit(): EbicsOrder? { - return when (this) { - postfinance -> null - gls -> EbicsOrder.V3("BTU", "SCI", "DE", "pain.001") - maerki_baumann -> throw IllegalArgumentException("Maerki Baumann does not have EBICS access") - } + if (this == maerki_baumann) throw IllegalArgumentException("Maerki Baumann does not have EBICS access") + return this.standard().instantDirectDebit() } /** All orders required for a dialect implementation to work */ diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt @@ -20,7 +20,6 @@ package tech.libeufin.nexus.iso20022 import tech.libeufin.common.* import tech.libeufin.nexus.* -import tech.libeufin.nexus.ebics.Dialect import java.io.InputStream import java.time.Instant import java.time.ZoneOffset @@ -477,7 +476,7 @@ data class AccountTransactions( } /** Parse camt.054 or camt.053 file */ -fun parseTx(notifXml: InputStream, dialect: Dialect): List<AccountTransactions> { +fun parseTx(notifXml: InputStream): List<AccountTransactions> { /* In ISO 20022 specifications, most fields are optional and the same information can be written several times in different places. For libeufin, we're only @@ -500,7 +499,7 @@ fun parseTx(notifXml: InputStream, dialect: Dialect): List<AccountTransactions> disadvantage of being known only by the account servicing institution. They should therefore only be used as a last resort. */ - logger.trace("Parse transactions camt file for $dialect") + logger.trace("Parse transactions camt file") val accountTxs = mutableListOf<AccountTransactions>() /** Common parsing logic for camt.052, camt.053 and camt.054 */ diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/pain001.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/pain001.kt @@ -20,7 +20,7 @@ package tech.libeufin.nexus.iso20022 import tech.libeufin.common.* import tech.libeufin.nexus.* -import tech.libeufin.nexus.ebics.Dialect +import tech.libeufin.nexus.ebics.* import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime @@ -58,9 +58,9 @@ fun createPain001( instant: Boolean ): ByteArray { val version = "09" - val suffix = when (dialect) { - Dialect.postfinance, Dialect.maerki_baumann -> ".ch.03" - Dialect.gls -> "" + val suffix = when (dialect.standard()) { + Standard.SIX -> ".ch.03" + Standard.GBIC -> "" } val zonedTimestamp = ZonedDateTime.ofInstant(msg.timestamp, ZoneId.of("UTC")) val totalAmount = getAmountNoCurrency(msg.sum) diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -140,7 +140,7 @@ class Iso20022Test { @Test fun postfinance_camt054() { assertContentEquals( - parseTx(Path("sample/platform/postfinance_camt054.xml").inputStream(), Dialect.postfinance), + parseTx(Path("sample/platform/postfinance_camt054.xml").inputStream()), listOf(AccountTransactions( iban = "CH9289144596463965762", currency = "CHF", @@ -184,7 +184,7 @@ class Iso20022Test { @Test fun postfinance_camt053() { assertContentEquals( - parseTx(Path("sample/platform/postfinance_camt053.xml").inputStream(), Dialect.postfinance), + parseTx(Path("sample/platform/postfinance_camt053.xml").inputStream()), listOf(AccountTransactions( iban = "CH9289144596463965762", currency = "CHF", @@ -211,9 +211,71 @@ class Iso20022Test { } @Test + fun valiant_camt052() { + assertContentEquals( + parseTx(Path("sample/platform/valiant_camt052.xml").inputStream()), + listOf(AccountTransactions( + iban = "CH9289144596463965762", + currency = "CHF", + txs = listOf( + OutgoingPayment( + id = OutgoingId("MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L", "4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE", "ZV20251030/511372/1"), + amount = TalerAmount("CHF:0.1"), + creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans"), + subject = "single 2025-10-30T09:46:04.55293090 9Z", + executionTime = dateToInstant("2025-10-30") + ), + OutgoingPayment( + id = OutgoingId("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U", "SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8", "ZV20251030/511373/1"), + amount = TalerAmount("CHF:0.1"), + creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans"), + subject = "multi 0 2025-10-30T09:46:10.3877961 30Z", + executionTime = dateToInstant("2025-10-30") + ), + OutgoingPayment( + id = OutgoingId("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U", "RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN", "ZV20251030/511373/2"), + amount = TalerAmount("CHF:0.11"), + creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans"), + subject = "multi 1 2025-10-30T09:46:10.3877961 30Z", + executionTime = dateToInstant("2025-10-30") + ), + OutgoingPayment( + id = OutgoingId("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U", "GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7", "ZV20251030/511373/3"), + amount = TalerAmount("CHF:0.12"), + creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans"), + subject = "multi 2 2025-10-30T09:46:10.3877961 30Z", + executionTime = dateToInstant("2025-10-30") + ), + OutgoingPayment( + id = OutgoingId("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U", "PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28", "ZV20251030/511373/4"), + amount = TalerAmount("CHF:0.13"), + creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans"), + subject = "multi 3 2025-10-30T09:46:10.3877961 30Z", + executionTime = dateToInstant("2025-10-30") + ), + IncomingPayment( + id = IncomingId(null, "51030655601.0001", "ZV20251030/514778/1"), + amount = TalerAmount("CHF:0.85"), + subject = "fun stuff", + executionTime = dateToInstant("2025-10-30"), + debtor = ibanPayto("CH7389144832588726658", "Grothoff Hans"), + ), + IncomingPayment( + id = IncomingId(null, "51030655601.0002", "ZV20251030/514779/1"), + amount = TalerAmount("CHF:0.95"), + subject = "Taler PC2MKG0B7CK32K1T7DP08P6E1B7FHB6HY6R Q0PT3VTPBPRPYM1B0", + executionTime = dateToInstant("2025-10-30"), + debtor = ibanPayto("CH7389144832588726658", "Grothoff Hans"), + ), + ) + )) + ) + } + + @Test fun gls_camt052() { assertContentEquals( - parseTx(Path("sample/platform/gls_camt052.xml").inputStream(), Dialect.gls), + parseTx(Path("sample/platform/gls_camt052.xml").inputStream()), listOf(AccountTransactions( iban = "DE84500105177118117964", currency = "EUR", @@ -280,7 +342,7 @@ class Iso20022Test { @Test fun gls_camt053() { assertContentEquals( - parseTx(Path("sample/platform/gls_camt053.xml").inputStream(), Dialect.gls), + parseTx(Path("sample/platform/gls_camt053.xml").inputStream()), listOf(AccountTransactions( iban = "DE84500105177118117964", currency = "EUR", @@ -335,7 +397,7 @@ class Iso20022Test { @Test fun gls_camt054() { assertContentEquals( - parseTx(Path("sample/platform/gls_camt054.xml").inputStream(), Dialect.gls), + parseTx(Path("sample/platform/gls_camt054.xml").inputStream()), listOf(AccountTransactions( iban = "DE84500105177118117964", currency = "EUR", @@ -355,7 +417,7 @@ class Iso20022Test { @Test fun maerki_baumann_camt053() { assertContentEquals( - parseTx(Path("sample/platform/maerki_baumann_camt053.xml").inputStream(), Dialect.maerki_baumann), + parseTx(Path("sample/platform/maerki_baumann_camt053.xml").inputStream()), listOf(AccountTransactions( iban = "CH7389144832588726658", currency = "CHF", @@ -458,7 +520,7 @@ class Iso20022Test { @Test fun pain002() { assertEquals( - parseCustomerPaymentStatusReport(Path("sample/platform/pain002.xml").inputStream()), + parseCustomerPaymentStatusReport(Path("sample/platform/pain002_part.xml").inputStream()), MsgStatus( id = "05BD4C5B4A2649B5B08F6EF6A31F197A", code = ExternalPaymentGroupStatusCode.PART, @@ -512,5 +574,19 @@ class Iso20022Test { ) ) ) + assertEquals( + parseCustomerPaymentStatusReport(Path("sample/platform/pain002_accp.xml").inputStream()), + MsgStatus( + id = "5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U", + code = ExternalPaymentGroupStatusCode.ACCP, + reasons = listOf( + Reason( + code = null, + information = "PN10630020F0297329.20251030104613.EBTUAAAC.PN1.0002372" + ) + ), + payments = emptyList() + ) + ) } } \ No newline at end of file diff --git a/nexus/src/test/kotlin/RegistrationTest.kt b/nexus/src/test/kotlin/RegistrationTest.kt @@ -268,7 +268,7 @@ class RegistrationTest { )) // Register pain files - db.register(cfg, "sample/platform/pain002.xml", OrderDoc.status) + db.register(cfg, "sample/platform/pain002_part.xml", OrderDoc.status) // Check state db.check( diff --git a/testbench/clean_test_logs.py b/testbench/clean_test_logs.py @@ -64,8 +64,9 @@ for platform in DIR.iterdir(): for file in payload_dir_path.iterdir(): content = file.read_text() rm_if_similar(file, content) - - if ( + if not any(payload_dir_path.iterdir()): + remove(request, "empty request") + elif ( request.name != "fetch" and request.name != "submit" and not payload_file_path.exists() diff --git a/testbench/src/test/kotlin/Iso20022Test.kt b/testbench/src/test/kotlin/Iso20022Test.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. + * Copyright (C) 2024-2025 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 @@ -42,7 +42,7 @@ class Iso20022Test { } else if (name.contains("pain.002") || name.contains("pain002") ) { parseCustomerPaymentStatusReport(content) } else { - parseTx(content, Dialect.postfinance) + parseTx(content) } } @@ -55,7 +55,7 @@ class Iso20022Test { } else if (name.contains("pain.002") || name.contains("pain002") ) { parseCustomerPaymentStatusReport(content) } else { - parseTx(content, Dialect.postfinance) + parseTx(content) } } } @@ -121,7 +121,7 @@ class Iso20022Test { } else if ( !name.contains("camt.052") && !name.contains("_C52_") && !name.contains("_Z01_") ) { - parseTx(content, dialect) + parseTx(content) } } }