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:
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() {