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:
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("""