libeufin

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

commit 4b3cf170c669079f644c33b809c528e780b1abd7
parent b490ddbec8a471d97370d2a20b92cdfd558533a0
Author: Antoine A <>
Date:   Tue, 27 May 2025 17:33:50 +0200

nexus: add bounce reason to the bounce subject

Diffstat:
Mcommon/src/main/kotlin/Subject.kt | 10+++++-----
Mcommon/src/test/kotlin/SubjectTest.kt | 4++--
Mdatabase-versioning/libeufin-conversion-setup.sql | 1+
Mdatabase-versioning/libeufin-nexus-procedures.sql | 6++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 13+++++++------
Mnexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt | 6++++--
Mnexus/src/test/kotlin/DatabaseTest.kt | 9++++++---
Mnexus/src/test/kotlin/RegistrationTest.kt | 10+++++-----
8 files changed, 34 insertions(+), 25 deletions(-)

diff --git a/common/src/main/kotlin/Subject.kt b/common/src/main/kotlin/Subject.kt @@ -88,7 +88,7 @@ private val ALPHA_NUMBERIC_PATTERN = Regex("[0-9a-zA-Z]*") **/ fun parseIncomingSubject(subject: String?): IncomingSubject { if (subject == null || subject.isEmpty()) { - throw Exception("Missing subject") + throw Exception("missing subject") } /** Parse an incoming subject */ @@ -150,7 +150,7 @@ fun parseIncomingSubject(subject: String?): IncomingSubject { if (best != null) { if (best.subject is IncomingSubject.AdminBalanceAdjust) { if (other.subject !is IncomingSubject.AdminBalanceAdjust) { - throw Exception("Found multiple subject kind") + throw Exception("found multiple subject kind") } } else if (other.quality > best.quality // We prefer high quality keys || ( // We prefer prefixed keys over reserve keys @@ -166,7 +166,7 @@ fun parseIncomingSubject(subject: String?): IncomingSubject { other.subject.type == IncomingType.reserve )) { - throw Exception("Found multiple reserve public key") + throw Exception("found multiple reserve public key") } } else { best = other @@ -175,11 +175,11 @@ fun parseIncomingSubject(subject: String?): IncomingSubject { } } - return best?.subject ?: throw Exception("Missing reserve public key") + return best?.subject ?: throw Exception("missing reserve public key") } /** Extract the reserve public key from an incoming Taler transaction subject */ fun parseOutgoingSubject(subject: String): Pair<ShortHashCode, BaseURL> { - val (wtid, baseUrl) = subject.splitOnce(" ") ?: throw Exception("Malformed outgoing subject") + val (wtid, baseUrl) = subject.splitOnce(" ") ?: throw Exception("malformed outgoing subject") return Pair(EddsaPublicKey(wtid), BaseURL.parse(baseUrl)) } \ No newline at end of file diff --git a/common/src/test/kotlin/SubjectTest.kt b/common/src/test/kotlin/SubjectTest.kt @@ -91,7 +91,7 @@ class SubjectTest { "$standard $other_standard", "$mixed $other_mixed", )) { - assertFailsMsg("Found multiple reserve public key") { + assertFailsMsg("found multiple reserve public key") { parseIncomingSubject(case) } } @@ -111,7 +111,7 @@ class SubjectTest { standard.substring(1), // Check fail if missing char "2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" // Check fail if not a valid key )) { - assertFailsMsg("Missing reserve public key") { + assertFailsMsg("missing reserve public key") { parseIncomingSubject(case) } } diff --git a/database-versioning/libeufin-conversion-setup.sql b/database-versioning/libeufin-conversion-setup.sql @@ -95,6 +95,7 @@ LANGUAGE plpgsql AS $$ -- end with 34 random chars which is valid for EBICS (max 35 chars) ,upper(replace(gen_random_uuid()::text, '-', '')) ,now_date + ,'amount too small to be converted' ); RETURN NULL; END IF; diff --git a/database-versioning/libeufin-nexus-procedures.sql b/database-versioning/libeufin-nexus-procedures.sql @@ -332,6 +332,7 @@ CREATE FUNCTION register_and_bounce_incoming( ,IN in_bounce_amount taler_amount ,IN in_now_date INT8 ,IN in_bounce_id TEXT + ,IN in_cause TEXT -- Error status ,OUT out_talerable BOOLEAN -- Success return @@ -354,7 +355,7 @@ IF out_talerable THEN RETURN; END IF; -- Bounce incoming transaction -SELECT bounce.out_bounce_id INTO out_bounce_id FROM bounce_incoming(out_tx_id, in_bounce_amount, in_bounce_id, in_now_date) AS bounce; +SELECT bounce.out_bounce_id INTO out_bounce_id FROM bounce_incoming(out_tx_id, in_bounce_amount, in_bounce_id, in_now_date, in_cause) AS bounce; END $$; CREATE FUNCTION bounce_incoming( @@ -362,6 +363,7 @@ CREATE FUNCTION bounce_incoming( ,IN in_bounce_amount taler_amount ,IN in_bounce_id TEXT ,IN in_now_date INT8 + ,IN in_cause TEXT ,OUT out_bounce_id TEXT ) LANGUAGE plpgsql AS $$ @@ -393,7 +395,7 @@ IF NOT FOUND THEN ,end_to_end_id ) VALUES ( in_bounce_amount - ,'bounce: ' || local_bank_id + ,'bounce ' || local_bank_id || ': ' || in_cause ,payto_uri ,in_now_date ,in_bounce_id diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt @@ -115,7 +115,7 @@ suspend fun registerIncomingPayment( logger.debug(fmt) } } - suspend fun bounce(msg: String) { + suspend fun bounce(cause: String) { if (payment.id == null) { logger.debug("{} ignored: missing bank ID", payment) return @@ -124,20 +124,20 @@ suspend fun registerIncomingPayment( AccountType.exchange -> { if (payment.executionTime < cfg.ignoreBouncesBefore) { val res = db.payment.registerIncoming(payment) - logRes(res, suffix = "ignored bounce: $msg") + logRes(res, suffix = "ignored bounce: $cause") } else { var bounceAmount = payment.amount if (payment.creditFee != null && cfg.bounceDeduceFee) { if (payment.creditFee > bounceAmount) { val res = db.payment.registerIncoming(payment) - logRes(res, suffix = "skip bounce (transfer fee higher than amount): $msg") + logRes(res, suffix = "skip bounce (transfer fee higher than amount): $cause") 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") + logRes(res, suffix = "skip bounce (bounce fee higher than amount): $cause") return } bounceAmount -= cfg.bounceFee @@ -145,13 +145,14 @@ suspend fun registerIncomingPayment( payment, bounceAmount, randEbicsId(), - Instant.now() + Instant.now(), + cause ) when (res) { IncomingBounceRegistrationResult.Talerable -> logger.warn("{} tried to bounce a talerable transaction", payment) is IncomingBounceRegistrationResult.Success -> - logRes(res, suffix=": $msg") + logRes(res, suffix=": $cause") } } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt @@ -84,11 +84,12 @@ class PaymentDAO(private val db: Database) { payment: IncomingPayment, bounceAmount: TalerAmount, bounceEndToEndId: String, - timestamp: Instant + timestamp: Instant, + cause: String ): IncomingBounceRegistrationResult = db.serializable( """ SELECT out_found, out_tx_id, out_completed, out_bounce_id, out_talerable - FROM register_and_bounce_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,?,?,(?,?)::taler_amount,?,?) + FROM register_and_bounce_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,?,?,(?,?)::taler_amount,?,?, ?) """ ) { bind(payment.amount) @@ -102,6 +103,7 @@ class PaymentDAO(private val db: Database) { bind(bounceAmount) bind(timestamp) bind(bounceEndToEndId) + bind(cause) one { if (it.getBoolean("out_talerable")) { IncomingBounceRegistrationResult.Talerable diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt @@ -155,7 +155,8 @@ class IncomingPaymentsTest { payment, TalerAmount("KUDOS:2.53"), id, - Instant.now() + Instant.now(), + "manual bounce" ).run { assertIs<IncomingBounceRegistrationResult.Success>(this) assertTrue(new) @@ -165,7 +166,8 @@ class IncomingPaymentsTest { payment, TalerAmount("KUDOS:2.53"), randEbicsId(), - Instant.now() + Instant.now(), + "manual bounce" ).run { assertIs<IncomingBounceRegistrationResult.Success>(this) assertFalse(new) @@ -385,7 +387,8 @@ class IncomingPaymentsTest { talerablePayment, TalerAmount("KUDOS:2.53"), randEbicsId(), - Instant.now() + Instant.now(), + "manual bounce" ).run { assertEquals(IncomingBounceRegistrationResult.Talerable, this) } diff --git a/nexus/src/test/kotlin/RegistrationTest.kt b/nexus/src/test/kotlin/RegistrationTest.kt @@ -564,35 +564,35 @@ class RegistrationTest { OutgoingPayment( id = OutgoingId(null, null, null), amount = TalerAmount("CHF:1"), - subject = "bounce: 7371795e-62fa-42dd-93b7-da89cc120faa", + subject = "bounce 7371795e-62fa-42dd-93b7-da89cc120faa: missing reserve public key", executionTime = Instant.EPOCH, creditor = ibanPayto("CH7389144832588726658", "Mr Test") ), OutgoingPayment( id = OutgoingId(null, null, null), amount = TalerAmount("CHF:0.5"), - subject = "bounce: 50523424675.0001", + subject = "bounce 50523424675.0001: missing subject", 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", + subject = "bounce f203fbb4-6e13-4c78-9b2a-d852fea6374a: missing reserve public key", 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", + subject = "bounce 81b0d8c6-a677-4577-b75e-a639dcc03681: missing reserve public key", executionTime = Instant.EPOCH, creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans") ), OutgoingPayment( id = OutgoingId(null, null, null), amount = TalerAmount("CHF:10"), - subject = "bounce: F000787951230001", + subject = "bounce F000787951230001: restricted account", executionTime = Instant.EPOCH, creditor = ibanPayto("DE20500105172419259181", "Mr German") )