diff options
author | MS <ms@taler.net> | 2023-11-21 09:55:23 +0100 |
---|---|---|
committer | MS <ms@taler.net> | 2023-11-21 10:01:14 +0100 |
commit | 28d3c8f0608632174cb88379a75742a6c0b1d375 (patch) | |
tree | c5a1c447b5441b2d87d490be5f6b1ee53833ed38 | |
parent | e0f1d57d1a46772437952a4224537b698ba324df (diff) | |
download | libeufin-28d3c8f0608632174cb88379a75742a6c0b1d375.tar.gz libeufin-28d3c8f0608632174cb88379a75742a6c0b1d375.tar.bz2 libeufin-28d3c8f0608632174cb88379a75742a6c0b1d375.zip |
nexus fetch
bouncing payments whose amounts are lower than
a configurable threshold. The bouncing however
ends with the creation of the related database
row (as a logging mean), but they never get reimbursed.
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt | 20 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt | 88 | ||||
-rw-r--r-- | nexus/src/test/kotlin/DatabaseTest.kt | 2 | ||||
-rw-r--r-- | nexus/src/test/kotlin/Parsing.kt | 14 |
4 files changed, 103 insertions, 21 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt index d80fd665..7e4c98e5 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt @@ -11,7 +11,7 @@ import java.sql.SQLException import java.time.Instant // Remove this once TalerAmount from the bank -// module gets moved to the 'util' module. +// module gets moved to the 'util' module (#7987). data class TalerAmount( val value: Long, val fraction: Int, // has at most 8 digits. @@ -287,10 +287,14 @@ class Database(dbConfig: String): java.io.Closeable { * @param paymentData information related to the incoming payment. * @param requestUid unique identifier of the outgoing payment to * initiate, in order to reimburse the bounced tx. + * @param refundAmount amount to send back to the original debtor. If + * null, it defaults to the amount of the bounced + * incoming payment. */ suspend fun incomingPaymentCreateBounced( paymentData: IncomingPayment, - requestUid: String + requestUid: String, + refundAmount: TalerAmount? = null ): Boolean = runConn { conn -> val refundTimestamp = Instant.now().toDbMicros() ?: throw Exception("Could not convert refund execution time from Instant.now() to microsends.") @@ -306,8 +310,12 @@ class Database(dbConfig: String): java.io.Closeable { ,? ,? )""") - stmt.setLong(1, paymentData.amount.value) - stmt.setInt(2, paymentData.amount.fraction) + + var finalAmount = paymentData.amount + if (refundAmount != null) finalAmount = refundAmount + + stmt.setLong(1, finalAmount.value) + stmt.setInt(2, finalAmount.fraction) stmt.setString(3, paymentData.wireTransferSubject) stmt.setLong(4, executionTime) stmt.setString(5, paymentData.debitPaytoUri) @@ -548,8 +556,8 @@ class Database(dbConfig: String): java.io.Closeable { ,initiation_time ,request_uid FROM initiated_outgoing_transactions - WHERE submitted='unsubmitted' - OR submitted='transient_failure'; + WHERE (submitted='unsubmitted' OR submitted='transient_failure') + AND ((amount).val != 0 OR (amount).frac != 0); """) val maybeMap = mutableMapOf<Long, InitiatedPayment>() stmt.executeQuery().use { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt index 53bf2ea7..768dde2f 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt @@ -51,16 +51,21 @@ data class FetchContext( /** * EBICS version. For the HAC message type, version gets switched to EBICS 2. */ - var ebicsVersion: EbicsVersion = EbicsVersion.three, + var ebicsVersion: EbicsVersion, /** - * Start date of the returned documents. Only - * used in --transient mode. + * Logs to STDERR the init phase of an EBICS download request. */ - var pinnedStart: Instant? = null, + val ebicsExtraLog: Boolean, /** - * Logs to STDERR the init phase of an EBICS download request. + * Not triggering any Taler logic, if the incoming amount + * is below the following value. + */ + val minimumAmount: TalerAmount?, + /** + * Start date of the returned documents. Only + * used in --transient mode. */ - val ebicsExtraLog: Boolean = false + var pinnedStart: Instant? = null ) /** @@ -191,13 +196,14 @@ private fun makeTalerFrac(bankFrac: String): Int { */ fun getTalerAmount( noCurrencyAmount: String, - currency: String + currency: String, + errorMessagePrefix: String = "" ): TalerAmount { - if (currency.isEmpty()) throw Exception("Currency is empty") + if (currency.isEmpty()) throw Exception("Wrong helper invocation: currency is empty") val split = noCurrencyAmount.split(".") // only 1 (no fraction) or 2 (with fraction) sizes allowed. - if (split.size != 1 && split.size != 2) throw Exception("Invalid amount: ${noCurrencyAmount}") - val value = split[0].toLongOrNull() ?: throw Exception("value part not a long") + if (split.size != 1 && split.size != 2) throw Exception("${errorMessagePrefix}invalid amount: $noCurrencyAmount") + val value = split[0].toLongOrNull() ?: throw Exception("${errorMessagePrefix}value part not a long") if (split.size == 1) return TalerAmount( value = value, fraction = 0, @@ -322,6 +328,7 @@ private suspend fun ingestOutgoingPayment( */ private suspend fun ingestIncomingPayment( db: Database, + ctx: FetchContext, incomingPayment: IncomingPayment ) { logger.debug("Ingesting incoming payment UID: ${incomingPayment.bankTransferId}, subject: ${incomingPayment.wireTransferSubject}") @@ -329,10 +336,30 @@ private suspend fun ingestIncomingPayment( logger.debug("Incoming payment with UID '${incomingPayment.bankTransferId}' already seen.") return } + if ( + ctx.minimumAmount != null && + firstLessThanSecond( + incomingPayment.amount, + ctx.minimumAmount + )) { + /** + * Setting the refund amount to zero makes the initiated + * payment _never_ paid back. Inserting this row merely + * logs the incoming payment event, for which the policy + * has no reimbursement. + */ + db.incomingPaymentCreateBounced( + incomingPayment, + UUID.randomUUID().toString().take(35), + TalerAmount(0, 0, ctx.cfg.currency) + ) + return + } val reservePub = getTalerReservePub(db, incomingPayment) if (reservePub == null) { db.incomingPaymentCreateBounced( - incomingPayment, UUID.randomUUID().toString().take(35) + incomingPayment, + UUID.randomUUID().toString().take(35) ) return } @@ -340,6 +367,25 @@ private suspend fun ingestIncomingPayment( } /** + * Compares amounts. + * + * @param a first argument + * @param b second argument + * @return true if the first argument + * is less than the second + */ +fun firstLessThanSecond( + a: TalerAmount, + b: TalerAmount +): Boolean { + if (a.currency != b.currency) + throw Exception("different currencies: ${a.currency} vs. ${b.currency}") + if (a.value == b.value) + return a.fraction < b.fraction + return a.value < b.value +} + +/** * Parses the response of an EBICS notification looking for * incoming payments. As a result, it either creates a Taler * withdrawal or bounces the incoming payment. In detail, this @@ -391,7 +437,7 @@ private fun ingestNotification( try { runBlocking { incomingPayments.forEach { - ingestIncomingPayment(db, it) + ingestIncomingPayment(db, ctx, it) } outgoingPayments.forEach { ingestOutgoingPayment(db, it) @@ -562,13 +608,29 @@ class EbicsFetch: CliktCommand("Fetches bank records. Defaults to camt.054 noti } return } + val minAmountCfg: String? = cfg.config.lookupString( + "nexus-fetch", + "minimum_amount" + ) + var minAmount: TalerAmount? = null + if (minAmountCfg != null) { + minAmount = doOrFail { + getTalerAmount( + cfg.currency, + minAmountCfg, + "[nexus-fetch]/minimum_amount, " + ) + } + } val ctx = FetchContext( cfg, HttpClient(), clientKeys, bankKeys, whichDoc, - ebicsExtraLog = ebicsExtraLog + EbicsVersion.three, + ebicsExtraLog, + minAmount ) if (transient) { logger.info("Transient mode: fetching once and returning.") diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt index 0f2cc4fa..cacec157 100644 --- a/nexus/src/test/kotlin/DatabaseTest.kt +++ b/nexus/src/test/kotlin/DatabaseTest.kt @@ -176,7 +176,7 @@ class PaymentInitiationsTest { assertEquals(db.initiatedPaymentCreate(initPay), PaymentInitiationOutcome.SUCCESS) assertEquals(db.initiatedPaymentCreate(initPay), PaymentInitiationOutcome.UNIQUE_CONSTRAINT_VIOLATION) val haveOne = db.initiatedPaymentsSubmittableGet("KUDOS") - assertTrue { + assertTrue("Size ${haveOne.size} instead of 1") { haveOne.size == 1 && haveOne.containsKey(1) && haveOne[1]?.requestUid == "unique" diff --git a/nexus/src/test/kotlin/Parsing.kt b/nexus/src/test/kotlin/Parsing.kt index 45737e1f..4d833d1e 100644 --- a/nexus/src/test/kotlin/Parsing.kt +++ b/nexus/src/test/kotlin/Parsing.kt @@ -11,6 +11,18 @@ import kotlin.test.assertTrue class Parsing { + @Test // move eventually to util (#7987) + fun amountComparison() { + val one = TalerAmount(1, 0, "KUDOS") + val two = TalerAmount(2, 0, "KUDOS") + val moreFrac = TalerAmount(2, 4, "KUDOS") + val lessFrac = TalerAmount(2, 3, "KUDOS") + val zeroMoreFrac = TalerAmount(0, 4, "KUDOS") + val zeroLessFrac = TalerAmount(0, 3, "KUDOS") + assertTrue(firstLessThanSecond(one, two)) + assertTrue(firstLessThanSecond(lessFrac, moreFrac)) + assertTrue(firstLessThanSecond(zeroLessFrac, zeroMoreFrac)) + } @Test fun gregorianTime() { parseCamtTime("2023-11-06T20:00:00") @@ -120,7 +132,7 @@ class Parsing { getTalerAmount("1.", "KUDOS") } assertThrows<Exception> { - getTalerAmount("0.123456789", "KUDOS") + getTalerAmount("0.123", "KUDOS") } assertThrows<Exception> { getTalerAmount("noise", "KUDOS") |