libeufin

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

commit b490ddbec8a471d97370d2a20b92cdfd558533a0
parent 5e46be49604cd727c8ee0fb3f63b3a5a8d627005
Author: Antoine A <>
Date:   Tue, 27 May 2025 17:12:45 +0200

nexus: add account restriction feature

Diffstat:
Mcommon/src/main/kotlin/TalerConfig.kt | 3+++
Mcontrib/nexus.conf | 3+++
Mnexus/conf/maerki_baumann.conf | 3+++
Mnexus/sample/platform/maerki_baumann_camt053.xml | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/Config.kt | 5++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 6++++++
Mnexus/src/test/kotlin/Iso20022Test.kt | 20++++++++++++++------
Mnexus/src/test/kotlin/RegistrationTest.kt | 15+++++++++++++++
8 files changed, 132 insertions(+), 7 deletions(-)

diff --git a/common/src/main/kotlin/TalerConfig.kt b/common/src/main/kotlin/TalerConfig.kt @@ -458,6 +458,9 @@ class TalerConfigSection internal constructor( /** Access [option] as String */ fun string(option: String) = option(option, "string") { it } + /** Access [option] as Regex */ + fun regex(option: String) = option(option, "regex") { Regex(it) } + /** Access [option] as BaseURL */ fun baseURL(option: String) = option(option, "baseURL") { BaseURL.parse(it) } diff --git a/contrib/nexus.conf b/contrib/nexus.conf @@ -72,6 +72,9 @@ CHECKPOINT_TIME_OF_DAY = 19:00 # An additional fee to deduce from the bounced amount # BOUNCE_FEE = KUDOS:0 +# Bounce transactions coming from account not matching this regex +# RESTRICTION_PAYTO_REGEX = payto://iban/CH.* + [nexus-submit] # How often should ebics-fetch submit pending transactions FREQUENCY = 30m diff --git a/nexus/conf/maerki_baumann.conf b/nexus/conf/maerki_baumann.conf @@ -11,5 +11,8 @@ IBAN = CH7389144832588726658 BIC = BIC NAME = myname +[nexus-fetch] +RESTRICTION_PAYTO_REGEX = payto://iban/CH.* + [libeufin-nexusdb-postgres] CONFIG = postgres:///libeufincheck \ No newline at end of file diff --git a/nexus/sample/platform/maerki_baumann_camt053.xml b/nexus/sample/platform/maerki_baumann_camt053.xml @@ -813,6 +813,90 @@ </NtryDtls> <AddtlNtryInf>Transfer Taler Operations AG</AddtlNtryInf> </Ntry> + <Ntry> + <Amt Ccy="CHF">9.8</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <RvslInd>false</RvslInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2025-05-26</Dt> + </BookgDt> + <ValDt> + <Dt>2025-05-26</Dt> + </ValDt> + <AcctSvcrRef>ZV20250526/852733</AcctSvcrRef> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>RCDT</Cd> + <SubFmlyCd>OTHR</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <NtryDtls> + <Btch> + <NbOfTxs>1</NbOfTxs> + <TtlAmt Ccy="CHF">9.8</TtlAmt> + <CdtDbtInd>CRDT</CdtDbtInd> + </Btch> + <TxDtls> + <Refs> + <AcctSvcrRef>ZV20250526/852733/1</AcctSvcrRef> + <EndToEndId>6b515f17ecc9408191f7b9b1d755faf7</EndToEndId> + <TxId>F000787951230001</TxId> + </Refs> + <Amt Ccy="CHF">9.8</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">10</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">10</Amt> + </TxAmt> + </AmtDtls> + <Chrgs> + <Rcrd> + <Amt Ccy="CHF">.2</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <ChrgInclInd>true</ChrgInclInd> + <Tp> + <Prtry> + <Id>PT inc.paym.exp</Id> + </Prtry> + </Tp> + <Br>CRED</Br> + </Rcrd> + </Chrgs> + <RltdPties> + <Dbtr> + <Nm>Mr German</Nm> + </Dbtr> + <DbtrAcct> + <Id> + <IBAN>DE20500105172419259181</IBAN> + </Id> + </DbtrAcct> + </RltdPties> + <RltdAgts> + <DbtrAgt> + <FinInstnId> + <ClrSysMmbId> + <ClrSysId> + <Cd>CHSIC</Cd> + </ClrSysId> + <MmbId>083077</MmbId> + </ClrSysMmbId> + </FinInstnId> + </DbtrAgt> + </RltdAgts> + <RmtInf> + <Ustrd>Taler XT3D9MADR4V85JBWX47SMJFDQD2FDZDHHPH8R25YDG1KNVTSEH6G</Ustrd> + </RmtInf> + </TxDtls> + </NtryDtls> + </Ntry> </Stmt> </BkToCstmrStmt> </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 @@ -32,12 +32,13 @@ data class NexusIngestConfig( val accountType: AccountType, val ignoreTransactionsBefore: Instant, val ignoreBouncesBefore: Instant, + val restrictionPaytoRegex: Regex?, val bounceDeduceFee: Boolean, val bounceFee: TalerAmount ) { companion object { fun default(accountType: AccountType, currency: String = "KUDOS") - = NexusIngestConfig(accountType, Instant.MIN, Instant.MIN, false, TalerAmount.zero(currency)) + = NexusIngestConfig(accountType, Instant.MIN, Instant.MIN, null, false, TalerAmount.zero(currency)) } } @@ -48,6 +49,7 @@ class NexusFetchConfig(config: TalerConfig, currency: String) { val checkpointTime = section.time("checkpoint_time_of_day").require() val ignoreTransactionsBefore = section.date("ignore_transactions_before").default(Instant.MIN) val ignoreBouncesBefore = section.date("ignore_bounces_before").default(Instant.MIN) + val restrictionPaytoRegex = section.regex("restriction_payto_regex").orNull() val bounceDeduceFee = section.boolean("bounce_deduce_fee").default(false) val bounceFee = section.amount("bounce_fee", currency).default(TalerAmount.zero(currency)) } @@ -123,6 +125,7 @@ class NexusConfig internal constructor (val cfg: TalerConfig) { accountType, fetch.ignoreTransactionsBefore, fetch.ignoreBouncesBefore, + fetch.restrictionPaytoRegex, fetch.bounceDeduceFee, fetch.bounceFee ) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt @@ -167,6 +167,12 @@ suspend fun registerIncomingPayment( logRes(res, kind = "incomplete") return } + if (cfg.restrictionPaytoRegex != null) { + if (!cfg.restrictionPaytoRegex.matches(payment.debtor.toString())) { + bounce("restricted account") + return + } + } // Else we try to parse the incoming subject runCatching { parseIncomingSubject(payment.subject) }.fold( onSuccess = { metadata -> diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -139,7 +139,7 @@ class Iso20022Test { @Test fun postfinance_camt054() { - assertEquals( + assertContentEquals( parseTx(Path("sample/platform/postfinance_camt054.xml").inputStream(), Dialect.postfinance), listOf(AccountTransactions( iban = "CH9289144596463965762", @@ -183,7 +183,7 @@ class Iso20022Test { @Test fun postfinance_camt053() { - assertEquals( + assertContentEquals( parseTx(Path("sample/platform/postfinance_camt053.xml").inputStream(), Dialect.postfinance), listOf(AccountTransactions( iban = "CH9289144596463965762", @@ -212,7 +212,7 @@ class Iso20022Test { @Test fun gls_camt052() { - assertEquals( + assertContentEquals( parseTx(Path("sample/platform/gls_camt052.xml").inputStream(), Dialect.gls), listOf(AccountTransactions( iban = "DE84500105177118117964", @@ -279,7 +279,7 @@ class Iso20022Test { @Test fun gls_camt053() { - assertEquals( + assertContentEquals( parseTx(Path("sample/platform/gls_camt053.xml").inputStream(), Dialect.gls), listOf(AccountTransactions( iban = "DE84500105177118117964", @@ -334,7 +334,7 @@ class Iso20022Test { @Test fun gls_camt054() { - assertEquals( + assertContentEquals( parseTx(Path("sample/platform/gls_camt054.xml").inputStream(), Dialect.gls), listOf(AccountTransactions( iban = "DE84500105177118117964", @@ -354,7 +354,7 @@ class Iso20022Test { @Test fun maerki_baumann_camt053() { - assertEquals( + assertContentEquals( parseTx(Path("sample/platform/maerki_baumann_camt053.xml").inputStream(), Dialect.maerki_baumann), listOf(AccountTransactions( iban = "CH7389144832588726658", @@ -442,6 +442,14 @@ class Iso20022Test { executionTime = dateToInstant("2025-01-27"), debtor = null ), + IncomingPayment( + id = IncomingId(null, "F000787951230001", "ZV20250526/852733/1"), + amount = TalerAmount("CHF:10"), + creditFee = TalerAmount("CHF:0.2"), + subject = "Taler XT3D9MADR4V85JBWX47SMJFDQD2FDZDHHPH8R25YDG1KNVTSEH6G", + executionTime = dateToInstant("2025-05-26"), + debtor = ibanPayto("DE20500105172419259181", "Mr German") + ), ) )) ) diff --git a/nexus/src/test/kotlin/RegistrationTest.kt b/nexus/src/test/kotlin/RegistrationTest.kt @@ -514,6 +514,14 @@ class RegistrationTest { executionTime = dateToInstant("2025-01-27"), debtor = null ), + IncomingPayment( + id = IncomingId(null, "F000787951230001", "ZV20250526/852733/1"), + amount = TalerAmount("CHF:10"), + creditFee = TalerAmount("CHF:0.2"), + subject = "Taler XT3D9MADR4V85JBWX47SMJFDQD2FDZDHHPH8R25YDG1KNVTSEH6G", + executionTime = dateToInstant("2025-05-26"), + debtor = ibanPayto("DE20500105172419259181", "Mr German") + ), ), outgoing = listOf( OutgoingPayment( @@ -580,6 +588,13 @@ class RegistrationTest { subject = "bounce: 81b0d8c6-a677-4577-b75e-a639dcc03681", executionTime = Instant.EPOCH, creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans") + ), + OutgoingPayment( + id = OutgoingId(null, null, null), + amount = TalerAmount("CHF:10"), + subject = "bounce: F000787951230001", + executionTime = Instant.EPOCH, + creditor = ibanPayto("DE20500105172419259181", "Mr German") ) ) )