libeufin

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

commit 3eaa7331d444122787ae54ec382b5bf01e3705a1
parent ead9f9be0ad8cd52f94d20f2ac7185285d7afb38
Author: MS <ms@taler.net>
Date:   Wed, 15 Nov 2023 10:50:04 +0100

nexus fetch: ignoring seen payments

Diffstat:
Mdatabase-versioning/libeufin-nexus-0001.sql | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/Database.kt | 39+++++++++++++++++++++------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt | 6+++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt | 28+++++++++++++++++++++-------
4 files changed, 48 insertions(+), 27 deletions(-)

diff --git a/database-versioning/libeufin-nexus-0001.sql b/database-versioning/libeufin-nexus-0001.sql @@ -47,7 +47,7 @@ COMMENT ON TYPE submission_state CREATE TABLE IF NOT EXISTS incoming_transactions (incoming_transaction_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE ,amount taler_amount NOT NULL - ,wire_transfer_subject TEXT + ,wire_transfer_subject TEXT NOT NULL ,execution_time INT8 NOT NULL ,debit_payto_uri TEXT NOT NULL ,bank_transfer_id TEXT NOT NULL -- EBICS or Depolymerizer (generic) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt @@ -11,27 +11,10 @@ import java.time.Instant data class TalerAmount( val value: Long, - val fraction: Int, + val fraction: Int, // has at most 8 digits. val currency: String ) -/** - * Stringifies TalerAmount's. NOTE: the caller must enforce - * length-checks on the output fractional part, to ensure compatibility - * with the bank. - * - * @return the amount in the $currency:x.y format. - */ -fun TalerAmount.stringify(): String { - if (fraction == 0) { - return "$currency:$value" - } else { - val fractionFormat = this.fraction.toString().padStart(8, '0').dropLastWhile { it == '0' } - if (fractionFormat.length > 2) throw Exception("Sub-cent amounts not supported") - return "$currency:$value.$fractionFormat" - } -} - // INCOMING PAYMENTS STRUCTS /** @@ -333,6 +316,26 @@ class Database(dbConfig: String): java.io.Closeable { } /** + * Checks if the incoming payment was already processed by Nexus. + * + * @param bankUid unique identifier assigned by the bank to the payment. + * Normally, that's the <AcctSvcrRef> value found in camt.05x records. + * @return true if found, false otherwise + */ + suspend fun isIncomingPaymentSeen(bankUid: String): Boolean = runConn { conn -> + val stmt = conn.prepareStatement(""" + SELECT 1 + FROM incoming_transactions + WHERE bank_transfer_id = ?; + """) + stmt.setString(1, bankUid) + val res = stmt.executeQuery() + res.use { + return@runConn it.next() + } + } + + /** * Checks if the reserve public key already exists. * * @param maybeReservePub reserve public key to look up diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt @@ -374,6 +374,10 @@ fun ingestNotification( try { incomingPayments.forEach { runBlocking { + if (db.isIncomingPaymentSeen(it.bankTransferId)) { + logger.debug("Incoming payment with UID '${it.bankTransferId}' already seen.") + return@runBlocking + } val reservePub = isTalerable(db, it) if (reservePub == null) { db.incomingPaymentCreateBounced( @@ -429,7 +433,7 @@ private suspend fun fetchDocuments( ) // Parsing the XML: only camt.054 (Detailavisierung) supported currently. if (ctx.whichDocument != SupportedDocument.CAMT_054) { - logger.warn("Not parsing ${ctx.whichDocument}. Only camt.054 notifications supported.") + logger.warn("Not ingesting ${ctx.whichDocument}. Only camt.054 notifications supported.") return } if (!ingestNotification(db, ctx, maybeContent)) { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -14,6 +14,26 @@ data class Pain001Namespaces( ) /** + * Gets the amount number, also converting it from the + * Taler-friendly 8 fractional digits to the more bank + * friendly with 2. + * + * @param amount the Taler amount where to extract the number + * @return [String] of the amount number without the currency. + */ +fun getAmountNoCurrency(amount: TalerAmount): String { + if (amount.fraction == 0) { + return amount.value.toString() + } else { + val fractionFormat = amount.fraction.toString().padStart(8, '0').dropLastWhile { it == '0' } + if (fractionFormat.length > 2) throw Exception("Sub-cent amounts not supported") + return "${amount.value}.${fractionFormat}" + } +} + + + +/** * Create a pain.001 document. It requires the debtor BIC. * * @param requestUid UID of this request, helps to make this request idempotent. @@ -43,13 +63,7 @@ fun createPain001( xsdFilename = "pain.001.001.09.ch.03.xsd" ) val zonedTimestamp = ZonedDateTime.ofInstant(initiationTimestamp, ZoneId.of("UTC")) - val amountWithoutCurrency: String = amount.stringify().split(":").run { - if (this.size != 2) throw NexusSubmitException( - "Invalid stringified amount: $amount", - stage=NexusSubmissionStage.pain - ) - return@run this[1] - } + val amountWithoutCurrency: String = getAmountNoCurrency(amount) val creditorName: String = creditAccount.receiverName ?: throw NexusSubmitException( "Cannot operate without the creditor name",