gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: nexus fetch, adding:


From: gnunet
Subject: [libeufin] branch master updated: nexus fetch, adding:
Date: Tue, 14 Nov 2023 17:47:26 +0100

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new cc35923a nexus fetch, adding:
cc35923a is described below

commit cc35923acc45c1fe0acf4fa277ec490b43a27094
Author: MS <ms@taler.net>
AuthorDate: Tue Nov 14 17:45:27 2023 +0100

    nexus fetch, adding:
    
    - SQL to store talerable and bounced incoming transactions
    - draft of camt.054 parser
    - logic to bounce or accept based on the payment subject
---
 database-versioning/libeufin-nexus-procedures.sql  |  63 ++++-
 .../main/kotlin/tech/libeufin/nexus/Database.kt    |  95 ++++++--
 .../main/kotlin/tech/libeufin/nexus/EbicsFetch.kt  | 270 ++++++++++++++++++++-
 nexus/src/test/kotlin/DatabaseTest.kt              |  76 ++----
 nexus/src/test/kotlin/Parsing.kt                   |  72 ++++++
 util/src/main/kotlin/time.kt                       |  14 +-
 6 files changed, 505 insertions(+), 85 deletions(-)

diff --git a/database-versioning/libeufin-nexus-procedures.sql 
b/database-versioning/libeufin-nexus-procedures.sql
index 84dbcfc6..7a36b91a 100644
--- a/database-versioning/libeufin-nexus-procedures.sql
+++ b/database-versioning/libeufin-nexus-procedures.sql
@@ -9,8 +9,12 @@ CREATE OR REPLACE FUNCTION create_incoming_and_bounce(
   ,IN in_bank_transfer_id TEXT
   ,IN in_timestamp BIGINT
   ,IN in_request_uid TEXT
-) RETURNS void
+  ,OUT out_ok BOOLEAN
+) RETURNS BOOLEAN
 LANGUAGE plpgsql AS $$
+DECLARE
+new_tx_id INT8;
+new_init_id INT8;
 BEGIN
 -- creating the bounced incoming transaction.
 INSERT INTO incoming_transactions (
@@ -19,15 +23,14 @@ INSERT INTO incoming_transactions (
   ,execution_time
   ,debit_payto_uri
   ,bank_transfer_id
-  ,bounced
   ) VALUES (
     in_amount
     ,in_wire_transfer_subject
     ,in_execution_time
     ,in_debit_payto_uri
     ,in_bank_transfer_id
-    ,true
-  );
+  ) RETURNING incoming_transaction_id INTO new_tx_id;
+
 -- creating its reimbursement.
 INSERT INTO initiated_outgoing_transactions (
   amount
@@ -41,7 +44,16 @@ INSERT INTO initiated_outgoing_transactions (
     ,in_debit_payto_uri
     ,in_timestamp
     ,in_request_uid
-  );
+  ) RETURNING initiated_outgoing_transaction_id INTO new_init_id;
+
+INSERT INTO bounced_transactions (
+  incoming_transaction_id
+  ,initiated_outgoing_transaction_id
+) VALUES (
+  new_tx_id
+  ,new_init_id
+);
+out_ok = TRUE;
 END $$;
 
 COMMENT ON FUNCTION create_incoming_and_bounce(taler_amount, TEXT, BIGINT, 
TEXT, TEXT, BIGINT, TEXT)
@@ -141,3 +153,44 @@ UPDATE incoming_transactions
 END $$;
 
 COMMENT ON FUNCTION bounce_payment(BIGINT, BIGINT, TEXT) IS 'Marks an incoming 
payment as bounced and initiates its refunding payment';
+
+CREATE OR REPLACE FUNCTION create_incoming_talerable(
+  IN in_amount taler_amount
+  ,IN in_wire_transfer_subject TEXT
+  ,IN in_execution_time BIGINT
+  ,IN in_debit_payto_uri TEXT
+  ,IN in_bank_transfer_id TEXT
+  ,IN in_reserve_public_key BYTEA
+  ,OUT out_ok BOOLEAN
+) RETURNS BOOLEAN
+LANGUAGE plpgsql AS $$
+DECLARE
+new_tx_id INT8;
+BEGIN
+INSERT INTO incoming_transactions (
+  amount
+  ,wire_transfer_subject
+  ,execution_time
+  ,debit_payto_uri
+  ,bank_transfer_id
+  ) VALUES (
+    in_amount
+    ,in_wire_transfer_subject
+    ,in_execution_time
+    ,in_debit_payto_uri
+    ,in_bank_transfer_id
+  ) RETURNING incoming_transaction_id INTO new_tx_id;
+INSERT INTO talerable_incoming_transactions (
+  incoming_transaction_id
+  ,reserve_public_key
+) VALUES (
+  new_tx_id
+  ,in_reserve_public_key
+);
+out_ok = TRUE;
+END $$;
+
+COMMENT ON FUNCTION create_incoming_talerable(taler_amount, TEXT, BIGINT, 
TEXT, TEXT, BYTEA) IS '
+Creates one row in the incoming transactions table and one row
+in the talerable transactions table.  The talerable row links the
+incoming one.';
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
index cc91b557..9b83a707 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
@@ -39,7 +39,7 @@ fun TalerAmount.stringify(): String {
  */
 data class IncomingPayment(
     val amount: TalerAmount,
-    val wireTransferSubject: String?,
+    val wireTransferSubject: String,
     val debitPaytoUri: String,
     val executionTime: Instant,
     val bankTransferId: String
@@ -281,13 +281,13 @@ class Database(dbConfig: String): java.io.Closeable {
     suspend fun incomingPaymentCreateBounced(
         paymentData: IncomingPayment,
         requestUid: String
-        ) = runConn { conn ->
+        ): Boolean = runConn { conn ->
         val refundTimestamp = Instant.now().toDbMicros()
             ?: throw Exception("Could not convert refund execution time from 
Instant.now() to microsends.")
         val executionTime = paymentData.executionTime.toDbMicros()
             ?: throw Exception("Could not convert payment execution time from 
Instant to microseconds.")
         val stmt = conn.prepareStatement("""
-            SELECT create_incoming_and_bounce (
+            SELECT out_ok FROM create_incoming_and_bounce (
               (?,?)::taler_amount
               ,?
               ,?
@@ -304,7 +304,11 @@ class Database(dbConfig: String): java.io.Closeable {
         stmt.setString(6, paymentData.bankTransferId)
         stmt.setLong(7, refundTimestamp)
         stmt.setString(8, requestUid)
-        stmt.executeQuery()
+        val res = stmt.executeQuery()
+        res.use {
+            if (!it.next()) return@runConn false
+            return@runConn it.getBoolean("out_ok")
+        }
     }
 
     /**
@@ -329,7 +333,78 @@ class Database(dbConfig: String): java.io.Closeable {
     }
 
     /**
-     * Creates a new incoming payment record in the database.
+     * Checks if the reserve public key already exists.
+     *
+     * @param maybeReservePub reserve public key to look up
+     * @return true if found, false otherwise
+     */
+    suspend fun isReservePubFound(maybeReservePub: ByteArray): Boolean = 
runConn { conn ->
+        val stmt = conn.prepareStatement("""
+             SELECT 1
+               FROM talerable_incoming_transactions
+               WHERE reserve_public_key = ?;
+        """)
+        stmt.setBytes(1, maybeReservePub)
+        val res = stmt.executeQuery()
+        res.use {
+            return@runConn it.next()
+        }
+    }
+
+    /**
+     * Creates an incoming transaction row and  links a new talerable
+     * row to it.
+     *
+     * @param paymentData incoming talerable payment.
+     * @param reservePub reserve public key.  The caller is
+     *        responsible to check it.
+     */
+    suspend fun incomingTalerablePaymentCreate(
+        paymentData: IncomingPayment,
+        reservePub: ByteArray
+    ): Boolean = runConn { conn ->
+        val stmt = conn.prepareStatement("""
+           SELECT out_ok FROM create_incoming_talerable(
+              (?,?)::taler_amount
+              ,?
+              ,?
+              ,?
+              ,?
+              ,?
+           )""")
+        bindIncomingPayment(paymentData, stmt)
+        stmt.setBytes(7, reservePub)
+        stmt.executeQuery().use {
+            if (!it.next()) return@runConn false
+            return@runConn it.getBoolean("out_ok")
+        }
+    }
+
+    /**
+     * Binds the values of an incoming payment to the prepared
+     * statement's placeholders.  Warn: may easily break in case
+     * the placeholders get their positions changed!
+     *
+     * @param data incoming payment to bind to the placeholders
+     * @param stmt statement to receive the values in its placeholders
+     */
+    private fun bindIncomingPayment(
+        data: IncomingPayment,
+        stmt: PreparedStatement
+    ) {
+        stmt.setLong(1, data.amount.value)
+        stmt.setInt(2, data.amount.fraction)
+        stmt.setString(3, data.wireTransferSubject)
+        val executionTime = data.executionTime.toDbMicros() ?: run {
+            throw Exception("Execution time could not be converted to 
microseconds for the database.")
+        }
+        stmt.setLong(4, executionTime)
+        stmt.setString(5, data.debitPaytoUri)
+        stmt.setString(6, data.bankTransferId)
+    }
+    /**
+     * Creates a new incoming payment record in the database.  It does NOT
+     * update the "talerable" table.
      *
      * @param paymentData information related to the incoming payment.
      * @return true on success, false otherwise.
@@ -350,15 +425,7 @@ class Database(dbConfig: String): java.io.Closeable {
               ,?
             )
         """)
-        stmt.setLong(1, paymentData.amount.value)
-        stmt.setInt(2, paymentData.amount.fraction)
-        stmt.setString(3, paymentData.wireTransferSubject)
-        val executionTime = paymentData.executionTime.toDbMicros() ?: run {
-            throw Exception("Execution time could not be converted to 
microseconds for the database.")
-        }
-        stmt.setLong(4, executionTime)
-        stmt.setString(5, paymentData.debitPaytoUri)
-        stmt.setString(6, paymentData.bankTransferId)
+        bindIncomingPayment(paymentData, stmt)
         return@runConn stmt.maybeUpdate()
     }
 
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index c26ea937..138a695a 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -5,21 +5,21 @@ import com.github.ajalt.clikt.parameters.options.flag
 import com.github.ajalt.clikt.parameters.options.option
 import io.ktor.client.*
 import kotlinx.coroutines.runBlocking
-import org.apache.commons.compress.archivers.zip.ZipFile
-import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
+import net.taler.wallet.crypto.Base32Crockford
+import net.taler.wallet.crypto.EncodingException
 import tech.libeufin.nexus.ebics.*
-import tech.libeufin.util.EbicsOrderParams
+import tech.libeufin.util.*
 import tech.libeufin.util.ebics_h005.Ebics3Request
-import tech.libeufin.util.getXmlDate
-import tech.libeufin.util.toDbMicros
 import java.io.File
+import java.io.IOException
+import java.lang.StringBuilder
 import java.nio.file.Path
 import java.time.Instant
 import java.time.LocalDate
 import java.time.ZoneId
+import java.util.UUID
 import kotlin.concurrent.fixedRateTimer
 import kotlin.io.path.createDirectories
-import kotlin.reflect.typeOf
 import kotlin.system.exitProcess
 
 /**
@@ -158,6 +158,239 @@ fun maybeLogFile(
     }
 }
 
+/**
+ * Converts the given fractional value to the sub-cent 8 digits
+ * fraction used in Taler.  Note: this value has very likely a <2
+ * length, but the function is general, for each fraction with at
+ * most 8 digits.
+ *
+ * @param bankFrac fractional value
+ * @return the Taler fractional value with at most 8 digits.
+ */
+fun makeTalerFrac(bankFrac: String): Int {
+    if (bankFrac.length > 8) throw Exception("Fractional value has more than 8 
digits")
+    var buf = bankFrac.toIntOrNull() ?: throw Exception("Fractional value not 
an Int: $bankFrac")
+    repeat(8 - bankFrac.length) {
+        buf *= 10
+    }
+    return buf
+}
+
+/**
+ * Gets Taler amount from a currency-agnostic value.
+ *
+ * @param noCurrencyAmount currency-agnostic value coming from the bank.
+ * @param currency currency to set to the result.
+ * @return [TalerAmount]
+ */
+fun getTalerAmount(
+    noCurrencyAmount: String,
+    currency: String
+): TalerAmount {
+    if (currency.isEmpty()) throw Exception("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) return TalerAmount(
+        value = value,
+        fraction = 0,
+        currency = currency
+    )
+    return TalerAmount(
+        value = value,
+        fraction = makeTalerFrac(split[1]),
+        currency = currency
+    )
+}
+
+/**
+ * Searches for incoming transactions in a camt.054 document, that
+ * was downloaded via EBICS notification.
+ *
+ * @param notifXml the input document.
+ * @return any incoming payment as a list of [IncomingPayment]
+ */
+fun findIncomingTxInNotification(
+    notifXml: String,
+    acceptedCurrency: String
+): List<IncomingPayment> {
+    val notifDoc = XMLUtil.parseStringIntoDom(notifXml)
+    val ret = mutableListOf<IncomingPayment>()
+    destructXml(notifDoc) {
+        requireRootElement("Document") {
+            requireUniqueChildNamed("BkToCstmrDbtCdtNtfctn") {
+                mapEachChildNamed("Ntfctn") {
+                    mapEachChildNamed("Ntry") {
+                        mapEachChildNamed("NtryDtls") {
+                            mapEachChildNamed("TxDtls") maybeDbit@{
+
+                                // currently, only incoming payments are 
considered.
+                                if (requireUniqueChildNamed("CdtDbtInd") {
+                                        focusElement.textContent == "DBIT"
+                                }) return@maybeDbit
+
+                                // Obtaining the amount.
+                                val amount: TalerAmount = 
requireUniqueChildNamed("Amt") {
+                                    val currency = 
focusElement.getAttribute("Ccy")
+                                    if (currency != acceptedCurrency) throw 
Exception("Currency $currency not supported")
+                                    getTalerAmount(focusElement.textContent, 
currency)
+                                }
+                                // Obtaining payment UID.
+                                val uidFromBank: String = 
requireUniqueChildNamed("Refs") {
+                                    requireUniqueChildNamed("AcctSvcrRef") {
+                                        focusElement.textContent
+                                    }
+                                }
+                                // Obtaining payment subject.
+                                val subject = StringBuilder()
+                                requireUniqueChildNamed("RmtInf") {
+                                    this.mapEachChildNamed("Ustrd") {
+                                        val piece = 
this.focusElement.textContent
+                                        subject.append(piece)
+                                    }
+                                }
+                                // Obtaining the execution time.
+                                val executionTime: Instant = 
requireUniqueChildNamed("RltdDts") {
+                                    requireUniqueChildNamed("AccptncDtTm") {
+                                        
parseGregorianTime(focusElement.textContent)
+                                    }
+                                }
+                                // Obtaining the payer's details
+                                val debtorPayto = 
StringBuilder("payto://iban/")
+                                requireUniqueChildNamed("RltdPties") {
+                                    requireUniqueChildNamed("DbtrAcct") {
+                                        requireUniqueChildNamed("Id") {
+                                            requireUniqueChildNamed("IBAN") {
+                                                
debtorPayto.append(focusElement.textContent)
+                                            }
+                                        }
+                                    }
+                                    // warn: it might need the postal address 
too..
+                                    requireUniqueChildNamed("Dbtr") {
+                                        requireUniqueChildNamed("Nm") {
+                                            
debtorPayto.append("?receiver-name=${focusElement.textContent}")
+                                        }
+                                    }
+                                }
+                                val incomingPayment = IncomingPayment(
+                                    amount = amount,
+                                    bankTransferId = uidFromBank,
+                                    debitPaytoUri = debtorPayto.toString(),
+                                    executionTime = executionTime,
+                                    wireTransferSubject = subject.toString()
+                                )
+                                ret.add(incomingPayment)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return ret
+}
+
+/**
+ * Converts valid reserve pubs to its binary representation.
+ *
+ * @param maybeReservePub input.
+ * @return [ByteArray] or null if not valid.
+ */
+fun isReservePub(maybeReservePub: String): ByteArray? {
+    val dec = try {
+        Base32Crockford.decode(maybeReservePub)
+    } catch (e: EncodingException) {
+        logger.error("Not a reserve pub: $maybeReservePub")
+        return null
+    }
+    logger.debug("Reserve how many bytes: ${dec.size}")
+    if (dec.size != 32) {
+        logger.error("Not a reserve pub, wrong length: ${dec.size}")
+        return null
+    }
+    return dec
+}
+/**
+ * Checks the two conditions that may invalidate one incoming
+ * payment: subject validity and availability.
+ *
+ * @param db database connection.
+ * @param payment incoming payment whose subject is to be checked.
+ * @return [ByteArray] as the reserve public key, or null if the
+ *         payment cannot lead to a Taler withdrawal.
+ */
+suspend fun isTalerable(
+    db: Database,
+    payment: IncomingPayment
+): ByteArray? {
+    // Checking validity first.
+    val dec = isReservePub(payment.wireTransferSubject) ?: return null
+    // Now checking availability.
+    val maybeUnavailable = db.isReservePubFound(dec)
+    if (maybeUnavailable) {
+        logger.error("Incoming payment with subject 
'${payment.wireTransferSubject}' exists already")
+        return null
+    }
+    return dec
+}
+
+/**
+ * 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
+ * function extracts the camt.054 from the ZIP archive, invokes
+ * the lower-level camt.054 parser and updates the database.
+ *
+ * @param db database connection.
+ * @param content the ZIP file that contains the EBICS
+ *        notification as camt.054 records.
+ * @return true if the ingestion succeeded, false otherwise.
+ *         False should fail the process, since it means that
+ *         the notification could not be parsed.
+ */
+fun ingestNotification(
+    db: Database,
+    ctx: FetchContext,
+    content: ByteArray
+): Boolean {
+    val incomingPayments = mutableListOf<IncomingPayment>()
+    try {
+        content.unzipForEach { fileName, xmlContent ->
+            // discarding plain "avisierung", since they don't bring any 
payment subject.
+            if (!fileName.startsWith("camt.054_P_")) return@unzipForEach
+            val found = findIncomingTxInNotification(xmlContent, 
ctx.cfg.currency)
+            incomingPayments += found
+        }
+    } catch (e: IOException) {
+        logger.error("Could not open any ZIP archive")
+        return false
+    } catch (e: Exception) {
+        logger.error(e.message)
+        return false
+    }
+    // Distinguishing now valid and invalid payments.
+    // Any error at this point is only due to Nexus.
+    try {
+        incomingPayments.forEach {
+            runBlocking {
+                val reservePub = isTalerable(db, it)
+                if (reservePub == null) {
+                    db.incomingPaymentCreateBounced(
+                        it, UUID.randomUUID().toString().take(35)
+                    )
+                    return@runBlocking
+                }
+                db.incomingTalerablePaymentCreate(it, reservePub)
+            }
+        }
+    } catch (e: Exception) {
+        logger.error(e.message)
+        return false
+    }
+    return true
+}
+
 /**
  * Fetches the banking records via EBICS notifications requests.
  *
@@ -184,14 +417,25 @@ private suspend fun fetchDocuments(
 ) {
     // maybe get last execution_date.
     val lastExecutionTime: Instant? = ctx.pinnedStart ?: 
db.incomingPaymentLastExecTime()
-    logger.debug("Fetching documents from timestamp: $lastExecutionTime")
+    logger.debug("Fetching ${ctx.whichDocument} from timestamp: 
$lastExecutionTime")
+    // downloading the content
     val maybeContent = downloadHelper(ctx, lastExecutionTime) ?: 
exitProcess(1) // client is wrong, failing.
     if (maybeContent.isEmpty()) return
+    // logging, if the configuration wants.
     maybeLogFile(
         ctx.cfg,
         maybeContent,
         nonZip = ctx.whichDocument == SupportedDocument.PAIN_002_LOGS
     )
+    // 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.")
+        return
+    }
+    if (!ingestNotification(db, ctx, maybeContent)) {
+        logger.error("Ingesting notifications failed")
+        exitProcess(1)
+    }
 }
 
 class EbicsFetch: CliktCommand("Fetches bank records.  Defaults to camt.054 
notifications") {
@@ -218,13 +462,15 @@ class EbicsFetch: CliktCommand("Fetches bank records.  
Defaults to camt.054 noti
     ).flag(default = false)
 
     private val onlyLogs by option(
-        help = "Downloads only EBICS activity logs via pain.002, only 
available to --transient mode.  Config needs log directory"
+        help = "Downloads only EBICS activity logs via pain.002," +
+                " only available to --transient mode.  Config needs" +
+                " log directory"
     ).flag(default = false)
 
     private val pinnedStart by option(
-        help = "constant YYYY-MM-DD date for the earliest document to download 
" +
-                "(only consumed in --transient mode).  The latest document is 
always" +
-                " until the current time."
+        help = "constant YYYY-MM-DD date for the earliest document" +
+                " to download (only consumed in --transient mode).  The" +
+                " latest document is always until the current time."
     )
 
     /**
@@ -252,6 +498,7 @@ class EbicsFetch: CliktCommand("Fetches bank records.  
Defaults to camt.054 noti
             logger.error("Client private keys not found at: 
${cfg.clientPrivateKeysFilename}")
             exitProcess(1)
         }
+
         // Deciding what to download.
         var whichDoc = SupportedDocument.CAMT_054
         if (onlyAck) whichDoc = SupportedDocument.PAIN_002
@@ -266,7 +513,6 @@ class EbicsFetch: CliktCommand("Fetches bank records.  
Defaults to camt.054 noti
             bankKeys,
             whichDoc
         )
-
         if (transient) {
             logger.info("Transient mode: fetching once and returning.")
             val pinnedStartVal = pinnedStart
diff --git a/nexus/src/test/kotlin/DatabaseTest.kt 
b/nexus/src/test/kotlin/DatabaseTest.kt
index 9a5cd79e..b8b12c0e 100644
--- a/nexus/src/test/kotlin/DatabaseTest.kt
+++ b/nexus/src/test/kotlin/DatabaseTest.kt
@@ -1,11 +1,10 @@
 import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
 import org.junit.Test
 import tech.libeufin.nexus.*
 import java.time.Instant
+import kotlin.random.Random
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
-import kotlin.test.assertNull
 import kotlin.test.assertTrue
 
 
@@ -43,7 +42,7 @@ class OutgoingPaymentsTest {
     }
 }
 
-@Ignore // enable after having modified the bouncing logic in Kotlin
+// @Ignore // enable after having modified the bouncing logic in Kotlin
 class IncomingPaymentsTest {
     // Tests creating and bouncing incoming payments in one DB transaction.
     @Test
@@ -51,17 +50,21 @@ class IncomingPaymentsTest {
         val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
         runBlocking {
             // creating and bouncing one incoming transaction.
-            db.incomingPaymentCreateBounced(
+            assertTrue(db.incomingPaymentCreateBounced(
                 genIncPay("incoming and bounced"),
                 "UID"
-            )
+            ))
             db.runConn {
-                // check the bounced flaag is true
+                // Checking one incoming got created
+                val checkIncoming = it.prepareStatement("""
+                    SELECT 1 FROM incoming_transactions WHERE 
incoming_transaction_id = 1;
+                """).executeQuery()
+                assertTrue(checkIncoming.next())
+                // Checking the bounced table got its row.
                 val checkBounced = it.prepareStatement("""
-                    SELECT bounced FROM incoming_transactions WHERE 
incoming_transaction_id = 1;
+                    SELECT 1 FROM bounced_transactions WHERE 
incoming_transaction_id = 1;
                 """).executeQuery()
                 assertTrue(checkBounced.next())
-                assertTrue(checkBounced.getBoolean("bounced"))
                 // check the related initiated payment exists.
                 val checkInitiated = it.prepareStatement("""
                     SELECT 
@@ -74,55 +77,22 @@ class IncomingPaymentsTest {
         }
     }
 
-    // Tests the function that flags incoming payments as bounced.
+    // Tests the creation of a talerable incoming payment.
     @Test
-    fun incomingPaymentBounce() {
+    fun incomingTalerableCreation() {
         val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
-        runBlocking {
-            // creating one incoming payment.
-            assertTrue(db.incomingPaymentCreate(genIncPay("to be bounced"))) 
// row ID == 1.
-            db.runConn {
-                val bouncedSql = """
-                    SELECT bounced
-                      FROM incoming_transactions
-                      WHERE incoming_transaction_id = 1"""
-                // asserting is NOT bounced.
-                val expectNotBounced = it.execSQLQuery(bouncedSql)
-                assertTrue(expectNotBounced.next())
-                assertFalse(expectNotBounced.getBoolean("bounced"))
-                // now bouncing it.
-                assertTrue(db.incomingPaymentSetAsBounced(1, "unique 0"))
-                // asserting it got flagged as bounced.
-                val expectBounced = it.execSQLQuery(bouncedSql)
-                assertTrue(expectBounced.next())
-                assertTrue(expectBounced.getBoolean("bounced"))
-                // Trying to bounce a non-existing payment.
-                assertFalse(db.incomingPaymentSetAsBounced(5, "unique 1"))
-            }
-        }
-    }
+        val reservePub = ByteArray(32)
+        Random.nextBytes(reservePub)
 
-    // Tests the creation of an incoming payment.
-    @Test
-    fun incomingPaymentCreation() {
-        val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
-        val countRows = "SELECT count(*) AS how_many FROM 
incoming_transactions"
         runBlocking {
-            // Asserting the table is empty.
-            db.runConn {
-                val res = it.execSQLQuery(countRows)
-                assertTrue(res.next())
-                assertEquals(0, res.getInt("how_many"))
-            }
-            assertTrue(db.incomingPaymentCreate(genIncPay("singleton")))
-            // Asserting the table has one.
-            db.runConn {
-                val res = it.execSQLQuery(countRows)
-                assertTrue(res.next())
-                assertEquals(1, res.getInt("how_many"))
-            }
-            // Checking insertion of null (allowed) subjects.
-            assertTrue(db.incomingPaymentCreate(genIncPay()))
+            // Checking the reserve is not found.
+            assertFalse(db.isReservePubFound(reservePub))
+            assertTrue(db.incomingTalerablePaymentCreate(
+                genIncPay("reserve-pub"),
+                reservePub
+            ))
+            // Checking the reserve is not found.
+            assertTrue(db.isReservePubFound(reservePub))
         }
     }
 }
diff --git a/nexus/src/test/kotlin/Parsing.kt b/nexus/src/test/kotlin/Parsing.kt
new file mode 100644
index 00000000..441b05aa
--- /dev/null
+++ b/nexus/src/test/kotlin/Parsing.kt
@@ -0,0 +1,72 @@
+import org.junit.Test
+import org.junit.jupiter.api.assertThrows
+import tech.libeufin.nexus.getTalerAmount
+import tech.libeufin.nexus.isReservePub
+import java.lang.StringBuilder
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class Parsing {
+    @Test // parses amounts as found in the camt.05x documents.
+    fun parseCurrencyAgnosticAmount() {
+        assertTrue {
+            getTalerAmount("1.00", "KUDOS").run {
+                this.value == 1L && this.fraction == 0 && this.currency == 
"KUDOS"
+            }
+        }
+        assertTrue {
+            getTalerAmount("1", "KUDOS").run {
+                this.value == 1L && this.fraction == 0 && this.currency == 
"KUDOS"
+            }
+        }
+        assertTrue {
+            getTalerAmount("0.99", "KUDOS").run {
+                this.value == 0L && this.fraction == 99000000 && this.currency 
== "KUDOS"
+            }
+        }
+        assertTrue {
+            getTalerAmount("0.01", "KUDOS").run {
+                this.value == 0L && this.fraction == 1000000 && this.currency 
== "KUDOS"
+            }
+        }
+        assertThrows<Exception> {
+            getTalerAmount("", "")
+        }
+        assertThrows<Exception> {
+            getTalerAmount(".1", "KUDOS")
+        }
+        assertThrows<Exception> {
+            getTalerAmount("1.", "KUDOS")
+        }
+        assertThrows<Exception> {
+            getTalerAmount("0.123456789", "KUDOS")
+        }
+        assertThrows<Exception> {
+            getTalerAmount("noise", "KUDOS")
+        }
+        assertThrows<Exception> {
+            getTalerAmount("1.noise", "KUDOS")
+        }
+        assertThrows<Exception> {
+            getTalerAmount("5", "")
+        }
+    }
+
+    // Checks that the input decodes to a 32-bytes value.
+    @Test
+    fun validateReservePub() {
+        val valid = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
+        val validBytes = isReservePub(valid)
+        assertNotNull(validBytes)
+        assertEquals(32, validBytes.size)
+        assertNull(isReservePub("noise"))
+        val trimmedInput = valid.dropLast(10)
+        assertNull(isReservePub(trimmedInput))
+        val invalidChar = StringBuilder(valid)
+        invalidChar.setCharAt(10, '*')
+        assertNull(isReservePub(invalidChar.toString()))
+        // assertNull(isReservePub(valid.dropLast(1))) // FIXME: this fails 
now because the decoder is buggy.
+    }
+}
\ No newline at end of file
diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt
index c0b85171..dc0ebb5a 100644
--- a/util/src/main/kotlin/time.kt
+++ b/util/src/main/kotlin/time.kt
@@ -20,8 +20,8 @@
 package tech.libeufin.util
 
 import java.time.*
+import java.time.format.DateTimeFormatter
 import java.time.temporal.ChronoUnit
-import java.util.concurrent.TimeUnit
 
 /**
  * Converts the 'this' Instant to the number of nanoseconds
@@ -79,4 +79,16 @@ fun Long.microsToJavaInstant(): Instant? {
         logger.error(e.message)
         return null
     }
+}
+
+/**
+ * Parses one timestamp from the ISO 8601 format.
+ *
+ * @param timeFromXml input time string from the XML
+ * @return [Instant] in the UTC timezone
+ */
+fun parseGregorianTime(timeFromXml: String): Instant {
+    val formatter = DateTimeFormatter.ISO_DATE_TIME.parse(timeFromXml)
+    return Instant.from(formatter)
+
 }
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]