libeufin

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

commit cd5fa2eebe83150ae1229e6f3eebf9908c82ae29
parent fd77974c95f07882bf566840b3cb345a08d33923
Author: Antoine A <>
Date:   Mon,  7 Oct 2024 19:44:54 +0200

bank: wire gateway transfer generate failure for unknown accounts

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt | 5++---
Mbank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt | 55+++++++++++++++++++++++++++++++------------------------
Mbank/src/test/kotlin/WireGatewayApiTest.kt | 54+++++++++++++++++++++++++++++++++++++++++++-----------
Mbank/src/test/kotlin/bench.kt | 25++++++++++++++++++-------
Mbank/src/test/kotlin/helpers.kt | 4++--
Mcommon/src/main/kotlin/TalerCommon.kt | 8++++++--
Mcommon/src/main/kotlin/db/types.kt | 6++++--
Adatabase-versioning/libeufin-bank-0009.sql | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdatabase-versioning/libeufin-bank-procedures.sql | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mtestbench/src/test/kotlin/MigrationTest.kt | 3+++
10 files changed, 255 insertions(+), 82 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt @@ -60,7 +60,6 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { "$username is not an exchange account.", TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE ) - is TransferResult.UnknownCreditor -> throw unknownCreditorAccount(req.credit_account.canonical) is TransferResult.BothPartyAreExchange -> throw conflict( "Wire transfer attempted with credit and debit party being both exchange account", TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE @@ -119,10 +118,10 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE ) - if (params.status != null && params.status != TransferStatusState.success) { + if (params.status != null && params.status != TransferStatusState.success && params.status != TransferStatusState.permanent_failure) { call.respond(HttpStatusCode.NoContent) } else { - val items = db.exchange.pageTransfer(params.page, bankAccount.bankAccountId, ctx.payto) + val items = db.exchange.pageTransfer(params.page, bankAccount.bankAccountId, params.status, ctx.payto) if (items.isEmpty()) { call.respond(HttpStatusCode.NoContent) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt @@ -104,7 +104,6 @@ class ExchangeDAO(private val db: Database) { data class Success(val id: Long, val timestamp: TalerProtocolTimestamp): TransferResult data object NotAnExchange: TransferResult data object UnknownExchange: TransferResult - data object UnknownCreditor: TransferResult data object BothPartyAreExchange: TransferResult data object BalanceInsufficient: TransferResult data object ReserveUidReuse: TransferResult @@ -120,7 +119,6 @@ class ExchangeDAO(private val db: Database) { SELECT out_debtor_not_found ,out_debtor_not_exchange - ,out_creditor_not_found ,out_both_exchanges ,out_request_uid_reuse ,out_exchange_balance_insufficient @@ -150,7 +148,6 @@ class ExchangeDAO(private val db: Database) { when { it.getBoolean("out_debtor_not_found") -> TransferResult.UnknownExchange it.getBoolean("out_debtor_not_exchange") -> TransferResult.NotAnExchange - it.getBoolean("out_creditor_not_found") -> TransferResult.UnknownCreditor it.getBoolean("out_both_exchanges") -> TransferResult.BothPartyAreExchange it.getBoolean("out_exchange_balance_insufficient") -> TransferResult.BalanceInsufficient it.getBoolean("out_request_uid_reuse") -> TransferResult.ReserveUidReuse @@ -172,27 +169,27 @@ class ExchangeDAO(private val db: Database) { SELECT wtid ,exchange_base_url - ,transaction_date + ,transfer_date ,(amount).val AS amount_val ,(amount).frac AS amount_frac ,creditor_payto - ,creditor_name - FROM taler_exchange_outgoing - JOIN bank_account_transactions ON bank_transaction=bank_transaction_id - WHERE bank_transaction_id=? AND bank_account_id=? + ,status + ,status_msg + FROM transfer_operations + WHERE transfer_operation_id=? AND exchange_id=? """ ) { setLong(1, txId) setLong(2, exchangeId) oneOrNull { TransferStatus( - status = TransferStatusState.success, - status_msg = null, + status = it.getEnum<TransferStatusState>("status"), + status_msg = it.getString("status_msg"), amount = it.getAmount("amount", db.bankCurrency), origin_exchange_url = it.getString("exchange_base_url"), wtid = ShortHashCode(it.getBytes("wtid")), - credit_account = it.getBankPayto("creditor_payto", "creditor_name", ctx), - timestamp = it.getTalerTimestamp("transaction_date"), + credit_account = it.getBankPayto("creditor_payto", null, ctx), + timestamp = it.getTalerTimestamp("transfer_date"), ) } } @@ -201,33 +198,43 @@ class ExchangeDAO(private val db: Database) { suspend fun pageTransfer( params: PageParams, exchangeId: Long, + status: TransferStatusState?, ctx: BankPaytoCtx ): List<TransferListStatus> = db.page( params, - "bank_transaction_id", + "transfer_operation_id", """ SELECT - bank_transaction_id - ,transaction_date + transfer_operation_id + ,transfer_date ,(amount).val AS amount_val ,(amount).frac AS amount_frac ,creditor_payto - ,creditor_name - FROM taler_exchange_outgoing - JOIN bank_account_transactions ON bank_transaction=bank_transaction_id - WHERE bank_account_id=? AND + ,status + FROM transfer_operations + WHERE exchange_id=? AND ${ + when (status) { + null -> "" + else -> "status=?::transfer_status AND" + } + } """, { setLong(1, exchangeId) - 1 + if (status == null) { + 1 + } else { + setString(2, status.name) + 2 + } } ) { TransferListStatus( - row_id = it.getLong("bank_transaction_id"), - status = TransferStatusState.success, + row_id = it.getLong("transfer_operation_id"), + status = it.getEnum<TransferStatusState>("status"), amount = it.getAmount("amount", db.bankCurrency), - credit_account = it.getBankPayto("creditor_payto", "creditor_name", ctx), - timestamp = it.getTalerTimestamp("transaction_date"), + credit_account = it.getBankPayto("creditor_payto", null, ctx), + timestamp = it.getTalerTimestamp("transfer_date"), ) } diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -76,15 +76,6 @@ class WireGatewayApiTest { } }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - // Unknown account - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "request_uid" to HashCode.rand() - "wtid" to ShortHashCode.rand() - "credit_account" to unknownPayto - } - }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR) - // Same account client.postA("/accounts/exchange/taler-wire-gateway/transfer") { json(valid_req) { @@ -126,7 +117,7 @@ class WireGatewayApiTest { // GET /accounts/{USERNAME}/taler-wire-gateway/transfers/{ROW_ID} @Test fun transferById() = bankSetup { - val wtid = ShortHashCode.rand() + var wtid = ShortHashCode.rand() val valid_req = obj { "request_uid" to HashCode.rand() "amount" to "KUDOS:0.12" @@ -150,6 +141,24 @@ class WireGatewayApiTest { assertEquals(wtid, tx.wtid) assertEquals(resp.timestamp, tx.timestamp) } + // Unknown account + wtid = ShortHashCode.rand() + client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + json(valid_req) { + "request_uid" to HashCode.rand() + "wtid" to wtid + "credit_account" to unknownPayto + } + }.assertOkJson<TransferResponse> { resp -> + client.getA("/accounts/exchange/taler-wire-gateway/transfers/${resp.row_id}") + .assertOkJson<TransferStatus> { tx -> + assertEquals(TransferStatusState.permanent_failure, tx.status) + assertEquals(TalerAmount("KUDOS:0.12"), tx.amount) + assertEquals("http://exchange.example.com/", tx.origin_exchange_url) + assertEquals(wtid, tx.wtid) + assertEquals(resp.timestamp, tx.timestamp) + } + } // Check unknown transaction client.getA("/accounts/exchange/taler-wire-gateway/transfers/42") .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) @@ -172,7 +181,7 @@ class WireGatewayApiTest { "amount" to "KUDOS:0.12" "exchange_base_url" to "http://exchange.example.com/" "wtid" to ShortHashCode.rand() - "credit_account" to merchantPayto.canonical + "credit_account" to merchantPayto } }.assertOkJson<TransferResponse>() } @@ -185,6 +194,26 @@ class WireGatewayApiTest { ) } client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=pending").assertNoContent() + client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=permanent_failure").assertNoContent() + + client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + json { + "request_uid" to HashCode.rand() + "amount" to "KUDOS:0.12" + "exchange_base_url" to "http://exchange.example.com/" + "wtid" to ShortHashCode.rand() + "credit_account" to unknownPayto + } + }.assertOkJson<TransferResponse>() + client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertOkJson<TransferList> { + assertEquals(6, it.transfers.size) + } + client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOkJson<TransferList> { + assertEquals(5, it.transfers.size) + } + client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=permanent_failure").assertOkJson<TransferList> { + assertEquals(1, it.transfers.size) + } } // GET /accounts/{USERNAME}/taler-wire-gateway/history/incoming @@ -235,6 +264,9 @@ class WireGatewayApiTest { { transfer("KUDOS:10") } ), ignored = listOf( + // Failed transfer + { transfer("KUDOS:10", unknownPayto) }, + // Ignore manual outgoing transaction { tx("exchange", "KUDOS:10", "merchant", "${ShortHashCode.rand()} http://exchange.example.com/") }, diff --git a/bank/src/test/kotlin/bench.kt b/bank/src/test/kotlin/bench.kt @@ -81,10 +81,18 @@ class Bench { val uuid = UUID.randomUUID() "$uuid\t$account\t\\\\x$hex\t0\n" }, - "taler_exchange_outgoing(wtid, request_uid, exchange_base_url, bank_transaction, creditor_account_id)" to { + "taler_exchange_outgoing(wtid, exchange_base_url, bank_transaction)" to { + val hex32 = token32.rand().encodeHex() + "\\\\x$hex32\t\\url\t${it*2-1}\n" + }, + "transfer_operations(wtid, request_uid, amount, exchange_base_url, exchange_outgoing_id, exchange_id, transfer_date, creditor_payto, status, status_msg)" to { val hex32 = token32.rand().encodeHex() val hex64 = token64.rand().encodeHex() - "\\\\x$hex32\t\\\\x$hex64\turl\t${it*2-1}\t$it\n" + if (it % 2 == 0) { + "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t$it\t$it\t0\tpayto://x-taler-bank/localhost/10\tsuccess\t\\N\n" + } else { + "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t\\N\t$it\t0\tpayto://x-taler-bank/localhost/10\tpermanent_failure\tfailure\n" + } }, "taler_exchange_incoming(type, reserve_pub, account_pub, bank_transaction)" to { val hex = token32.rand().encodeHex() @@ -109,8 +117,8 @@ class Bench { @Test fun benchDb() { - val ITER = System.getenv("BENCH_ITER")?.toIntOrNull() ?: 0 - val AMOUNT = System.getenv("BENCH_AMOUNT")?.toIntOrNull() ?: 0 + val ITER = System.getenv("BENCH_ITER")?.toIntOrNull() ?: 1 + val AMOUNT = System.getenv("BENCH_AMOUNT")?.toIntOrNull() ?: 10 if (ITER == 0) { println("Skip benchmark, missing BENCH_ITER") @@ -278,7 +286,7 @@ class Bench { } // Wire gateway - measureAction("wg_transfer") { + val transfers = measureAction("wg_transfer") { client.postA("/accounts/exchange/taler-wire-gateway/transfer") { json { "request_uid" to HashCode.rand() @@ -287,14 +295,17 @@ class Bench { "wtid" to ShortHashCode.rand() "credit_account" to customerPayto.canonical } - }.assertOk() + }.assertOkJson<TransferResponse>().row_id } measureAction("wg_transfer_get") { - client.getA("/accounts/exchange/taler-wire-gateway/transfers/1").assertOk() + client.getA("/accounts/exchange/taler-wire-gateway/transfers/${transfers[it]}").assertOk() } measureAction("wg_transfer_page") { client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertOk() } + measureAction("wg_transfer_page_filter") { + client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOk() + } measureAction("wg_add") { client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { json { diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -207,14 +207,14 @@ suspend fun ApplicationTestBuilder.tx(from: String, amount: String, to: String, } /** Perform a taler outgoing transaction of [amount] from exchange to merchant */ -suspend fun ApplicationTestBuilder.transfer(amount: String) { +suspend fun ApplicationTestBuilder.transfer(amount: String, payto: IbanPayto = merchantPayto) { client.postA("/accounts/exchange/taler-wire-gateway/transfer") { json { "request_uid" to HashCode.rand() "amount" to TalerAmount(amount) "exchange_base_url" to "http://exchange.example.com/" "wtid" to ShortHashCode.rand() - "credit_account" to merchantPayto + "credit_account" to payto } }.assertOk() } diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt @@ -297,9 +297,13 @@ sealed class Payto { abstract val receiverName: String? /** Transform a payto URI to its bank form, using [name] as the receiver-name and the bank [ctx] */ - fun bank(name: String, ctx: BankPaytoCtx): String = when (this) { + fun bank(name: String?, ctx: BankPaytoCtx): String = when (this) { is IbanPayto -> IbanPayto.build(iban.toString(), ctx.bic, name) - is XTalerBankPayto -> "payto://x-taler-bank/${ctx.hostname ?: "localhost"}/$username?receiver-name=${name.encodeURLParameter()}" + is XTalerBankPayto -> { + val domain = ctx.hostname ?: "localhost" + val name = if (name != null) "?receiver-name=${name.encodeURLParameter()}" else "" + "payto://x-taler-bank/$domain/$username$name" + } } fun expectIban(): IbanPayto { diff --git a/common/src/main/kotlin/db/types.kt b/common/src/main/kotlin/db/types.kt @@ -55,8 +55,10 @@ fun ResultSet.getTalerTimestamp(name: String): TalerProtocolTimestamp{ return TalerProtocolTimestamp(getLong(name).asInstant()) } -fun ResultSet.getBankPayto(payto: String, name: String, ctx: BankPaytoCtx): String { - return Payto.parse(getString(payto)).bank(getString(name), ctx) +fun ResultSet.getBankPayto(payto: String, name: String?, ctx: BankPaytoCtx): String { + return Payto.parse(getString(payto)).bank( + name?.let { getString(it) } + , ctx) } fun ResultSet.getIbanPayto(payto: String): IbanPayto { diff --git a/database-versioning/libeufin-bank-0009.sql b/database-versioning/libeufin-bank-0009.sql @@ -0,0 +1,69 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + +BEGIN; + +SELECT _v.register_patch('libeufin-bank-0009', NULL, NULL); +SET search_path TO libeufin_bank; + +-- Add missing unique constraints +ALTER TABLE taler_exchange_outgoing ADD UNIQUE (exchange_outgoing_id); +ALTER TABLE taler_exchange_incoming ADD UNIQUE (exchange_incoming_id); +ALTER TABLE taler_withdrawal_operations ADD UNIQUE (withdrawal_id); + +CREATE TYPE transfer_status AS ENUM + ('permanent_failure' + ,'success' + ); + +CREATE TABLE transfer_operations + ( transfer_operation_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE + ,request_uid BYTEA UNIQUE NOT NULL CHECK (LENGTH(request_uid)=64) + ,wtid BYTEA UNIQUE NOT NULL CHECK (LENGTH(wtid)=32) + ,amount taler_amount NOT NULL + ,exchange_base_url TEXT NOT NULL + ,transfer_date INT8 NOT NULL + ,exchange_outgoing_id INT8 UNIQUE REFERENCES taler_exchange_outgoing(exchange_outgoing_id) ON DELETE CASCADE + ,creditor_payto TEXT NOT NULL + ,status transfer_status NOT NULL + ,status_msg TEXT + ,exchange_id INT8 NOT NULL REFERENCES bank_accounts(bank_account_id) ON DELETE CASCADE + ,CONSTRAINT transfer_operations_polymorphism CHECK( + CASE status + WHEN 'success' THEN exchange_outgoing_id IS NOT NULL + ELSE exchange_outgoing_id IS NULL + END + ) + ); +COMMENT ON TABLE transfer_operations + IS 'Operation table for idempotent wire gateway transfers with status.'; + +-- Migrate data from taler_exchange_outgoing to transfer_operations +INSERT INTO transfer_operations(transfer_operation_id, request_uid, amount, wtid, exchange_base_url, transfer_date, exchange_outgoing_id, creditor_payto, status, status_msg, exchange_id) + SELECT bank_transaction_id, request_uid, amount, wtid, exchange_base_url, transaction_date, exchange_outgoing_id, creditor_payto, 'success'::transfer_status, NULL, bank_account_id + FROM taler_exchange_outgoing JOIN bank_account_transactions ON bank_transaction = bank_transaction_id; + +CREATE INDEX transfer_operations_status_index ON transfer_operations (status); +COMMENT ON INDEX transfer_operations_status_index IS 'for listing taler transfers by status'; + +CREATE INDEX transfer_operations_account_index ON transfer_operations (exchange_id); +COMMENT ON INDEX transfer_operations_account_index IS 'for listing taler transfers by account'; + +-- Remove unused columns +ALTER TABLE taler_exchange_outgoing + DROP COLUMN request_uid, + DROP COLUMN creditor_account_id; + +COMMIT; diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -531,14 +531,14 @@ UPDATE customers SET deleted_at = in_timestamp WHERE customer_id = my_customer_i END $$; COMMENT ON FUNCTION account_delete IS 'Deletes an account if the balance is zero'; -CREATE PROCEDURE register_outgoing( - IN in_request_uid BYTEA, +CREATE FUNCTION register_outgoing( IN in_wtid BYTEA, IN in_exchange_base_url TEXT, IN in_debtor_account_id INT8, IN in_creditor_account_id INT8, IN in_debit_row_id INT8, - IN in_credit_row_id INT8 + IN in_credit_row_id INT8, + OUT in_tx_row_id INT8 ) LANGUAGE plpgsql AS $$ DECLARE @@ -548,18 +548,14 @@ BEGIN -- register outgoing transaction INSERT INTO taler_exchange_outgoing ( - request_uid, wtid, exchange_base_url, - bank_transaction, - creditor_account_id + bank_transaction ) VALUES ( - in_request_uid, in_wtid, in_exchange_base_url, - in_debit_row_id, - in_creditor_account_id -); + in_debit_row_id +) RETURNING exchange_outgoing_id INTO in_tx_row_id; -- TODO check if not drain -- update stats SELECT (amount).val, (amount).frac, bank_account_id @@ -569,7 +565,7 @@ CALL stats_register_payment('taler_out', NULL, local_amount, null); -- notify new transaction PERFORM pg_notify('bank_outgoing_tx', in_debtor_account_id || ' ' || in_creditor_account_id || ' ' || in_debit_row_id || ' ' || in_credit_row_id); END $$; -COMMENT ON PROCEDURE register_outgoing +COMMENT ON FUNCTION register_outgoing IS 'Register a bank transaction as a taler outgoing transaction and announce it'; CREATE PROCEDURE register_incoming( @@ -623,7 +619,6 @@ CREATE FUNCTION taler_transfer( -- Error status OUT out_debtor_not_found BOOLEAN, OUT out_debtor_not_exchange BOOLEAN, - OUT out_creditor_not_found BOOLEAN, OUT out_both_exchanges BOOLEAN, OUT out_request_uid_reuse BOOLEAN, OUT out_exchange_balance_insufficient BOOLEAN, @@ -633,44 +628,71 @@ CREATE FUNCTION taler_transfer( ) LANGUAGE plpgsql AS $$ DECLARE -exchange_bank_account_id INT8; -receiver_bank_account_id INT8; +exchange_account_id INT8; +creditor_account_id INT8; +creditor_name TEXT; credit_row_id INT8; +debit_row_id INT8; +outgoing_id INT8; BEGIN -- Check for idempotence and conflict SELECT (amount != in_amount OR creditor_payto != in_credit_account_payto OR exchange_base_url != in_exchange_base_url OR wtid != in_wtid) - ,bank_transaction_id, transaction_date + ,transfer_operation_id, transfer_date INTO out_request_uid_reuse, out_tx_row_id, out_timestamp - FROM taler_exchange_outgoing - JOIN bank_account_transactions AS txs - ON bank_transaction=txs.bank_transaction_id + FROM transfer_operations WHERE request_uid = in_request_uid; IF found THEN RETURN; END IF; +out_timestamp=in_timestamp; -- Find exchange bank account id SELECT bank_account_id, NOT is_taler_exchange - INTO exchange_bank_account_id, out_debtor_not_exchange + INTO exchange_account_id, out_debtor_not_exchange FROM bank_accounts JOIN customers ON customer_id=owning_customer_id WHERE username = in_username AND deleted_at IS NULL; -IF NOT FOUND OR out_debtor_not_exchange THEN - out_debtor_not_found=NOT FOUND; +out_debtor_not_found=NOT FOUND; +IF out_debtor_not_found OR out_debtor_not_exchange THEN RETURN; END IF; --- Find receiver bank account id +-- Find creditor bank account id SELECT bank_account_id, is_taler_exchange - INTO receiver_bank_account_id, out_both_exchanges + INTO creditor_account_id, out_both_exchanges FROM bank_accounts WHERE internal_payto = in_credit_account_payto; -IF NOT FOUND OR out_both_exchanges THEN - out_creditor_not_found=NOT FOUND; +IF NOT FOUND THEN + -- Register failure + INSERT INTO transfer_operations ( + request_uid, + wtid, + amount, + exchange_base_url, + transfer_date, + exchange_outgoing_id, + creditor_payto, + status, + status_msg, + exchange_id + ) VALUES ( + in_request_uid, + in_wtid, + in_amount, + in_exchange_base_url, + in_timestamp, + NULL, + in_credit_account_payto, + 'permanent_failure', + 'Unknown account', + exchange_account_id + ) RETURNING transfer_operation_id INTO out_tx_row_id; + RETURN; +ELSIF out_both_exchanges THEN RETURN; END IF; -- Perform bank transfer @@ -679,23 +701,47 @@ SELECT out_debit_row_id, out_credit_row_id INTO out_exchange_balance_insufficient, - out_tx_row_id, credit_row_id + debit_row_id, credit_row_id FROM bank_wire_transfer( - receiver_bank_account_id, - exchange_bank_account_id, + creditor_account_id, + exchange_account_id, in_subject, in_amount, in_timestamp, NULL, NULL, NULL - ) as transfer; + ); IF out_exchange_balance_insufficient THEN RETURN; END IF; -out_timestamp=in_timestamp; -- Register outgoing transaction -CALL register_outgoing(in_request_uid, in_wtid, in_exchange_base_url, exchange_bank_account_id, receiver_bank_account_id, out_tx_row_id, credit_row_id); +SELECT in_tx_row_id INTO outgoing_id + FROM register_outgoing(in_wtid, in_exchange_base_url, exchange_account_id, creditor_account_id, debit_row_id, credit_row_id); +-- Register success +INSERT INTO transfer_operations ( + request_uid, + wtid, + amount, + exchange_base_url, + transfer_date, + exchange_outgoing_id, + creditor_payto, + status, + status_msg, + exchange_id +) VALUES ( + in_request_uid, + in_wtid, + in_amount, + in_exchange_base_url, + in_timestamp, + outgoing_id, + in_credit_account_payto, + 'success', + NULL, + exchange_account_id +) RETURNING transfer_operation_id INTO out_tx_row_id; END $$; COMMENT ON FUNCTION taler_transfer IS 'Create an outgoing taler transaction and register it'; diff --git a/testbench/src/test/kotlin/MigrationTest.kt b/testbench/src/test/kotlin/MigrationTest.kt @@ -74,6 +74,9 @@ class MigrationTest { // libeufin-bank-0008 conn.execSQLUpdate(Path("../database-versioning/libeufin-bank-0008.sql").readText()) + // libeufin-bank-0009 + conn.execSQLUpdate(Path("../database-versioning/libeufin-bank-0009.sql").readText()) + // libeufin-nexus-0001 conn.execSQLUpdate(Path("../database-versioning/libeufin-nexus-0001.sql").readText()) conn.execSQLUpdate("""