libeufin

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

commit f803d69f428be15486efc58d07eb3b2ee2b0c327
parent 48d2df00c3e631763f5f0fb7ad9803bd4a999d2a
Author: MS <ms@taler.net>
Date:   Tue, 24 Oct 2023 10:56:54 +0200

Nexus database.

Creating incoming payments and setting them as submitted.

Diffstat:
Mdatabase-versioning/libeufin-nexus-0001.sql | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/Database.kt | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mnexus/src/test/kotlin/Common.kt | 11+++++++++++
Mnexus/src/test/kotlin/DatabaseTest.kt | 30+++++++++++++++++++++++++++---
Mutil/src/main/kotlin/DB.kt | 2+-
5 files changed, 119 insertions(+), 7 deletions(-)

diff --git a/database-versioning/libeufin-nexus-0001.sql b/database-versioning/libeufin-nexus-0001.sql @@ -58,7 +58,7 @@ CREATE TABLE IF NOT EXISTS initiated_outgoing_transactions ,hidden BOOL DEFAULT FALSE -- FIXME: explain this. ,client_request_uuid TEXT UNIQUE ,failure_message TEXT -- NOTE: that may mix soon failures (those found at initiation time), or late failures (those found out along a fetch operation) - ); + ); COMMENT ON COLUMN initiated_outgoing_transactions.outgoing_transaction_id IS 'Points to the bank transaction that was found via nexus-fetch. If "submitted" is false or nexus-fetch could not download this initiation, this column is expected to be NULL.'; diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt @@ -12,14 +12,28 @@ import java.sql.PreparedStatement import java.sql.SQLException import java.time.Instant -/* only importing TalerAmount from bank ONCE that Nexus has -* its httpd component. */ data class TalerAmount( val value: Long, val fraction: Int, val currency: String ) +// INCOMING PAYMENTS STRUCTS + +/** + * Represents an incoming payment in the database. + */ +data class IncomingPayment( + val amount: TalerAmount, + val wireTransferSubject: String, + val debitPaytoUri: String, + val executionTime: Instant, + val bankTransferId: String, + val bounced: Boolean +) + +// INITIATED PAYMENTS STRUCTS + /** * Minimal set of information to initiate a new payment in * the database. @@ -97,6 +111,69 @@ class Database(dbConfig: String): java.io.Closeable { } } + // INCOMING PAYMENTS METHODS + + /** + * Flags an incoming payment as bounced. NOTE: the flag merely means + * that the payment had an invalid subject for a Taler withdrawal _and_ + * it got sent to the initiated outgoing payments. In NO way this flag + * means that the actual value was returned to the initial debtor. + * + * FIXME: this needs to run within the same transaction where the payment gets initiated. + * + * @param rowId row ID of the payment to flag as bounced. + * @return true on success, false otherwise. + */ + suspend fun incomingPaymentSetAsBounced(rowId: Long): Boolean = runConn { conn -> + val stmt = conn.prepareStatement(""" + UPDATE incoming_transactions + SET bounced = true + WHERE incoming_transaction_id=? + """ + ) + stmt.setLong(1, rowId) + return@runConn stmt.maybeUpdate() + } + + /** + * Creates a new incoming payment record in the database. + * + * @param paymentData information related to the incoming payment. + * @return true on success, false otherwise. + */ + suspend fun incomingPaymentCreate(paymentData: IncomingPayment): Boolean = runConn { conn -> + val stmt = conn.prepareStatement(""" + INSERT INTO incoming_transactions ( + amount + ,wire_transfer_subject + ,execution_time + ,debit_payto_uri + ,bank_transfer_id + ,bounced + ) VALUES ( + (?,?)::taler_amount + ,? + ,? + ,? + ,? + ,? + ) + """) + 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) + stmt.setBoolean(7, paymentData.bounced) + return@runConn stmt.maybeUpdate() + } + + // INITIATED PAYMENTS METHODS + /** * Sets payment initiation as submitted. * diff --git a/nexus/src/test/kotlin/Common.kt b/nexus/src/test/kotlin/Common.kt @@ -79,4 +79,15 @@ fun genInitPay(subject: String, rowUuid: String? = null) = wireTransferSubject = subject, initiationTime = Instant.now(), clientRequestUuid = rowUuid + ) + +// Generates an incoming payment, given its subject. +fun genIncPay(subject: String, rowUuid: String? = null) = + IncomingPayment( + amount = TalerAmount(44, 0, "KUDOS"), + debitPaytoUri = "payto://iban/not-used", + wireTransferSubject = subject, + executionTime = Instant.now(), + bounced = false, + bankTransferId = "entropic" ) \ No newline at end of file diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt @@ -1,16 +1,41 @@ +import io.ktor.client.* +import io.ktor.client.request.* import kotlinx.coroutines.runBlocking import org.junit.Test import tech.libeufin.nexus.InitiatedPayment import tech.libeufin.nexus.NEXUS_CONFIG_SOURCE import tech.libeufin.nexus.PaymentInitiationOutcome import tech.libeufin.nexus.TalerAmount -import tech.libeufin.util.connectWithSchema +import tech.libeufin.util.transaction import java.time.Instant import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class DatabaseTest { +class IncomingPaymentsTest { + // 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")) + } + 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")) + } + } + } +} +class PaymentInitiationsTest { // Tests the flagging of payments as submitted. @Test fun paymentInitiationSetAsSubmitted() { @@ -34,7 +59,6 @@ class DatabaseTest { } // Switching the submitted state to true. assertTrue(db.initiatedPaymentSetSubmitted(1)) - // Asserting on the submitted state being TRUE now. db.runConn { conn -> val isSubmitted = conn.execSQLQuery(getRowOne) diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt @@ -345,6 +345,7 @@ fun pgDataSource(dbConfig: String): PGSimpleDataSource { fun PGSimpleDataSource.pgConnection(): PgConnection { val conn = connection.unwrap(PgConnection::class.java) + // FIXME: bring the DB schema to a function argument. conn.execSQLUpdate("SET search_path TO libeufin_bank;") return conn } @@ -401,7 +402,6 @@ fun initializeDatabaseTables(cfg: DatabaseConfig, sqlFilePrefix: String) { conn.execSQLUpdate(sqlPatchText) } val sqlProcedures = File("${cfg.sqlDir}/procedures.sql") - // Nexus doesn't have any procedures. if (!sqlProcedures.exists()) { logger.info("No procedures.sql for the SQL collection: $sqlFilePrefix") return@transaction