libeufin

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

commit 217975d6bc77d84aa391cb02bba2bf4f317576a5
parent cc1bc351553f18fe464935a53e7bd4a2818bf178
Author: MS <ms@taler.net>
Date:   Tue, 24 Oct 2023 15:32:31 +0200

nexus db: outgoing payments logic.

Diffstat:
Mdatabase-versioning/libeufin-nexus-procedures.sql | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/Database.kt | 69++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mnexus/src/test/kotlin/Common.kt | 12+++++++++++-
Mnexus/src/test/kotlin/DatabaseTest.kt | 44+++++++++++++++++++++++++++++++++++++++-----
4 files changed, 171 insertions(+), 7 deletions(-)

diff --git a/database-versioning/libeufin-nexus-procedures.sql b/database-versioning/libeufin-nexus-procedures.sql @@ -1,6 +1,59 @@ BEGIN; SET search_path TO libeufin_nexus; +CREATE OR REPLACE FUNCTION create_outgoing_tx( + IN in_amount taler_amount + ,IN in_wire_transfer_subject TEXT + ,IN in_execution_time BIGINT + ,IN in_credit_payto_uri TEXT + ,IN in_bank_transfer_id TEXT + ,IN in_initiated_id BIGINT + ,OUT out_nx_initiated BOOLEAN +) +LANGUAGE plpgsql AS $$ +DECLARE +new_outgoing_transaction_id BIGINT; +BEGIN + +IF in_initiated_id IS NULL THEN + out_nx_initiated = FALSE; +ELSE + PERFORM 1 + FROM initiated_outgoing_transactions + WHERE initiated_outgoing_transaction_id = in_initiated_id; + IF NOT FOUND THEN + out_nx_initiated = TRUE; + RETURN; + END IF; +END IF; + +INSERT INTO outgoing_transactions ( + amount + ,wire_transfer_subject + ,execution_time + ,credit_payto_uri + ,bank_transfer_id +) VALUES ( + in_amount + ,in_wire_transfer_subject + ,in_execution_time + ,in_credit_payto_uri + ,in_bank_transfer_id +) + RETURNING outgoing_transaction_id + INTO new_outgoing_transaction_id; + +IF in_initiated_id IS NOT NULL +THEN + UPDATE initiated_outgoing_transactions + SET outgoing_transaction_id = new_outgoing_transaction_id + WHERE initiated_outgoing_transaction_id = in_initiated_id; +END IF; +END $$; + +COMMENT ON FUNCTION create_outgoing_tx(taler_amount, TEXT, BIGINT, TEXT, TEXT, BIGINT) + IS 'Creates a new outgoing payment and optionally reconciles the related initiated payment with it. If the initiated payment to reconcile is not found, it inserts NOTHING.'; + CREATE OR REPLACE FUNCTION bounce_payment( IN in_incoming_transaction_id BIGINT ,IN in_initiation_time BIGINT diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt @@ -25,7 +25,7 @@ data class TalerAmount( */ data class IncomingPayment( val amount: TalerAmount, - val wireTransferSubject: String, + val wireTransferSubject: String?, val debitPaytoUri: String, val executionTime: Instant, val bankTransferId: String, @@ -56,6 +56,25 @@ enum class PaymentInitiationOutcome { SUCCESS } +// OUTGOING PAYMENTS STRUCTS + +data class OutgoingPayment( + val amount: TalerAmount, + val wireTransferSubject: String?, + val executionTime: Instant, + val creditPaytoUri: String, + val bankTransferId: String +) + +/** + * Witnesses the outcome of inserting an outgoing + * payment into the database. + */ +enum class OutgoingPaymentOutcome { + INITIATED_COUNTERPART_NOT_FOUND, + SUCCESS +} + /** * Performs a INSERT, UPDATE, or DELETE operation. * @@ -111,6 +130,54 @@ class Database(dbConfig: String): java.io.Closeable { } } + // OUTGOING PAYMENTS METHODS + + /** + * Creates one outgoing payment OPTIONALLY reconciling it with its + * initiated payment counterpart. + * + * @param paymentData information about the outgoing payment. + * @param reconcileId optional row ID of the initiated payment + * that will reference this one. Note: if this value is + * not found, then NO row gets inserted in the database. + * @return operation outcome enum. + */ + suspend fun outgoingPaymentCreate( + paymentData: OutgoingPayment, + reconcileId: Long? = null + ): OutgoingPaymentOutcome = runConn { + val stmt = it.prepareStatement(""" + SELECT out_nx_initiated + FROM create_outgoing_tx( + (?,?)::taler_amount + ,? + ,? + ,? + ,? + ,? + )""" + ) + val executionTime = paymentData.executionTime.toDbMicros() + ?: throw Exception("Could not convert outgoing payment execution_time to microseconds") + stmt.setLong(1, paymentData.amount.value) + stmt.setInt(2, paymentData.amount.fraction) + stmt.setString(3, paymentData.wireTransferSubject) + stmt.setLong(4, executionTime) + stmt.setString(5, paymentData.creditPaytoUri) + stmt.setString(6, paymentData.bankTransferId) + if (reconcileId == null) + stmt.setNull(7, java.sql.Types.BIGINT) + else + stmt.setLong(7, reconcileId) + + stmt.executeQuery().use { + if (!it.next()) throw Exception("Inserting outgoing payment gave no outcome.") + if (it.getBoolean("out_nx_initiated")) + return@runConn OutgoingPaymentOutcome.INITIATED_COUNTERPART_NOT_FOUND + } + return@runConn OutgoingPaymentOutcome.SUCCESS + } + // INCOMING PAYMENTS METHODS /** diff --git a/nexus/src/test/kotlin/Common.kt b/nexus/src/test/kotlin/Common.kt @@ -82,7 +82,7 @@ fun genInitPay(subject: String, rowUuid: String? = null) = ) // Generates an incoming payment, given its subject. -fun genIncPay(subject: String, rowUuid: String? = null) = +fun genIncPay(subject: String? = null, rowUuid: String? = null) = IncomingPayment( amount = TalerAmount(44, 0, "KUDOS"), debitPaytoUri = "payto://iban/not-used", @@ -90,4 +90,14 @@ fun genIncPay(subject: String, rowUuid: String? = null) = executionTime = Instant.now(), bounced = false, bankTransferId = "entropic" + ) + +// Generates an outgoing payment, given its subject. +fun genOutPay(subject: String? = null) = + OutgoingPayment( + amount = TalerAmount(44, 0, "KUDOS"), + creditPaytoUri = "payto://iban/not-used", + wireTransferSubject = subject, + executionTime = Instant.now(), + 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,14 +1,46 @@ 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.nexus.* import java.time.Instant import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue + +class OutgoingPaymentsTest { + + /** + * Tests the insertion of outgoing payments, including + * the case where we reconcile with an initiated payment. + */ + @Test + fun outgoingPaymentCreation() { + val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE)) + runBlocking { + // inserting without reconciling + assertEquals( + OutgoingPaymentOutcome.SUCCESS, + db.outgoingPaymentCreate(genOutPay("paid by nexus")) + ) + // inserting trying to reconcile with a non-existing initiated payment. + assertEquals( + OutgoingPaymentOutcome.INITIATED_COUNTERPART_NOT_FOUND, + db.outgoingPaymentCreate(genOutPay("paid by nexus"), 5) + ) + // initiating a payment to reconcile later. Takes row ID == 1 + assertEquals( + PaymentInitiationOutcome.SUCCESS, + db.initiatedPaymentCreate(genInitPay("waiting for reconciliation")) + ) + // Creating an outgoing payment, reconciling it with the one above. + assertEquals( + OutgoingPaymentOutcome.SUCCESS, + db.outgoingPaymentCreate(genOutPay(), 1) + ) + } + } +} + class IncomingPaymentsTest { // Tests the function that flags incoming payments as bounced. @Test @@ -50,13 +82,15 @@ class IncomingPaymentsTest { assertTrue(res.next()) assertEquals(0, res.getInt("how_many")) } - db.incomingPaymentCreate(genIncPay("singleton")) + 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())) } } }