commit a752f81acf05d1610452913e84f685eafbc43571
parent 7769c74a2f4e756ebf8f1ca79a885810ebc75765
Author: MS <ms@taler.net>
Date: Wed, 25 Oct 2023 14:05:05 +0200
Initiated payments UID.
Every initiated payment gets one UID in the 'request_uid'
column. This value could come either from a nexus-httpd client
(the Taler exchange, for example) or from Nexus itself, in
case the initiated payment bounces an incoming one.
Diffstat:
5 files changed, 52 insertions(+), 29 deletions(-)
diff --git a/database-versioning/libeufin-nexus-0001.sql b/database-versioning/libeufin-nexus-0001.sql
@@ -56,11 +56,15 @@ CREATE TABLE IF NOT EXISTS initiated_outgoing_transactions
,outgoing_transaction_id INT8 REFERENCES outgoing_transactions (outgoing_transaction_id)
,submitted BOOL DEFAULT FALSE
,hidden BOOL DEFAULT FALSE -- FIXME: explain this.
- ,client_request_uuid TEXT UNIQUE -- only there for HTTP requests idempotence.
+ ,request_uid TEXT NOT NULL 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.';
-
-COMMIT;
+ 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.';
+COMMENT ON COLUMN initiated_outgoing_transactions.request_uid
+ IS 'Unique identifier of this outgoing transaction initiation.
+This value could come both from a nexus-httpd client or directly
+generated when nexus-fetch bounces one payment. In both cases, this
+value will be used as a unique identifier for its related pain.001 document.';
+COMMIT;
+\ No newline at end of file
diff --git a/database-versioning/libeufin-nexus-procedures.sql b/database-versioning/libeufin-nexus-procedures.sql
@@ -8,6 +8,7 @@ CREATE OR REPLACE FUNCTION create_incoming_and_bounce(
,IN in_debit_payto_uri TEXT
,IN in_bank_transfer_id TEXT
,IN in_timestamp BIGINT
+ ,IN in_request_uid TEXT
) RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
@@ -33,15 +34,17 @@ INSERT INTO initiated_outgoing_transactions (
,wire_transfer_subject
,credit_payto_uri
,initiation_time
+ ,request_uid
) VALUES (
in_amount
,'refund: ' || in_wire_transfer_subject
,in_debit_payto_uri
,in_timestamp
+ ,in_request_uid
);
END $$;
-COMMENT ON FUNCTION create_incoming_and_bounce(taler_amount, TEXT, BIGINT, TEXT, TEXT, BIGINT)
+COMMENT ON FUNCTION create_incoming_and_bounce(taler_amount, TEXT, BIGINT, TEXT, TEXT, BIGINT, TEXT)
IS 'creates one incoming transaction with a bounced state and initiates its related refund.';
CREATE OR REPLACE FUNCTION create_outgoing_payment(
@@ -100,6 +103,7 @@ COMMENT ON FUNCTION create_outgoing_payment(taler_amount, TEXT, BIGINT, TEXT, TE
CREATE OR REPLACE FUNCTION bounce_payment(
IN in_incoming_transaction_id BIGINT
,IN in_initiation_time BIGINT
+ ,IN in_request_uid TEXT
,OUT out_nx_incoming_payment BOOLEAN
)
LANGUAGE plpgsql AS $$
@@ -110,12 +114,14 @@ INSERT INTO initiated_outgoing_transactions (
,wire_transfer_subject
,credit_payto_uri
,initiation_time
+ ,request_uid
)
SELECT
amount
,'refund: ' || wire_transfer_subject
,debit_payto_uri
,in_initiation_time
+ ,in_request_uid
FROM incoming_transactions
WHERE incoming_transaction_id = in_incoming_transaction_id;
@@ -134,4 +140,4 @@ UPDATE incoming_transactions
WHERE incoming_transaction_id = in_incoming_transaction_id;
END $$;
-COMMENT ON FUNCTION bounce_payment(BIGINT, BIGINT) IS 'Marks an incoming payment as bounced and initiates its refunding payment';
+COMMENT ON FUNCTION bounce_payment(BIGINT, BIGINT, TEXT) IS 'Marks an incoming payment as bounced and initiates its refunding payment';
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
@@ -42,7 +42,7 @@ data class InitiatedPayment(
val wireTransferSubject: String?,
val creditPaytoUri: String,
val initiationTime: Instant,
- val clientRequestUuid: String? = null
+ val requestUid: String
)
/**
@@ -207,18 +207,21 @@ class Database(dbConfig: String): java.io.Closeable {
* means that the actual value was returned to the initial debtor.
*
* @param rowId row ID of the payment to flag as bounced.
+ * @param initiatedRequestUid unique identifier for the outgoing payment to
+ * initiate for this bouncing.
* @return true if the payment could be set as bounced, false otherwise.
*/
- suspend fun incomingPaymentSetAsBounced(rowId: Long): Boolean = runConn { conn ->
+ suspend fun incomingPaymentSetAsBounced(rowId: Long, initiatedRequestUid: String): Boolean = runConn { conn ->
val timestamp = Instant.now().toDbMicros()
?: throw Exception("Could not convert Instant.now() to microseconds, won't bounce this payment.")
val stmt = conn.prepareStatement("""
SELECT out_nx_incoming_payment
- FROM bounce_payment(?,?)
+ FROM bounce_payment(?,?,?)
"""
)
stmt.setLong(1, rowId)
stmt.setLong(2, timestamp)
+ stmt.setString(3, initiatedRequestUid)
stmt.executeQuery().use { maybeResult ->
if (!maybeResult.next()) throw Exception("Expected outcome from the SQL bounce_payment function")
return@runConn !maybeResult.getBoolean("out_nx_incoming_payment")
@@ -227,12 +230,16 @@ 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.
+ * reimbursement.
*
* @param paymentData information related to the incoming payment.
+ * @param requestUid unique identifier of the outgoing payment to
+ * initiate, in order to reimburse the bounced tx.
*/
- suspend fun incomingPaymentCreateBounced(paymentData: IncomingPayment) = runConn { conn ->
+ suspend fun incomingPaymentCreateBounced(
+ paymentData: IncomingPayment,
+ requestUid: String
+ ) = 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()
@@ -245,6 +252,7 @@ class Database(dbConfig: String): java.io.Closeable {
,?
,?
,?
+ ,?
)""")
stmt.setLong(1, paymentData.amount.value)
stmt.setInt(2, paymentData.amount.fraction)
@@ -253,6 +261,7 @@ class Database(dbConfig: String): java.io.Closeable {
stmt.setString(5, paymentData.debitPaytoUri)
stmt.setString(6, paymentData.bankTransferId)
stmt.setLong(7, refundTimestamp)
+ stmt.setString(8, requestUid)
stmt.executeQuery()
}
@@ -325,7 +334,7 @@ class Database(dbConfig: String): java.io.Closeable {
,wire_transfer_subject
,credit_payto_uri
,initiation_time
- ,client_request_uuid
+ ,request_uid
FROM initiated_outgoing_transactions
WHERE submitted=false;
""")
@@ -347,7 +356,7 @@ class Database(dbConfig: String): java.io.Closeable {
creditPaytoUri = it.getString("credit_payto_uri"),
wireTransferSubject = it.getString("wire_transfer_subject"),
initiationTime = initiationTime,
- clientRequestUuid = it.getString("client_request_uuid")
+ requestUid = it.getString("request_uid")
)
} while (it.next())
}
@@ -368,7 +377,7 @@ class Database(dbConfig: String): java.io.Closeable {
,wire_transfer_subject
,credit_payto_uri
,initiation_time
- ,client_request_uuid
+ ,request_uid
) VALUES (
(?,?)::taler_amount
,?
@@ -389,7 +398,7 @@ class Database(dbConfig: String): java.io.Closeable {
throw Exception("Initiation time could not be converted to microseconds for the database.")
}
stmt.setLong(5, initiationTime)
- stmt.setString(6, paymentData.clientRequestUuid) // can be null.
+ stmt.setString(6, paymentData.requestUid) // can be null.
if (stmt.maybeUpdate())
return@runConn PaymentInitiationOutcome.SUCCESS
/**
diff --git a/nexus/src/test/kotlin/Common.kt b/nexus/src/test/kotlin/Common.kt
@@ -72,17 +72,17 @@ fun getPofiConfig(userId: String, partnerId: String) = """
""".trimIndent()
// Generates a payment initiation, given its subject.
-fun genInitPay(subject: String? = null, rowUuid: String? = null) =
+fun genInitPay(subject: String? = null, rowUid: String = "unique") =
InitiatedPayment(
amount = TalerAmount(44, 0, "KUDOS"),
creditPaytoUri = "payto://iban/not-used",
wireTransferSubject = subject,
initiationTime = Instant.now(),
- clientRequestUuid = rowUuid
+ requestUid = rowUid
)
// Generates an incoming payment, given its subject.
-fun genIncPay(subject: String? = null, rowUuid: String? = null) =
+fun genIncPay(subject: String? = null) =
IncomingPayment(
amount = TalerAmount(44, 0, "KUDOS"),
debitPaytoUri = "payto://iban/not-used",
diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt
@@ -49,7 +49,10 @@ class IncomingPaymentsTest {
val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
runBlocking {
// creating and bouncing one incoming transaction.
- db.incomingPaymentCreateBounced(genIncPay("incoming and bounced"))
+ db.incomingPaymentCreateBounced(
+ genIncPay("incoming and bounced"),
+ "UID"
+ )
db.runConn {
// check the bounced flaag is true
val checkBounced = it.prepareStatement("""
@@ -86,13 +89,13 @@ class IncomingPaymentsTest {
assertTrue(expectNotBounced.next())
assertFalse(expectNotBounced.getBoolean("bounced"))
// now bouncing it.
- assertTrue(db.incomingPaymentSetAsBounced(1))
+ 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))
+ assertFalse(db.incomingPaymentSetAsBounced(5, "unique 1"))
}
}
}
@@ -168,7 +171,7 @@ class PaymentInitiationsTest {
amount = TalerAmount(44, 0, "KUDOS"),
creditPaytoUri = "payto://iban/not-used",
wireTransferSubject = "test",
- clientRequestUuid = "unique",
+ requestUid = "unique",
initiationTime = Instant.now()
)
runBlocking {
@@ -178,7 +181,7 @@ class PaymentInitiationsTest {
assertTrue {
haveOne.size == 1
&& haveOne.containsKey(1)
- && haveOne[1]?.clientRequestUuid == "unique"
+ && haveOne[1]?.requestUid == "unique"
}
}
}
@@ -189,11 +192,11 @@ class PaymentInitiationsTest {
fun paymentInitiationsMultiple() {
val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
runBlocking {
- assertEquals(db.initiatedPaymentCreate(genInitPay("#1")), PaymentInitiationOutcome.SUCCESS)
- assertEquals(db.initiatedPaymentCreate(genInitPay("#2")), PaymentInitiationOutcome.SUCCESS)
- assertEquals(db.initiatedPaymentCreate(genInitPay("#3")), PaymentInitiationOutcome.SUCCESS)
- assertEquals(db.initiatedPaymentCreate(genInitPay("#4")), PaymentInitiationOutcome.SUCCESS)
- assertEquals(db.initiatedPaymentCreate(genInitPay()), PaymentInitiationOutcome.SUCCESS) // checking the nullable subject
+ assertEquals(db.initiatedPaymentCreate(genInitPay("#1", "unique1")), PaymentInitiationOutcome.SUCCESS)
+ assertEquals(db.initiatedPaymentCreate(genInitPay("#2", "unique2")), PaymentInitiationOutcome.SUCCESS)
+ assertEquals(db.initiatedPaymentCreate(genInitPay("#3", "unique3")), PaymentInitiationOutcome.SUCCESS)
+ assertEquals(db.initiatedPaymentCreate(genInitPay("#4", "unique4")), PaymentInitiationOutcome.SUCCESS)
+ assertEquals(db.initiatedPaymentCreate(genInitPay(rowUid = "unique5")), PaymentInitiationOutcome.SUCCESS) // checking the nullable subject
// Marking one as submitted, hence not expecting it in the results.
db.runConn { conn ->