libeufin

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

commit b5f49fe4dc38c8e4f818fc45eba7e1264160f581
parent f33614e11329d738d4bd5ca08b50cb5705ca7ee8
Author: MS <ms@taler.net>
Date:   Wed, 25 Oct 2023 09:42:18 +0200

Nexus DB.

Method to create one incoming transaction, bounce it,
and finally create its related initiated outgoing payment
for the reimbursement.

Diffstat:
Mdatabase-versioning/libeufin-nexus-procedures.sql | 47+++++++++++++++++++++++++++++++++++++++++++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/Database.kt | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mnexus/src/test/kotlin/Common.kt | 1-
Mnexus/src/test/kotlin/DatabaseTest.kt | 26++++++++++++++++++++++++++
4 files changed, 125 insertions(+), 9 deletions(-)

diff --git a/database-versioning/libeufin-nexus-procedures.sql b/database-versioning/libeufin-nexus-procedures.sql @@ -1,7 +1,50 @@ BEGIN; SET search_path TO libeufin_nexus; -CREATE OR REPLACE FUNCTION create_outgoing_tx( +CREATE OR REPLACE FUNCTION create_incoming_and_bounce( + 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_timestamp BIGINT +) RETURNS void +LANGUAGE plpgsql AS $$ +BEGIN +-- creating the bounced incoming transaction. +INSERT INTO incoming_transactions ( + amount + ,wire_transfer_subject + ,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 + ); +-- creating its reimbursement. +INSERT INTO initiated_outgoing_transactions ( + amount + ,wire_transfer_subject + ,credit_payto_uri + ,initiation_time + ) VALUES ( + in_amount + ,'refund: ' || in_wire_transfer_subject + ,in_debit_payto_uri + ,in_timestamp + ); +END $$; + +COMMENT ON FUNCTION create_incoming_and_bounce(taler_amount, TEXT, BIGINT, TEXT, TEXT, BIGINT) + IS 'creates one incoming transaction with a bounced state and initiates its related refund.'; + +CREATE OR REPLACE FUNCTION create_outgoing_payment( IN in_amount taler_amount ,IN in_wire_transfer_subject TEXT ,IN in_execution_time BIGINT @@ -51,7 +94,7 @@ THEN END IF; END $$; -COMMENT ON FUNCTION create_outgoing_tx(taler_amount, TEXT, BIGINT, TEXT, TEXT, BIGINT) +COMMENT ON FUNCTION create_outgoing_payment(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( diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt @@ -28,8 +28,7 @@ data class IncomingPayment( val wireTransferSubject: String?, val debitPaytoUri: String, val executionTime: Instant, - val bankTransferId: String, - val bounced: Boolean + val bankTransferId: String ) // INITIATED PAYMENTS STRUCTS @@ -51,8 +50,18 @@ data class InitiatedPayment( * into the database. */ enum class PaymentInitiationOutcome { + /** + * The Payto address to send the payment to was invalid. + */ BAD_CREDIT_PAYTO, + /** + * The row contains a client_request_uid that exists + * already in the database. + */ UNIQUE_CONSTRAINT_VIOLATION, + /** + * Record successfully created. + */ SUCCESS } @@ -71,7 +80,18 @@ data class OutgoingPayment( * payment into the database. */ enum class OutgoingPaymentOutcome { + /** + * The caller wanted to link a previously initiated payment + * to this outgoing one, but the row ID passed to the inserting + * function could not be found in the payment initiations table. + * Note: NO insertion takes place in this case. + */ INITIATED_COUNTERPART_NOT_FOUND, + /** + * The outgoing payment got inserted and _in case_ the caller + * wanted to link a previously initiated payment to this one, that + * succeeded too. + */ SUCCESS } @@ -148,7 +168,7 @@ class Database(dbConfig: String): java.io.Closeable { ): OutgoingPaymentOutcome = runConn { val stmt = it.prepareStatement(""" SELECT out_nx_initiated - FROM create_outgoing_tx( + FROM create_outgoing_payment( (?,?)::taler_amount ,? ,? @@ -206,6 +226,37 @@ class Database(dbConfig: String): java.io.Closeable { } /** + * Creates an incoming payment as bounced _and_ initiates its + * reimbursement. Throws exception on unique constraint violation, + * or other errors. + * + * @param paymentData information related to the incoming payment. + */ + suspend fun incomingPaymentCreateBounced(paymentData: IncomingPayment) = 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 ( + (?,?)::taler_amount + ,? + ,? + ,? + ,? + ,? + )""") + 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.debitPaytoUri) + stmt.setString(6, paymentData.bankTransferId) + stmt.setLong(7, refundTimestamp) + stmt.executeQuery() + } + + /** * Creates a new incoming payment record in the database. * * @param paymentData information related to the incoming payment. @@ -219,14 +270,12 @@ class Database(dbConfig: String): java.io.Closeable { ,execution_time ,debit_payto_uri ,bank_transfer_id - ,bounced ) VALUES ( (?,?)::taler_amount ,? ,? ,? ,? - ,? ) """) stmt.setLong(1, paymentData.amount.value) @@ -238,7 +287,6 @@ class Database(dbConfig: String): java.io.Closeable { stmt.setLong(4, executionTime) stmt.setString(5, paymentData.debitPaytoUri) stmt.setString(6, paymentData.bankTransferId) - stmt.setBoolean(7, paymentData.bounced) return@runConn stmt.maybeUpdate() } diff --git a/nexus/src/test/kotlin/Common.kt b/nexus/src/test/kotlin/Common.kt @@ -88,7 +88,6 @@ fun genIncPay(subject: String? = null, rowUuid: String? = null) = debitPaytoUri = "payto://iban/not-used", wireTransferSubject = subject, executionTime = Instant.now(), - bounced = false, bankTransferId = "entropic" ) diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt @@ -43,6 +43,32 @@ class OutgoingPaymentsTest { } class IncomingPaymentsTest { + // Tests creating and bouncing incoming payments in one DB transaction. + @Test + fun incomingAndBounce() { + val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE)) + runBlocking { + // creating and bouncing one incoming transaction. + db.incomingPaymentCreateBounced(genIncPay("incoming and bounced")) + db.runConn { + // check the bounced flaag is true + val checkBounced = it.prepareStatement(""" + SELECT bounced FROM incoming_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 + COUNT(initiated_outgoing_transaction_id) AS how_many + FROM initiated_outgoing_transactions + """).executeQuery() + assertTrue(checkInitiated.next()) + assertEquals(1, checkInitiated.getInt("how_many")) + } + } + } + // Tests the function that flags incoming payments as bounced. @Test fun incomingPaymentBounce() {