libeufin

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

commit 2e081ba33d8f300f2a62a7b0ffb1afb4dfa9083e
parent 4c7d9b70d583faf4cbc14d755f22fc44e0ac2dfe
Author: Antoine A <>
Date:   Sat,  4 Oct 2025 18:48:21 +0200

bank: during cashout user account legal name

Diffstat:
Mdatabase-versioning/libeufin-bank-0014.sql | 1+
Mdatabase-versioning/libeufin-bank-procedures.sql | 23++++++++++++++++++++++-
Mtestbench/src/test/kotlin/IntegrationTest.kt | 55++++++++++++++++++++++++++++++++++++++++++++++---------
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") } } }