commit 2e081ba33d8f300f2a62a7b0ffb1afb4dfa9083e
parent 4c7d9b70d583faf4cbc14d755f22fc44e0ac2dfe
Author: Antoine A <>
Date: Sat, 4 Oct 2025 18:48:21 +0200
bank: during cashout user account legal name
Diffstat:
3 files changed, 69 insertions(+), 10 deletions(-)
diff --git a/database-versioning/libeufin-bank-0014.sql b/database-versioning/libeufin-bank-0014.sql
@@ -19,6 +19,7 @@ SELECT _v.register_patch('libeufin-bank-0014', NULL, NULL);
SET search_path TO libeufin_bank;
+-- Cashout request UID need to be null for code triggered cashouts
ALTER TABLE cashout_operations DROP CONSTRAINT cashout_operations_pkey;
ALTER TABLE cashout_operations ADD CONSTRAINT request_uid_unique UNIQUE (request_uid);
ALTER TABLE cashout_operations ALTER COLUMN request_uid DROP NOT NULL;
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -39,6 +39,25 @@ BEGIN
END
$do$;
+CREATE FUNCTION url_encode(input TEXT)
+RETURNS TEXT
+LANGUAGE plpgsql IMMUTABLE AS $$
+DECLARE
+ result TEXT := '';
+ char TEXT;
+BEGIN
+ FOR i IN 1..length(input) LOOP
+ char := substring(input FROM i FOR 1);
+ IF char ~ '[A-Za-z0-9\-._~]' THEN
+ result := result || char;
+ ELSE
+ result := result || '%' || lpad(upper(to_hex(ascii(char))), 2, '0');
+ END IF;
+ END LOOP;
+ RETURN result;
+END;
+$$;
+
CREATE FUNCTION amount_normalize(
IN amount taler_amount
,OUT normalized taler_amount
@@ -1464,7 +1483,9 @@ BEGIN
-- Check account exists, has all info and if 2FA is required
SELECT
bank_account_id, is_taler_exchange, conversion_rate_class_id,
- cashout_payto, (NOT in_is_tan AND tan_channel IS NOT NULL)
+ -- Remove potential residual query string an add the receiver_name
+ split_part(cashout_payto, '?', 1) || '?receiver-name=' || url_encode(name),
+ (NOT in_is_tan AND tan_channel IS NOT NULL)
INTO
account_id, out_account_is_exchange, account_conversion_rate_class_id,
account_cashout_payto, out_tan_required
diff --git a/testbench/src/test/kotlin/IntegrationTest.kt b/testbench/src/test/kotlin/IntegrationTest.kt
@@ -264,8 +264,7 @@ class IntegrationTest {
@Test
fun conversion() {
-
- suspend fun NexusDb.checkInitiated(amount: TalerAmount) {
+ suspend fun NexusDb.checkInitiated(amount: TalerAmount, name: String?) {
serializable(
"""
SELECT
@@ -279,10 +278,11 @@ class IntegrationTest {
) {
one {
val am = it.getAmount("amount", amount.currency)
+ println(it.getString("credit_payto"))
val payto = it.getIbanPayto("credit_payto")
val subject = it.getString("subject")
- println("$am $payto '$subject'")
assertEquals(amount, am)
+ assertEquals(payto.receiverName, name)
}
}
}
@@ -308,7 +308,7 @@ class IntegrationTest {
json {
"username" to "customer"
"password" to "customer-password"
- "name" to "JohnSmith"
+ "name" to "John Smith"
"internal_payto_uri" to userPayTo
"cashout_payto_uri" to fiatPayTo
"debit_threshold" to "KUDOS:100"
@@ -368,10 +368,10 @@ class IntegrationTest {
"amount_credit" to converted
}
}.assertOkJson<CashoutResponse>()
- db.checkInitiated(converted)
+ db.checkInitiated(converted, "John Smith")
}
- // Exchange bounce
+ // Exchange bounce no name
repeat(3) { i ->
val reservePub = EddsaPublicKey.randEdsaKey()
val amount = TalerAmount("EUR:${30+i}")
@@ -394,7 +394,43 @@ class IntegrationTest {
}
// Bounce
- val transferId = client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
+ client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
+ json {
+ "request_uid" to HashCode.rand()
+ "amount" to converted
+ "exchange_base_url" to "http://exchange.example.com/"
+ "wtid" to reservePub
+ "credit_account" to "payto://x-taler-bank/localhost/admin"
+ }
+ }.assertOkJson<TransferResponse>()
+
+ db.checkInitiated(amount, null)
+ }
+
+ // Exchange bounce with name
+ repeat(3) { i ->
+ val reservePub = EddsaPublicKey.randEdsaKey()
+ val amount = TalerAmount("EUR:${40+i}")
+ val subject = "exchange bounce test $i: $reservePub"
+
+ // Cashin
+ nexusCmd.run("testing fake-incoming $flags --subject \"$subject\" --amount $amount $userPayTo?receiver-name=John%20d%27Smith")
+ val converted = client.get("/conversion-info/cashin-rate?amount_debit=EUR:${40 + i}")
+ .assertOkJson<ConversionResponse>().amount_credit
+ client.getA("/accounts/exchange/transactions").assertOkJson<BankAccountTransactionsResponse> {
+ val tx = it.transactions.first()
+ assertEquals(subject, tx.subject)
+ assertEquals(converted, tx.amount)
+ }
+ client.getA("/accounts/exchange/taler-wire-gateway/history/incoming").assertOkJson<IncomingHistory> {
+ val tx = it.incoming_transactions.first()
+ assertEquals(converted, tx.amount)
+ assertIs<IncomingReserveTransaction>(tx)
+ assertEquals(reservePub, tx.reserve_pub)
+ }
+
+ // Bounce
+ client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
json {
"request_uid" to HashCode.rand()
"amount" to converted
@@ -402,8 +438,9 @@ class IntegrationTest {
"wtid" to reservePub
"credit_account" to "payto://x-taler-bank/localhost/admin"
}
- }.assertOkJson<TransferResponse>().row_id
- db.checkInitiated(amount)
+ }.assertOkJson<TransferResponse>()
+
+ db.checkInitiated(amount, "John d'Smith")
}
}
}