commit 5e46be49604cd727c8ee0fb3f63b3a5a8d627005
parent d34f0a94bd14d9ce328c48937c9f7c5f61845e3f
Author: Antoine A <>
Date: Tue, 27 May 2025 16:17:06 +0200
nexus: improve bounce fee logic
Diffstat:
5 files changed, 45 insertions(+), 12 deletions(-)
diff --git a/contrib/nexus.conf b/contrib/nexus.conf
@@ -66,6 +66,12 @@ CHECKPOINT_TIME_OF_DAY = 19:00
# Ignore all malformed transactions prior to a certain date, useful when you want to import old transactions without bouncing the malformed ones a second time
# IGNORE_BOUNCES_BEFORE = YYYY-MM-DD
+# Wether to deduce the fee payed by the exchange account from the bounced amount
+# BOUNCE_DEDUCE_FEE = NO
+
+# An additional fee to deduce from the bounced amount
+# BOUNCE_FEE = KUDOS:0
+
[nexus-submit]
# How often should ebics-fetch submit pending transactions
FREQUENCY = 30m
diff --git a/nexus/conf/skip.conf b/nexus/conf/skip.conf
@@ -1,5 +1,5 @@
[nexus-ebics]
-CURRENCY = CHF
+CURRENCY = KUDOS
BANK_DIALECT = postfinance
HOST_BASE_URL = https://isotest.postfinance.ch/ebicsweb/ebicsweb
BANK_PUBLIC_KEYS_FILE = test/tmp/bank-keys.json
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Config.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
@@ -31,21 +31,25 @@ val NEXUS_CONFIG_SOURCE = ConfigSource("libeufin", "libeufin-nexus", "libeufin-n
data class NexusIngestConfig(
val accountType: AccountType,
val ignoreTransactionsBefore: Instant,
- val ignoreBouncesBefore: Instant
+ val ignoreBouncesBefore: Instant,
+ val bounceDeduceFee: Boolean,
+ val bounceFee: TalerAmount
) {
companion object {
- fun default(accountType: AccountType)
- = NexusIngestConfig(accountType, Instant.MIN, Instant.MIN)
+ fun default(accountType: AccountType, currency: String = "KUDOS")
+ = NexusIngestConfig(accountType, Instant.MIN, Instant.MIN, false, TalerAmount.zero(currency))
}
}
-class NexusFetchConfig(config: TalerConfig) {
+class NexusFetchConfig(config: TalerConfig, currency: String) {
private val section = config.section("nexus-fetch")
val frequency = section.duration("frequency").require()
val frequencyRaw = section.string("frequency").require()
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 bounceDeduceFee = section.boolean("bounce_deduce_fee").default(false)
+ val bounceFee = section.amount("bounce_fee", currency).default(TalerAmount.zero(currency))
}
class NexusSubmitConfig(config: TalerConfig) {
@@ -111,14 +115,16 @@ class NexusConfig internal constructor (val cfg: TalerConfig) {
"exchange" to AccountType.exchange
)).require()
- val fetch by lazy { NexusFetchConfig(cfg) }
+ val fetch by lazy { NexusFetchConfig(cfg, currency) }
val submit by lazy { NexusSubmitConfig(cfg) }
val ebics by lazy { NexusEbicsConfig(sect) }
val ingest get() = NexusIngestConfig(
accountType,
fetch.ignoreTransactionsBefore,
- fetch.ignoreBouncesBefore
+ fetch.ignoreBouncesBefore,
+ fetch.bounceDeduceFee,
+ fetch.bounceFee
)
val wireGatewayApiCfg = cfg.section("nexus-httpd-wire-gateway-api").apiConf()
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt
@@ -127,14 +127,20 @@ suspend fun registerIncomingPayment(
logRes(res, suffix = "ignored bounce: $msg")
} else {
var bounceAmount = payment.amount
- if (payment.creditFee != null) {
+ if (payment.creditFee != null && cfg.bounceDeduceFee) {
if (payment.creditFee > bounceAmount) {
val res = db.payment.registerIncoming(payment)
- logRes(res, suffix = "skip bounce (fee higher than amount): $msg")
+ logRes(res, suffix = "skip bounce (transfer fee higher than amount): $msg")
return
}
bounceAmount -= payment.creditFee
}
+ if (cfg.bounceFee > bounceAmount) {
+ val res = db.payment.registerIncoming(payment)
+ logRes(res, suffix = "skip bounce (bounce fee higher than amount): $msg")
+ return
+ }
+ bounceAmount -= cfg.bounceFee
val res = db.payment.registerMalformedIncoming(
payment,
bounceAmount,
diff --git a/nexus/src/test/kotlin/RegistrationTest.kt b/nexus/src/test/kotlin/RegistrationTest.kt
@@ -185,6 +185,7 @@ class RegistrationTest {
)
}
}
+ println(bounced_tx)
assertContentEquals(bounced, bounced_tx)
}
@@ -554,17 +555,31 @@ class RegistrationTest {
bounced = listOf(
OutgoingPayment(
id = OutgoingId(null, null, null),
- amount = TalerAmount("CHF:0.8"),
+ amount = TalerAmount("CHF:1"),
subject = "bounce: 7371795e-62fa-42dd-93b7-da89cc120faa",
executionTime = Instant.EPOCH,
creditor = ibanPayto("CH7389144832588726658", "Mr Test")
),
OutgoingPayment(
id = OutgoingId(null, null, null),
- amount = TalerAmount("CHF:0.3"),
+ amount = TalerAmount("CHF:0.5"),
subject = "bounce: 50523424675.0001",
executionTime = Instant.EPOCH,
creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans")
+ ),
+ OutgoingPayment(
+ id = OutgoingId(null, null, null),
+ amount = TalerAmount("CHF:0.15"),
+ subject = "bounce: f203fbb4-6e13-4c78-9b2a-d852fea6374a",
+ executionTime = Instant.EPOCH,
+ creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans")
+ ),
+ OutgoingPayment(
+ id = OutgoingId(null, null, null),
+ amount = TalerAmount("CHF:0.1"),
+ subject = "bounce: 81b0d8c6-a677-4577-b75e-a639dcc03681",
+ executionTime = Instant.EPOCH,
+ creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans")
)
)
)