commit b490ddbec8a471d97370d2a20b92cdfd558533a0
parent 5e46be49604cd727c8ee0fb3f63b3a5a8d627005
Author: Antoine A <>
Date: Tue, 27 May 2025 17:12:45 +0200
nexus: add account restriction feature
Diffstat:
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")
)
)
)