commit 3eaa7331d444122787ae54ec382b5bf01e3705a1
parent ead9f9be0ad8cd52f94d20f2ac7185285d7afb38
Author: MS <ms@taler.net>
Date: Wed, 15 Nov 2023 10:50:04 +0100
nexus fetch: ignoring seen payments
Diffstat:
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",