libeufin

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

libeufin-bank-procedures.sql (72159B)


      1 --
      2 -- This file is part of TALER
      3 -- Copyright (C) 2023, 2024, 2025, 2026 Taler Systems SA
      4 --
      5 -- TALER is free software; you can redistribute it and/or modify it under the
      6 -- terms of the GNU General Public License as published by the Free Software
      7 -- Foundation; either version 3, or (at your option) any later version.
      8 --
      9 -- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10 -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11 -- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 --
     13 -- You should have received a copy of the GNU General Public License along with
     14 -- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 
     16 BEGIN;
     17 SET search_path TO libeufin_bank;
     18 
     19 -- Remove all existing functions
     20 DO
     21 $do$
     22 DECLARE
     23   _sql text;
     24 BEGIN
     25   SELECT INTO _sql
     26         string_agg(format('DROP %s %s CASCADE;'
     27                         , CASE prokind
     28                             WHEN 'f' THEN 'FUNCTION'
     29                             WHEN 'p' THEN 'PROCEDURE'
     30                           END
     31                         , oid::regprocedure)
     32                   , E'\n')
     33   FROM   pg_proc
     34   WHERE  pronamespace = 'libeufin_bank'::regnamespace;
     35 
     36   IF _sql IS NOT NULL THEN
     37     EXECUTE _sql;
     38   END IF;
     39 END
     40 $do$;
     41 
     42 CREATE FUNCTION url_encode(input TEXT)
     43 RETURNS TEXT
     44 LANGUAGE plpgsql IMMUTABLE AS $$
     45 DECLARE
     46     result TEXT := '';
     47     char TEXT;
     48 BEGIN
     49     FOR i IN 1..length(input) LOOP
     50         char := substring(input FROM i FOR 1);
     51         IF char ~ '[A-Za-z0-9\-._~]' THEN
     52             result := result || char;
     53         ELSE
     54             result := result || '%' || lpad(upper(to_hex(ascii(char))), 2, '0');
     55         END IF;
     56     END LOOP;
     57     RETURN result;
     58 END;
     59 $$;
     60 
     61 CREATE OR REPLACE FUNCTION sort_uniq(anyarray)
     62 RETURNS anyarray LANGUAGE SQL IMMUTABLE AS $$
     63   SELECT COALESCE(array_agg(DISTINCT x ORDER BY x), $1[0:0])
     64   FROM unnest($1) AS t(x);
     65 $$;
     66 
     67 CREATE FUNCTION amount_normalize(
     68     IN amount taler_amount
     69   ,OUT normalized taler_amount
     70 )
     71 LANGUAGE plpgsql IMMUTABLE AS $$
     72 BEGIN
     73   normalized.val = amount.val + amount.frac / 100000000;
     74   IF (normalized.val > 1::INT8<<52) THEN
     75     RAISE EXCEPTION 'amount value overflowed';
     76   END IF;
     77   normalized.frac = amount.frac % 100000000;
     78 
     79 END $$;
     80 COMMENT ON FUNCTION amount_normalize
     81   IS 'Returns the normalized amount by adding to the .val the value of (.frac / 100000000) and removing the modulus 100000000 from .frac.'
     82       'It raises an exception when the resulting .val is larger than 2^52';
     83 
     84 CREATE FUNCTION amount_add(
     85    IN l taler_amount
     86   ,IN r taler_amount
     87   ,OUT sum taler_amount
     88 )
     89 LANGUAGE plpgsql IMMUTABLE AS $$
     90 BEGIN
     91   sum = (l.val + r.val, l.frac + r.frac);
     92   SELECT normalized.val, normalized.frac INTO sum.val, sum.frac FROM amount_normalize(sum) as normalized;
     93 END $$;
     94 COMMENT ON FUNCTION amount_add
     95   IS 'Returns the normalized sum of two amounts. It raises an exception when the resulting .val is larger than 2^52';
     96 
     97 CREATE FUNCTION amount_left_minus_right(
     98   IN l taler_amount
     99  ,IN r taler_amount
    100  ,OUT diff taler_amount
    101  ,OUT ok BOOLEAN
    102 )
    103 LANGUAGE plpgsql IMMUTABLE AS $$
    104 BEGIN
    105 diff = l;
    106 IF diff.frac < r.frac THEN
    107   IF diff.val <= 0 THEN
    108     diff = (-1, -1);
    109     ok = FALSE;
    110     RETURN;
    111   END IF;
    112   diff.frac = diff.frac + 100000000;
    113   diff.val = diff.val - 1;
    114 END IF;
    115 IF diff.val < r.val THEN
    116   diff = (-1, -1);
    117   ok = FALSE;
    118   RETURN;
    119 END IF;
    120 diff.val = diff.val - r.val;
    121 diff.frac = diff.frac - r.frac;
    122 ok = TRUE;
    123 END $$;
    124 COMMENT ON FUNCTION amount_left_minus_right
    125   IS 'Subtracts the right amount from the left and returns the difference and TRUE, if the left amount is larger than the right, or an invalid amount and FALSE otherwise.';
    126 
    127 CREATE FUNCTION account_balance_is_sufficient(
    128   IN in_account_id INT8,
    129   IN in_amount taler_amount,
    130   IN in_wire_transfer_fees taler_amount,
    131   IN in_min_amount taler_amount,
    132   IN in_max_amount taler_amount,
    133   OUT out_balance_insufficient BOOLEAN,
    134   OUT out_bad_amount BOOLEAN
    135 )
    136 LANGUAGE plpgsql STABLE AS $$ 
    137 DECLARE
    138 account_has_debt BOOLEAN;
    139 account_balance taler_amount;
    140 account_max_debt taler_amount;
    141 amount_with_fee taler_amount;
    142 BEGIN
    143 
    144 -- Check min and max
    145 SELECT (SELECT in_min_amount IS NOT NULL AND NOT ok FROM amount_left_minus_right(in_amount, in_min_amount)) OR
    146   (SELECT in_max_amount IS NOT NULL AND NOT ok FROM amount_left_minus_right(in_max_amount, in_amount))
    147   INTO out_bad_amount;
    148 IF out_bad_amount THEN
    149   RETURN;
    150 END IF;
    151 
    152 -- Add fees to the amount
    153 IF in_wire_transfer_fees IS NOT NULL AND in_wire_transfer_fees != (0, 0)::taler_amount THEN
    154   SELECT sum.val, sum.frac 
    155     INTO amount_with_fee.val, amount_with_fee.frac 
    156     FROM amount_add(in_amount, in_wire_transfer_fees) as sum;
    157 ELSE
    158   amount_with_fee = in_amount;
    159 END IF;
    160 
    161 -- Get account info, we expect the account to exist
    162 SELECT
    163   has_debt,
    164   (balance).val, (balance).frac,
    165   (max_debt).val, (max_debt).frac
    166   INTO
    167     account_has_debt,
    168     account_balance.val, account_balance.frac,
    169     account_max_debt.val, account_max_debt.frac
    170   FROM bank_accounts WHERE bank_account_id=in_account_id;
    171 
    172 -- Check enough funds
    173 IF account_has_debt THEN 
    174   -- debt case: simply checking against the max debt allowed.
    175   SELECT sum.val, sum.frac 
    176     INTO account_balance.val, account_balance.frac 
    177     FROM amount_add(account_balance, amount_with_fee) as sum;
    178   SELECT NOT ok
    179     INTO out_balance_insufficient
    180     FROM amount_left_minus_right(account_max_debt, account_balance);
    181   IF out_balance_insufficient THEN
    182     RETURN;
    183   END IF;
    184 ELSE -- not a debt account
    185   SELECT NOT ok
    186     INTO out_balance_insufficient
    187     FROM amount_left_minus_right(account_balance, amount_with_fee);
    188   IF out_balance_insufficient THEN
    189      -- debtor will switch to debt: determine their new negative balance.
    190     SELECT
    191       (diff).val, (diff).frac
    192       INTO
    193         account_balance.val, account_balance.frac
    194       FROM amount_left_minus_right(amount_with_fee, account_balance);
    195     SELECT NOT ok
    196       INTO out_balance_insufficient
    197       FROM amount_left_minus_right(account_max_debt, account_balance);
    198     IF out_balance_insufficient THEN
    199       RETURN;
    200     END IF;
    201   END IF;
    202 END IF;
    203 END $$;
    204 COMMENT ON FUNCTION account_balance_is_sufficient IS 'Check if an account have enough fund to transfer an amount.';
    205 
    206 CREATE FUNCTION account_max_amount(
    207   IN in_account_id INT8,
    208   IN in_max_amount taler_amount,
    209   OUT out_max_amount taler_amount
    210 )
    211 LANGUAGE plpgsql STABLE AS $$
    212 BEGIN
    213 -- add balance and max_debt
    214 WITH computed AS (
    215   SELECT CASE has_debt
    216     WHEN false THEN amount_add(balance, max_debt)
    217     ELSE (SELECT diff FROM amount_left_minus_right(max_debt, balance))
    218   END AS amount 
    219   FROM bank_accounts WHERE bank_account_id=in_account_id
    220 ) SELECT (amount).val, (amount).frac
    221   INTO out_max_amount.val, out_max_amount.frac
    222   FROM computed;
    223 
    224 IF in_max_amount.val < out_max_amount.val 
    225   OR (in_max_amount.val = out_max_amount.val AND in_max_amount.frac < out_max_amount.frac) THEN
    226   out_max_amount = in_max_amount;
    227 END IF;
    228 END $$;
    229 
    230 CREATE FUNCTION create_token(
    231   IN in_username TEXT,
    232   IN in_content BYTEA,
    233   IN in_creation_time INT8,
    234   IN in_expiration_time INT8,
    235   IN in_scope token_scope_enum,
    236   IN in_refreshable BOOLEAN,
    237   IN in_description TEXT,
    238   IN in_is_tan BOOLEAN,
    239   OUT out_tan_required BOOLEAN
    240 )
    241 LANGUAGE plpgsql AS $$
    242 DECLARE
    243 local_customer_id INT8;
    244 BEGIN
    245 -- Get account id and check if 2FA is required
    246 SELECT customer_id, NOT in_is_tan AND cardinality(tan_channels) > 0
    247 INTO local_customer_id, out_tan_required
    248 FROM customers JOIN bank_accounts ON owning_customer_id = customer_id
    249 WHERE username = in_username AND deleted_at IS NULL;
    250 IF out_tan_required THEN
    251   RETURN;
    252 END IF;
    253 INSERT INTO bearer_tokens (
    254   content,
    255   creation_time,
    256   expiration_time,
    257   scope,
    258   bank_customer,
    259   is_refreshable,
    260   description,
    261   last_access
    262 ) VALUES (
    263   in_content,
    264   in_creation_time,
    265   in_expiration_time,
    266   in_scope,
    267   local_customer_id,
    268   in_refreshable,
    269   in_description,
    270   in_creation_time
    271 );
    272 END $$;
    273 
    274 CREATE FUNCTION bank_wire_transfer(
    275   IN in_creditor_account_id INT8,
    276   IN in_debtor_account_id INT8,
    277   IN in_subject TEXT,
    278   IN in_amount taler_amount,
    279   IN in_timestamp INT8,
    280   IN in_wire_transfer_fees taler_amount,
    281   IN in_min_amount taler_amount,
    282   IN in_max_amount taler_amount,
    283   -- Error status
    284   OUT out_balance_insufficient BOOLEAN,
    285   OUT out_bad_amount BOOLEAN,
    286   -- Success return
    287   OUT out_credit_row_id INT8,
    288   OUT out_debit_row_id INT8
    289 )
    290 LANGUAGE plpgsql AS $$
    291 DECLARE
    292 has_fee BOOLEAN;
    293 amount_with_fee taler_amount;
    294 admin_account_id INT8;
    295 admin_has_debt BOOLEAN;
    296 admin_balance taler_amount;
    297 admin_payto TEXT;
    298 admin_name TEXT;
    299 debtor_has_debt BOOLEAN;
    300 debtor_balance taler_amount;
    301 debtor_max_debt taler_amount;
    302 debtor_payto TEXT;
    303 debtor_name TEXT;
    304 creditor_has_debt BOOLEAN;
    305 creditor_balance taler_amount;
    306 creditor_payto TEXT;
    307 creditor_name TEXT;
    308 tmp_balance taler_amount;
    309 BEGIN
    310 -- Check min and max
    311 SELECT (SELECT in_min_amount IS NOT NULL AND NOT ok FROM amount_left_minus_right(in_amount, in_min_amount)) OR
    312   (SELECT in_max_amount IS NOT NULL AND NOT ok FROM amount_left_minus_right(in_max_amount, in_amount))
    313   INTO out_bad_amount;
    314 IF out_bad_amount THEN
    315   RETURN;
    316 END IF;
    317 
    318 has_fee = in_wire_transfer_fees IS NOT NULL AND in_wire_transfer_fees != (0, 0)::taler_amount;
    319 IF has_fee THEN
    320   -- Retrieve admin info
    321   SELECT
    322     bank_account_id, has_debt,
    323     (balance).val, (balance).frac,
    324     internal_payto, customers.name
    325     INTO 
    326       admin_account_id, admin_has_debt,
    327       admin_balance.val, admin_balance.frac,
    328       admin_payto, admin_name
    329     FROM bank_accounts
    330       JOIN customers ON customer_id=owning_customer_id
    331     WHERE username = 'admin';
    332   IF NOT FOUND THEN
    333     RAISE EXCEPTION 'No admin';
    334   END IF;
    335 END IF;
    336 
    337 -- Retrieve debtor info
    338 SELECT
    339   has_debt,
    340   (balance).val, (balance).frac,
    341   (max_debt).val, (max_debt).frac,
    342   internal_payto, customers.name
    343   INTO
    344     debtor_has_debt,
    345     debtor_balance.val, debtor_balance.frac,
    346     debtor_max_debt.val, debtor_max_debt.frac,
    347     debtor_payto, debtor_name
    348   FROM bank_accounts
    349     JOIN customers ON customer_id=owning_customer_id
    350   WHERE bank_account_id=in_debtor_account_id;
    351 IF NOT FOUND THEN
    352   RAISE EXCEPTION 'Unknown debtor %', in_debtor_account_id;
    353 END IF;
    354 -- Retrieve creditor info
    355 SELECT
    356   has_debt,
    357   (balance).val, (balance).frac,
    358   internal_payto, customers.name
    359   INTO
    360     creditor_has_debt,
    361     creditor_balance.val, creditor_balance.frac,
    362     creditor_payto, creditor_name
    363   FROM bank_accounts
    364     JOIN customers ON customer_id=owning_customer_id
    365   WHERE bank_account_id=in_creditor_account_id;
    366 IF NOT FOUND THEN
    367   RAISE EXCEPTION 'Unknown creditor %', in_creditor_account_id;
    368 END IF;
    369 
    370 -- Add fees to the amount
    371 IF has_fee AND admin_account_id != in_debtor_account_id THEN
    372   SELECT sum.val, sum.frac 
    373     INTO amount_with_fee.val, amount_with_fee.frac 
    374     FROM amount_add(in_amount, in_wire_transfer_fees) as sum;
    375 ELSE
    376   has_fee=false;
    377   amount_with_fee = in_amount;
    378 END IF;
    379 
    380 -- DEBTOR SIDE
    381 -- check debtor has enough funds.
    382 IF debtor_has_debt THEN 
    383   -- debt case: simply checking against the max debt allowed.
    384   SELECT sum.val, sum.frac 
    385     INTO debtor_balance.val, debtor_balance.frac 
    386     FROM amount_add(debtor_balance, amount_with_fee) as sum;
    387   SELECT NOT ok
    388     INTO out_balance_insufficient
    389     FROM amount_left_minus_right(debtor_max_debt,
    390                                  debtor_balance);
    391   IF out_balance_insufficient THEN
    392     RETURN;
    393   END IF;
    394 ELSE -- not a debt account
    395   SELECT
    396     NOT ok,
    397     (diff).val, (diff).frac
    398     INTO
    399       out_balance_insufficient,
    400       tmp_balance.val,
    401       tmp_balance.frac
    402     FROM amount_left_minus_right(debtor_balance,
    403                                  amount_with_fee);
    404   IF NOT out_balance_insufficient THEN -- debtor has enough funds in the (positive) balance.
    405     debtor_balance=tmp_balance;
    406   ELSE -- debtor will switch to debt: determine their new negative balance.
    407     SELECT
    408       (diff).val, (diff).frac
    409       INTO
    410         debtor_balance.val, debtor_balance.frac
    411       FROM amount_left_minus_right(amount_with_fee,
    412                                    debtor_balance);
    413     debtor_has_debt=TRUE;
    414     SELECT NOT ok
    415       INTO out_balance_insufficient
    416       FROM amount_left_minus_right(debtor_max_debt,
    417                                    debtor_balance);
    418     IF out_balance_insufficient THEN
    419       RETURN;
    420     END IF;
    421   END IF;
    422 END IF;
    423 
    424 -- CREDITOR SIDE.
    425 -- Here we figure out whether the creditor would switch
    426 -- from debit to a credit situation, and adjust the balance
    427 -- accordingly.
    428 IF NOT creditor_has_debt THEN -- easy case.
    429   SELECT sum.val, sum.frac 
    430     INTO creditor_balance.val, creditor_balance.frac 
    431     FROM amount_add(creditor_balance, in_amount) as sum;
    432 ELSE -- creditor had debit but MIGHT switch to credit.
    433   SELECT
    434     (diff).val, (diff).frac,
    435     NOT ok
    436     INTO
    437       tmp_balance.val, tmp_balance.frac,
    438       creditor_has_debt
    439     FROM amount_left_minus_right(in_amount,
    440                                  creditor_balance);
    441   IF NOT creditor_has_debt THEN
    442     creditor_balance=tmp_balance;
    443   ELSE
    444     -- the amount is not enough to bring the receiver
    445     -- to a credit state, switch operators to calculate the new balance.
    446     SELECT
    447       (diff).val, (diff).frac
    448       INTO creditor_balance.val, creditor_balance.frac
    449       FROM amount_left_minus_right(creditor_balance,
    450 	                           in_amount);
    451   END IF;
    452 END IF;
    453 
    454 -- ADMIN SIDE.
    455 -- Here we figure out whether the administrator would switch
    456 -- from debit to a credit situation, and adjust the balance
    457 -- accordingly.
    458 IF has_fee THEN
    459   IF NOT admin_has_debt THEN -- easy case.
    460     SELECT sum.val, sum.frac 
    461       INTO admin_balance.val, admin_balance.frac 
    462       FROM amount_add(admin_balance, in_wire_transfer_fees) as sum;
    463   ELSE -- creditor had debit but MIGHT switch to credit.
    464     SELECT (diff).val, (diff).frac, NOT ok
    465       INTO
    466         tmp_balance.val, tmp_balance.frac,
    467         admin_has_debt
    468       FROM amount_left_minus_right(in_wire_transfer_fees, admin_balance);
    469     IF NOT admin_has_debt THEN
    470       admin_balance=tmp_balance;
    471     ELSE
    472       -- the amount is not enough to bring the receiver
    473       -- to a credit state, switch operators to calculate the new balance.
    474       SELECT (diff).val, (diff).frac
    475         INTO admin_balance.val, admin_balance.frac
    476         FROM amount_left_minus_right(admin_balance, in_wire_transfer_fees);
    477     END IF;
    478   END IF;
    479 END IF;
    480 
    481 -- Lock account in order to prevent deadlocks
    482 PERFORM FROM bank_accounts
    483   WHERE bank_account_id IN (in_debtor_account_id, in_creditor_account_id, admin_account_id)
    484   ORDER BY bank_account_id
    485   FOR UPDATE;
    486 
    487 -- now actually create the bank transaction.
    488 -- debtor side:
    489 INSERT INTO bank_account_transactions (
    490   creditor_payto
    491   ,creditor_name
    492   ,debtor_payto
    493   ,debtor_name
    494   ,subject
    495   ,amount
    496   ,transaction_date
    497   ,direction
    498   ,bank_account_id
    499   )
    500 VALUES (
    501   creditor_payto,
    502   creditor_name,
    503   debtor_payto,
    504   debtor_name,
    505   in_subject,
    506   in_amount,
    507   in_timestamp,
    508   'debit',
    509   in_debtor_account_id
    510 ) RETURNING bank_transaction_id INTO out_debit_row_id;
    511 
    512 -- debtor side:
    513 INSERT INTO bank_account_transactions (
    514   creditor_payto
    515   ,creditor_name
    516   ,debtor_payto
    517   ,debtor_name
    518   ,subject
    519   ,amount
    520   ,transaction_date
    521   ,direction
    522   ,bank_account_id
    523   )
    524 VALUES (
    525   creditor_payto,
    526   creditor_name,
    527   debtor_payto,
    528   debtor_name,
    529   in_subject,
    530   in_amount,
    531   in_timestamp,
    532   'credit',
    533   in_creditor_account_id
    534 ) RETURNING bank_transaction_id INTO out_credit_row_id;
    535 
    536 -- checks and balances set up, now update bank accounts.
    537 UPDATE bank_accounts
    538 SET
    539   balance=debtor_balance,
    540   has_debt=debtor_has_debt
    541 WHERE bank_account_id=in_debtor_account_id;
    542 
    543 UPDATE bank_accounts
    544 SET
    545   balance=creditor_balance,
    546   has_debt=creditor_has_debt
    547 WHERE bank_account_id=in_creditor_account_id;
    548 
    549 -- Fee part
    550 IF has_fee THEN
    551   INSERT INTO bank_account_transactions (
    552     creditor_payto
    553     ,creditor_name
    554     ,debtor_payto
    555     ,debtor_name
    556     ,subject
    557     ,amount
    558     ,transaction_date
    559     ,direction
    560     ,bank_account_id
    561     )
    562   VALUES (
    563     admin_payto,
    564     admin_name,
    565     debtor_payto,
    566     debtor_name,
    567     'wire transfer fees for tx ' || out_debit_row_id,
    568     in_wire_transfer_fees,
    569     in_timestamp,
    570     'debit',
    571     in_debtor_account_id
    572   ), (
    573     admin_payto,
    574     admin_name,
    575     debtor_payto,
    576     debtor_name,
    577     'wire transfer fees for tx ' || out_debit_row_id,
    578     in_wire_transfer_fees,
    579     in_timestamp,
    580     'credit',
    581     admin_account_id
    582   );
    583 
    584   UPDATE bank_accounts
    585   SET
    586     balance=admin_balance,
    587     has_debt=admin_has_debt
    588   WHERE bank_account_id=admin_account_id;
    589 END IF;
    590 
    591 -- notify new transaction
    592 PERFORM pg_notify('bank_tx', in_debtor_account_id || ' ' || in_creditor_account_id || ' ' || out_debit_row_id || ' ' || out_credit_row_id);
    593 END $$;
    594 
    595 CREATE FUNCTION account_delete(
    596   IN in_username TEXT,
    597   IN in_timestamp INT8,
    598   IN in_is_tan BOOLEAN,
    599   OUT out_not_found BOOLEAN,
    600   OUT out_balance_not_zero BOOLEAN,
    601   OUT out_tan_required BOOLEAN
    602 )
    603 LANGUAGE plpgsql AS $$
    604 DECLARE
    605 my_customer_id INT8;
    606 BEGIN
    607 -- check if account exists, has zero balance and if 2FA is required
    608 SELECT 
    609    customer_id
    610   ,NOT in_is_tan AND cardinality(tan_channels) > 0
    611   ,(balance).val != 0 OR (balance).frac != 0
    612   INTO 
    613      my_customer_id
    614     ,out_tan_required
    615     ,out_balance_not_zero
    616   FROM customers 
    617     JOIN bank_accounts ON owning_customer_id = customer_id
    618   WHERE username = in_username AND deleted_at IS NULL;
    619 IF NOT FOUND OR out_balance_not_zero OR out_tan_required THEN
    620   out_not_found=NOT FOUND;
    621   RETURN;
    622 END IF;
    623 
    624 -- actual deletion
    625 UPDATE customers SET deleted_at = in_timestamp WHERE customer_id = my_customer_id;
    626 END $$;
    627 COMMENT ON FUNCTION account_delete IS 'Deletes an account if the balance is zero';
    628 
    629 CREATE FUNCTION register_incoming(
    630   IN in_tx_row_id INT8,
    631   IN in_type taler_incoming_type,
    632   IN in_metadata BYTEA,
    633   IN in_account_id INT8,
    634   IN in_authorization_pub BYTEA,
    635   IN in_authorization_sig BYTEA
    636 )
    637 RETURNS void
    638 LANGUAGE plpgsql AS $$
    639 DECLARE
    640 local_amount taler_amount;
    641 BEGIN
    642 -- Register incoming transaction
    643 INSERT INTO taler_exchange_incoming (
    644   metadata,
    645   bank_transaction,
    646   type,
    647   authorization_pub,
    648   authorization_sig
    649 ) VALUES (
    650   in_metadata,
    651   in_tx_row_id,
    652   in_type,
    653   in_authorization_pub,
    654   in_authorization_sig
    655 );
    656 -- Update stats
    657 IF in_type = 'reserve' THEN
    658   SELECT (amount).val, (amount).frac
    659     INTO local_amount.val, local_amount.frac
    660     FROM bank_account_transactions WHERE bank_transaction_id=in_tx_row_id;
    661   CALL stats_register_payment('taler_in', NULL, local_amount, null);
    662 END IF;
    663 -- Notify new incoming transaction
    664 PERFORM pg_notify('bank_incoming_tx', in_account_id || ' ' || in_tx_row_id);
    665 END $$;
    666 COMMENT ON FUNCTION register_incoming
    667   IS 'Register a bank transaction as a taler incoming transaction and announce it';
    668 
    669 CREATE FUNCTION bounce(
    670   IN in_debtor_account_id INT8,
    671   IN in_credit_transaction_id INT8,
    672   IN in_bounce_cause TEXT,
    673   IN in_timestamp INT8
    674 )
    675 RETURNS void
    676 LANGUAGE plpgsql AS $$
    677 DECLARE
    678 local_creditor_account_id INT8;
    679 local_amount taler_amount;
    680 BEGIN
    681 -- Load transaction info
    682 SELECT (amount).frac, (amount).val, bank_account_id
    683 INTO local_amount.frac, local_amount.val, local_creditor_account_id
    684 FROM bank_account_transactions
    685 WHERE bank_transaction_id=in_credit_transaction_id;
    686 
    687 -- No error can happens because an opposite transaction already took place in the same transaction
    688 PERFORM bank_wire_transfer(
    689   in_debtor_account_id,
    690   local_creditor_account_id,
    691   'Bounce ' || in_credit_transaction_id || ': ' || in_bounce_cause,
    692   local_amount,
    693   in_timestamp,
    694   NULL,
    695   NULL,
    696   NULL
    697 );
    698 
    699 -- Delete from pending if any
    700 DELETE FROM pending_recurrent_incoming_transactions WHERE bank_transaction_id = in_credit_transaction_id;
    701 END$$;
    702 
    703 CREATE FUNCTION make_incoming(
    704   IN in_creditor_account_id INT8,
    705   IN in_debtor_account_id INT8,
    706   IN in_subject TEXT,
    707   IN in_amount taler_amount,
    708   IN in_timestamp INT8,
    709   IN in_type taler_incoming_type,
    710   IN in_metadata BYTEA,
    711   IN in_wire_transfer_fees taler_amount,
    712   IN in_min_amount taler_amount,
    713   IN in_max_amount taler_amount,
    714   -- Error status
    715   OUT out_balance_insufficient BOOLEAN,
    716   OUT out_bad_amount BOOLEAN,
    717   OUT out_reserve_pub_reuse BOOLEAN,
    718   OUT out_mapping_reuse BOOLEAN,
    719   OUT out_unknown_mapping BOOLEAN,
    720   -- Success return
    721   OUT out_pending BOOLEAN,
    722   OUT out_credit_row_id INT8,
    723   OUT out_debit_row_id INT8
    724 )
    725 LANGUAGE plpgsql AS $$
    726 DECLARE
    727 local_withdrawal_uuid UUID;
    728 local_authorization_pub BYTEA;
    729 local_authorization_sig BYTEA;
    730 BEGIN
    731 out_pending=FALSE;
    732 
    733 -- Resolve mapping logic
    734 IF in_type = 'map' THEN
    735   SELECT prepared_transfers.type, account_pub, authorization_pub, authorization_sig, withdrawal_uuid,
    736       bank_transaction_id IS NOT NULL AND NOT recurrent,
    737       bank_transaction_id IS NOT NULL AND recurrent
    738     INTO in_type, in_metadata, local_authorization_pub, local_authorization_sig, local_withdrawal_uuid, out_mapping_reuse, out_pending
    739     FROM prepared_transfers
    740     LEFT JOIN taler_withdrawal_operations USING (withdrawal_id)
    741     WHERE authorization_pub = in_metadata;
    742   out_unknown_mapping = NOT FOUND;
    743   IF out_unknown_mapping OR out_mapping_reuse THEN
    744     RETURN;
    745   END IF;
    746 END IF;
    747 
    748 -- Check reserve pub reuse
    749 out_reserve_pub_reuse=in_type = 'reserve' AND NOT out_pending AND EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_metadata AND type = 'reserve');
    750 IF out_reserve_pub_reuse THEN
    751   RETURN;
    752 END IF;
    753 
    754 -- Perform bank wire transfer
    755 SELECT 
    756   transfer.out_balance_insufficient,
    757   transfer.out_bad_amount,
    758   transfer.out_credit_row_id,
    759   transfer.out_debit_row_id
    760   INTO 
    761     out_balance_insufficient,
    762     out_bad_amount,
    763     out_credit_row_id,
    764     out_debit_row_id
    765   FROM bank_wire_transfer(
    766     in_creditor_account_id,
    767     in_debtor_account_id,
    768     in_subject,
    769     in_amount,
    770     in_timestamp,
    771     in_wire_transfer_fees,
    772     in_min_amount,
    773     in_max_amount
    774   ) as transfer;
    775 IF out_balance_insufficient OR out_bad_amount THEN
    776   RETURN;
    777 END IF;
    778 
    779 IF out_pending THEN
    780   -- Delay talerable registration until mapping again
    781   INSERT INTO pending_recurrent_incoming_transactions (bank_transaction_id, debtor_account_id, authorization_pub)
    782     VALUES (out_credit_row_id, in_debtor_account_id, local_authorization_pub);
    783 ELSE
    784   UPDATE prepared_transfers
    785   SET bank_transaction_id = out_credit_row_id
    786   WHERE (
    787     bank_transaction_id IS NULL AND account_pub = in_metadata AND type='reserve'
    788   ) OR authorization_pub = local_authorization_pub;
    789   IF local_withdrawal_uuid IS NOT NULL THEN
    790     PERFORM abort_taler_withdrawal(local_withdrawal_uuid);
    791   END IF;
    792   PERFORM register_incoming(out_credit_row_id, in_type, in_metadata, in_creditor_account_id, local_authorization_pub, local_authorization_sig);
    793 END IF;
    794 END $$;
    795 
    796 
    797 CREATE FUNCTION taler_transfer(
    798   IN in_request_uid BYTEA,
    799   IN in_wtid BYTEA,
    800   IN in_subject TEXT,
    801   IN in_amount taler_amount,
    802   IN in_exchange_base_url TEXT,
    803   IN in_metadata TEXT,
    804   IN in_credit_account_payto TEXT,
    805   IN in_username TEXT,
    806   IN in_timestamp INT8,
    807   IN in_conversion BOOLEAN,
    808   -- Error status
    809   OUT out_debtor_not_found BOOLEAN,
    810   OUT out_debtor_not_exchange BOOLEAN,
    811   OUT out_both_exchanges BOOLEAN,
    812   OUT out_creditor_admin BOOLEAN,
    813   OUT out_request_uid_reuse BOOLEAN,
    814   OUT out_wtid_reuse BOOLEAN,
    815   OUT out_exchange_balance_insufficient BOOLEAN,
    816   -- Success return
    817   OUT out_tx_row_id INT8,
    818   OUT out_timestamp INT8
    819 )
    820 LANGUAGE plpgsql AS $$
    821 DECLARE
    822 exchange_account_id INT8;
    823 creditor_account_id INT8;
    824 account_conversion_rate_class_id INT8;
    825 creditor_name TEXT;
    826 creditor_admin BOOLEAN;
    827 credit_row_id INT8;
    828 debit_row_id INT8;
    829 outgoing_id INT8;
    830 bounce_tx INT8;
    831 bounce_amount taler_amount;
    832 BEGIN
    833 -- Check for idempotence and conflict
    834 SELECT (amount != in_amount 
    835           OR creditor_payto != in_credit_account_payto
    836           OR exchange_base_url != in_exchange_base_url
    837           OR metadata != in_metadata
    838           OR wtid != in_wtid)
    839         ,transfer_operation_id, transfer_date
    840   INTO out_request_uid_reuse, out_tx_row_id, out_timestamp
    841   FROM transfer_operations
    842   WHERE request_uid = in_request_uid;
    843 IF found THEN
    844   RETURN;
    845 END IF;
    846 out_wtid_reuse = EXISTS(SELECT FROM transfer_operations WHERE wtid = in_wtid);
    847 IF out_wtid_reuse THEN
    848   RETURN;
    849 END IF;
    850 out_timestamp=in_timestamp;
    851 -- Find exchange bank account id
    852 SELECT
    853   bank_account_id, NOT is_taler_exchange, conversion_rate_class_id
    854   INTO exchange_account_id, out_debtor_not_exchange, account_conversion_rate_class_id
    855   FROM bank_accounts 
    856       JOIN customers 
    857         ON customer_id=owning_customer_id
    858   WHERE username = in_username AND deleted_at IS NULL;
    859 out_debtor_not_found=NOT FOUND;
    860 IF out_debtor_not_found OR out_debtor_not_exchange THEN
    861   RETURN;
    862 END IF;
    863 -- Find creditor bank account id
    864 SELECT
    865   bank_account_id, is_taler_exchange, username = 'admin'
    866   INTO creditor_account_id, out_both_exchanges, creditor_admin
    867   FROM bank_accounts
    868   JOIN customers ON owning_customer_id=customer_id
    869   WHERE internal_payto = in_credit_account_payto;
    870 IF NOT FOUND THEN
    871   -- Register failure
    872   INSERT INTO transfer_operations (
    873     request_uid,
    874     wtid,
    875     amount,
    876     exchange_base_url,
    877     metadata,
    878     transfer_date,
    879     exchange_outgoing_id,
    880     creditor_payto,
    881     status,
    882     status_msg,
    883     exchange_id
    884   ) VALUES (
    885     in_request_uid,
    886     in_wtid,
    887     in_amount,
    888     in_exchange_base_url,
    889     in_metadata,
    890     in_timestamp,
    891     NULL,
    892     in_credit_account_payto,
    893     'permanent_failure',
    894     'Unknown account',
    895     exchange_account_id
    896   ) RETURNING transfer_operation_id INTO out_tx_row_id;
    897   RETURN;
    898 ELSIF out_both_exchanges THEN
    899   RETURN;
    900 END IF;
    901 
    902 IF creditor_admin THEN
    903   -- Check if this is a conversion bounce
    904   IF NOT in_conversion THEN
    905     out_creditor_admin=TRUE;
    906     RETURN;
    907   END IF;
    908   
    909   -- Find the bounced transaction
    910   SELECT (amount).val, (amount).frac, incoming_transaction_id
    911     INTO bounce_amount.val, bounce_amount.frac, bounce_tx
    912     FROM libeufin_nexus.incoming_transactions
    913     JOIN libeufin_nexus.talerable_incoming_transactions USING (incoming_transaction_id)
    914     WHERE metadata=in_wtid AND type='reserve';
    915   IF NOT FOUND THEN
    916     -- Register failure
    917     INSERT INTO transfer_operations (
    918       request_uid,
    919       wtid,
    920       amount,
    921       exchange_base_url,
    922       metadata,
    923       transfer_date,
    924       exchange_outgoing_id,
    925       creditor_payto,
    926       status,
    927       status_msg,
    928       exchange_id
    929     ) VALUES (
    930       in_request_uid,
    931       in_wtid,
    932       in_amount,
    933       in_exchange_base_url,
    934       in_metadata,
    935       in_timestamp,
    936       NULL,
    937       in_credit_account_payto,
    938       'permanent_failure',
    939       'Unknown bounced transaction',
    940       exchange_account_id
    941     ) RETURNING transfer_operation_id INTO out_tx_row_id;
    942     RETURN;
    943   END IF;
    944 
    945   -- Bounce the transaction
    946   PERFORM libeufin_nexus.bounce_incoming(
    947     bounce_tx
    948     ,((bounce_amount).val, (bounce_amount).frac)::libeufin_nexus.taler_amount
    949     ,libeufin_nexus.ebics_id_gen()
    950     ,in_timestamp
    951     ,'exchange bounced'
    952   );
    953 END IF;
    954 -- Perform bank transfer
    955 SELECT
    956   out_balance_insufficient,
    957   out_debit_row_id, out_credit_row_id
    958   INTO
    959     out_exchange_balance_insufficient,
    960     debit_row_id, credit_row_id
    961   FROM bank_wire_transfer(
    962     creditor_account_id,
    963     exchange_account_id,
    964     in_subject,
    965     in_amount,
    966     in_timestamp,
    967     NULL,
    968     NULL,
    969     NULL
    970   );
    971 IF out_exchange_balance_insufficient THEN
    972   RETURN;
    973 END IF;
    974 -- Register outgoing transaction
    975 INSERT INTO taler_exchange_outgoing (
    976   bank_transaction
    977 ) VALUES (
    978   debit_row_id
    979 ) RETURNING exchange_outgoing_id INTO outgoing_id;
    980 -- Update stats
    981 CALL stats_register_payment('taler_out', NULL, in_amount, null);
    982 -- Register success
    983 INSERT INTO transfer_operations (
    984   request_uid,
    985   wtid,
    986   amount,
    987   exchange_base_url,
    988   metadata,
    989   transfer_date,
    990   exchange_outgoing_id,
    991   creditor_payto,
    992   status,
    993   status_msg,
    994   exchange_id
    995 ) VALUES (
    996   in_request_uid,
    997   in_wtid,
    998   in_amount,
    999   in_exchange_base_url,
   1000   in_metadata,
   1001   in_timestamp,
   1002   outgoing_id,
   1003   in_credit_account_payto,
   1004   'success',
   1005   NULL,
   1006   exchange_account_id
   1007 ) RETURNING transfer_operation_id INTO out_tx_row_id;
   1008 
   1009 -- Notify new transaction
   1010 PERFORM pg_notify('bank_outgoing_tx', exchange_account_id || ' ' || creditor_account_id || ' ' || debit_row_id || ' ' || credit_row_id);
   1011 
   1012 IF creditor_admin THEN
   1013   -- Create cashout operation
   1014   INSERT INTO cashout_operations (
   1015     request_uid
   1016     ,amount_debit
   1017     ,amount_credit
   1018     ,creation_time
   1019     ,bank_account
   1020     ,subject
   1021     ,local_transaction
   1022   ) VALUES (
   1023     NULL
   1024     ,in_amount
   1025     ,bounce_amount
   1026     ,in_timestamp
   1027     ,exchange_account_id
   1028     ,in_subject
   1029     ,debit_row_id
   1030   );
   1031 
   1032   -- update stats
   1033   CALL stats_register_payment('cashout', NULL, in_amount, bounce_amount);
   1034 END IF;
   1035 END $$;
   1036 COMMENT ON FUNCTION taler_transfer IS 'Create an outgoing taler transaction and register it';
   1037 
   1038 CREATE FUNCTION taler_add_incoming(
   1039   IN in_key BYTEA,
   1040   IN in_subject TEXT,
   1041   IN in_amount taler_amount,
   1042   IN in_debit_account_payto TEXT,
   1043   IN in_username TEXT,
   1044   IN in_timestamp INT8,
   1045   IN in_type taler_incoming_type,
   1046   -- Error status
   1047   OUT out_creditor_not_found BOOLEAN,
   1048   OUT out_creditor_not_exchange BOOLEAN,
   1049   OUT out_debtor_not_found BOOLEAN,
   1050   OUT out_both_exchanges BOOLEAN,
   1051   OUT out_reserve_pub_reuse BOOLEAN,
   1052   OUT out_mapping_reuse BOOLEAN,
   1053   OUT out_unknown_mapping BOOLEAN,
   1054   OUT out_debitor_balance_insufficient BOOLEAN,
   1055   -- Success return
   1056   OUT out_tx_row_id INT8,
   1057   OUT out_pending INT8
   1058 )
   1059 LANGUAGE plpgsql AS $$
   1060 DECLARE
   1061 exchange_bank_account_id INT8;
   1062 sender_bank_account_id INT8;
   1063 BEGIN
   1064 -- Find exchange bank account id
   1065 SELECT
   1066   bank_account_id, NOT is_taler_exchange
   1067   INTO exchange_bank_account_id, out_creditor_not_exchange
   1068   FROM bank_accounts 
   1069       JOIN customers 
   1070         ON customer_id=owning_customer_id
   1071   WHERE username = in_username AND deleted_at IS NULL;
   1072 IF NOT FOUND OR out_creditor_not_exchange THEN
   1073   out_creditor_not_found=NOT FOUND;
   1074   RETURN;
   1075 END IF;
   1076 -- Find sender bank account id
   1077 SELECT
   1078   bank_account_id, is_taler_exchange
   1079   INTO sender_bank_account_id, out_both_exchanges
   1080   FROM bank_accounts
   1081   WHERE internal_payto = in_debit_account_payto;
   1082 IF NOT FOUND OR out_both_exchanges THEN
   1083   out_debtor_not_found=NOT FOUND;
   1084   RETURN;
   1085 END IF;
   1086 -- Perform bank transfer
   1087 SELECT
   1088   out_balance_insufficient,
   1089   out_credit_row_id,
   1090   t.out_reserve_pub_reuse,
   1091   t.out_mapping_reuse,
   1092   t.out_unknown_mapping
   1093   INTO
   1094     out_debitor_balance_insufficient,
   1095     out_tx_row_id,
   1096     out_reserve_pub_reuse,
   1097     out_mapping_reuse,
   1098     out_unknown_mapping
   1099   FROM make_incoming(
   1100     exchange_bank_account_id,
   1101     sender_bank_account_id,
   1102     in_subject,
   1103     in_amount,
   1104     in_timestamp,
   1105     in_type,
   1106     in_key,
   1107     NULL,
   1108     NULL,
   1109     NULL
   1110   ) as t;
   1111 END $$;
   1112 COMMENT ON FUNCTION taler_add_incoming IS 'Create an incoming taler transaction and register it';
   1113 
   1114 CREATE FUNCTION bank_transaction(
   1115   IN in_credit_account_payto TEXT,
   1116   IN in_debit_account_username TEXT,
   1117   IN in_subject TEXT,
   1118   IN in_amount taler_amount,
   1119   IN in_timestamp INT8,
   1120   IN in_is_tan BOOLEAN,
   1121   IN in_request_uid BYTEA,
   1122   IN in_wire_transfer_fees taler_amount,
   1123   IN in_min_amount taler_amount,
   1124   IN in_max_amount taler_amount,
   1125   IN in_type taler_incoming_type,
   1126   IN in_metadata BYTEA,
   1127   IN in_bounce_cause TEXT,
   1128   -- Error status
   1129   OUT out_creditor_not_found BOOLEAN,
   1130   OUT out_debtor_not_found BOOLEAN,
   1131   OUT out_same_account BOOLEAN,
   1132   OUT out_balance_insufficient BOOLEAN,
   1133   OUT out_creditor_admin BOOLEAN,
   1134   OUT out_tan_required BOOLEAN,
   1135   OUT out_request_uid_reuse BOOLEAN,
   1136   OUT out_bad_amount BOOLEAN,
   1137   -- Success return
   1138   OUT out_credit_bank_account_id INT8,
   1139   OUT out_debit_bank_account_id INT8,
   1140   OUT out_credit_row_id INT8,
   1141   OUT out_debit_row_id INT8,
   1142   OUT out_creditor_is_exchange BOOLEAN,
   1143   OUT out_debtor_is_exchange BOOLEAN,
   1144   OUT out_idempotent BOOLEAN
   1145 )
   1146 LANGUAGE plpgsql AS $$
   1147 DECLARE
   1148 local_reserve_pub_reuse BOOLEAN;
   1149 local_mapping_reuse BOOLEAN;
   1150 local_unknown_mapping BOOLEAN;
   1151 BEGIN
   1152 -- Find credit bank account id and check it's not admin
   1153 SELECT bank_account_id, is_taler_exchange, username='admin'
   1154   INTO out_credit_bank_account_id, out_creditor_is_exchange, out_creditor_admin
   1155   FROM bank_accounts
   1156     JOIN customers ON customer_id=owning_customer_id
   1157   WHERE internal_payto = in_credit_account_payto AND deleted_at IS NULL;
   1158 IF NOT FOUND OR out_creditor_admin THEN
   1159   out_creditor_not_found=NOT FOUND;
   1160   RETURN;
   1161 END IF;
   1162 -- Find debit bank account ID and check it's a different account and if 2FA is required
   1163 SELECT bank_account_id, is_taler_exchange, out_credit_bank_account_id=bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0
   1164   INTO out_debit_bank_account_id, out_debtor_is_exchange, out_same_account, out_tan_required
   1165   FROM bank_accounts 
   1166     JOIN customers ON customer_id=owning_customer_id
   1167   WHERE username = in_debit_account_username AND deleted_at IS NULL;
   1168 IF NOT FOUND OR out_same_account THEN
   1169   out_debtor_not_found=NOT FOUND;
   1170   RETURN;
   1171 END IF;
   1172 -- Check for idempotence and conflict
   1173 IF in_request_uid IS NOT NULL THEN
   1174   SELECT (amount != in_amount
   1175       OR subject != in_subject 
   1176       OR bank_account_id != out_debit_bank_account_id), bank_transaction
   1177     INTO out_request_uid_reuse, out_debit_row_id
   1178     FROM bank_transaction_operations
   1179       JOIN bank_account_transactions ON bank_transaction = bank_transaction_id
   1180     WHERE request_uid = in_request_uid;
   1181   IF found OR out_tan_required THEN
   1182     out_idempotent = found AND NOT out_request_uid_reuse;
   1183     RETURN;
   1184   END IF;
   1185 ELSIF out_tan_required THEN
   1186   RETURN;
   1187 END IF;
   1188 
   1189 -- Try to perform an incoming transfer
   1190 IF out_creditor_is_exchange AND NOT out_debtor_is_exchange AND in_bounce_cause IS NULL THEN
   1191   -- Perform an incoming transfer
   1192   SELECT
   1193     transfer.out_balance_insufficient,
   1194     transfer.out_bad_amount,
   1195     transfer.out_credit_row_id,
   1196     transfer.out_debit_row_id,
   1197     out_reserve_pub_reuse,
   1198     out_mapping_reuse,
   1199     out_unknown_mapping
   1200     INTO
   1201       out_balance_insufficient,
   1202       out_bad_amount,
   1203       out_credit_row_id,
   1204       out_debit_row_id,
   1205       local_reserve_pub_reuse,
   1206       local_mapping_reuse,
   1207       local_unknown_mapping
   1208     FROM make_incoming(
   1209       out_credit_bank_account_id,
   1210       out_debit_bank_account_id,
   1211       in_subject,
   1212       in_amount,
   1213       in_timestamp,
   1214       in_type,
   1215       in_metadata,
   1216       in_wire_transfer_fees,
   1217       in_min_amount,
   1218       in_max_amount
   1219     ) as transfer;
   1220   IF out_balance_insufficient OR out_bad_amount THEN
   1221     RETURN;
   1222   END IF; 
   1223   IF local_reserve_pub_reuse THEN
   1224     in_bounce_cause = 'reserve public key reuse';
   1225   ELSIF local_mapping_reuse THEN
   1226     in_bounce_cause = 'mapping public key reuse';
   1227   ELSIF local_unknown_mapping THEN
   1228     in_bounce_cause = 'unknown mapping public key';
   1229   END IF;
   1230 END IF;
   1231 
   1232 IF out_credit_row_id IS NULL THEN
   1233   -- Perform common bank transfer
   1234   SELECT
   1235     transfer.out_balance_insufficient,
   1236     transfer.out_bad_amount,
   1237     transfer.out_credit_row_id,
   1238     transfer.out_debit_row_id
   1239     INTO
   1240       out_balance_insufficient,
   1241       out_bad_amount,
   1242       out_credit_row_id,
   1243       out_debit_row_id
   1244     FROM bank_wire_transfer(
   1245       out_credit_bank_account_id,
   1246       out_debit_bank_account_id,
   1247       in_subject,
   1248       in_amount,
   1249       in_timestamp,
   1250       in_wire_transfer_fees,
   1251       in_min_amount,
   1252       in_max_amount
   1253     ) as transfer;
   1254   IF out_balance_insufficient OR out_bad_amount THEN
   1255     RETURN;
   1256   END IF; 
   1257 END IF;
   1258 
   1259 -- Bounce if necessary
   1260 IF out_creditor_is_exchange AND in_bounce_cause IS NOT NULL THEN
   1261   PERFORM bounce(out_debit_bank_account_id, out_credit_row_id, in_bounce_cause, in_timestamp);
   1262 END IF;
   1263 
   1264 -- Store operation
   1265 IF in_request_uid IS NOT NULL THEN
   1266   INSERT INTO bank_transaction_operations (request_uid, bank_transaction)  
   1267     VALUES (in_request_uid, out_debit_row_id);
   1268 END IF;
   1269 END $$;
   1270 COMMENT ON FUNCTION bank_transaction IS 'Create a bank transaction';
   1271 
   1272 CREATE FUNCTION create_taler_withdrawal(
   1273   IN in_account_username TEXT,
   1274   IN in_withdrawal_uuid UUID,
   1275   IN in_amount taler_amount,
   1276   IN in_suggested_amount taler_amount,
   1277   IN in_no_amount_to_wallet BOOLEAN,
   1278   IN in_timestamp INT8,
   1279   IN in_wire_transfer_fees taler_amount,
   1280   IN in_min_amount taler_amount,
   1281   IN in_max_amount taler_amount,
   1282    -- Error status
   1283   OUT out_account_not_found BOOLEAN,
   1284   OUT out_account_is_exchange BOOLEAN,
   1285   OUT out_balance_insufficient BOOLEAN,
   1286   OUT out_bad_amount BOOLEAN
   1287 )
   1288 LANGUAGE plpgsql AS $$ 
   1289 DECLARE
   1290 account_id INT8;
   1291 amount_with_fee taler_amount;
   1292 BEGIN
   1293 IF in_account_username IS NOT NULL THEN
   1294   -- Check account exists
   1295   SELECT bank_account_id, is_taler_exchange
   1296     INTO account_id, out_account_is_exchange
   1297     FROM bank_accounts
   1298     JOIN customers ON bank_accounts.owning_customer_id = customers.customer_id
   1299     WHERE username=in_account_username AND deleted_at IS NULL;
   1300   out_account_not_found=NOT FOUND;
   1301   IF out_account_not_found OR out_account_is_exchange THEN
   1302     RETURN;
   1303   END IF;
   1304 
   1305   -- Check enough funds
   1306   IF in_amount IS NOT NULL OR in_suggested_amount IS NOT NULL THEN
   1307     SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient(
   1308       account_id, 
   1309       COALESCE(in_amount, in_suggested_amount), 
   1310       in_wire_transfer_fees,
   1311       in_min_amount,
   1312       in_max_amount
   1313     ) AS test INTO out_balance_insufficient, out_bad_amount;
   1314     IF out_balance_insufficient OR out_bad_amount THEN
   1315       RETURN;
   1316     END IF;
   1317   END IF;
   1318 END IF;
   1319 
   1320 -- Create withdrawal operation
   1321 INSERT INTO taler_withdrawal_operations (
   1322   withdrawal_uuid,
   1323   wallet_bank_account,
   1324   amount,
   1325   suggested_amount,
   1326   no_amount_to_wallet,
   1327   type,
   1328   creation_date
   1329 ) VALUES (
   1330   in_withdrawal_uuid,
   1331   account_id,
   1332   in_amount,
   1333   in_suggested_amount,
   1334   in_no_amount_to_wallet,
   1335   'reserve',
   1336   in_timestamp
   1337 );
   1338 END $$;
   1339 COMMENT ON FUNCTION create_taler_withdrawal IS 'Create a new withdrawal operation';
   1340 
   1341 CREATE FUNCTION select_taler_withdrawal(
   1342   IN in_withdrawal_uuid uuid,
   1343   IN in_reserve_pub BYTEA,
   1344   IN in_subject TEXT,
   1345   IN in_selected_exchange_payto TEXT,
   1346   IN in_amount taler_amount,
   1347   IN in_wire_transfer_fees taler_amount,
   1348   IN in_min_amount taler_amount,
   1349   IN in_max_amount taler_amount,
   1350   -- Error status
   1351   OUT out_no_op BOOLEAN,
   1352   OUT out_already_selected BOOLEAN,
   1353   OUT out_reserve_pub_reuse BOOLEAN,
   1354   OUT out_account_not_found BOOLEAN,
   1355   OUT out_account_is_not_exchange BOOLEAN,
   1356   OUT out_amount_differs BOOLEAN,
   1357   OUT out_balance_insufficient BOOLEAN,
   1358   OUT out_bad_amount BOOLEAN,
   1359   OUT out_aborted BOOLEAN,
   1360   -- Success return
   1361   OUT out_status TEXT
   1362 )
   1363 LANGUAGE plpgsql AS $$ 
   1364 DECLARE
   1365 selected BOOLEAN;
   1366 account_id INT8;
   1367 exchange_account_id INT8;
   1368 amount_with_fee taler_amount;
   1369 BEGIN
   1370 -- Check exchange account
   1371 SELECT bank_account_id, NOT is_taler_exchange
   1372   INTO exchange_account_id, out_account_is_not_exchange
   1373   FROM bank_accounts
   1374   WHERE internal_payto=in_selected_exchange_payto;
   1375 out_account_not_found=NOT FOUND;
   1376 IF out_account_not_found OR out_account_is_not_exchange THEN
   1377   RETURN;
   1378 END IF;
   1379 
   1380 -- Check for conflict and idempotence
   1381 SELECT
   1382   selection_done, 
   1383   aborted,
   1384   CASE 
   1385     WHEN confirmation_done THEN 'confirmed'
   1386     ELSE 'selected'
   1387   END,
   1388   selection_done 
   1389     AND (exchange_bank_account != exchange_account_id OR reserve_pub != in_reserve_pub OR amount != in_amount),
   1390   amount != in_amount,
   1391   wallet_bank_account
   1392   INTO selected, out_aborted, out_status, out_already_selected, out_amount_differs, account_id
   1393   FROM taler_withdrawal_operations
   1394   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1395 out_no_op = NOT FOUND;
   1396 IF out_no_op OR out_aborted OR out_already_selected OR out_amount_differs OR selected THEN
   1397   RETURN;
   1398 END IF;
   1399 
   1400 -- Check reserve_pub reuse
   1401 out_reserve_pub_reuse=EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_reserve_pub AND type = 'reserve') OR
   1402   EXISTS(SELECT FROM taler_withdrawal_operations WHERE reserve_pub = in_reserve_pub AND type = 'reserve');
   1403 IF out_reserve_pub_reuse THEN
   1404   RETURN;
   1405 END IF;
   1406 
   1407 IF in_amount IS NOT NULL THEN
   1408   SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient(
   1409     account_id,
   1410     in_amount,
   1411     in_wire_transfer_fees,
   1412     in_min_amount,
   1413     in_max_amount
   1414   ) AS test INTO out_balance_insufficient, out_bad_amount;
   1415   IF out_balance_insufficient OR out_bad_amount THEN
   1416     RETURN;
   1417   END IF;
   1418 END IF;
   1419 
   1420 -- Update withdrawal operation
   1421 UPDATE taler_withdrawal_operations
   1422   SET exchange_bank_account=exchange_account_id,
   1423       reserve_pub=in_reserve_pub,
   1424       subject=in_subject,
   1425       selection_done=true,
   1426       amount=COALESCE(amount, in_amount)
   1427   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1428 
   1429 -- Notify status change
   1430 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' selected');
   1431 END $$;
   1432 COMMENT ON FUNCTION select_taler_withdrawal IS 'Set details of a withdrawal operation';
   1433 
   1434 CREATE FUNCTION abort_taler_withdrawal(
   1435   IN in_withdrawal_uuid uuid,
   1436   OUT out_no_op BOOLEAN,
   1437   OUT out_already_confirmed BOOLEAN
   1438 )
   1439 LANGUAGE plpgsql AS $$
   1440 BEGIN
   1441 UPDATE taler_withdrawal_operations
   1442   SET aborted = NOT confirmation_done
   1443   WHERE withdrawal_uuid=in_withdrawal_uuid
   1444   RETURNING confirmation_done
   1445   INTO out_already_confirmed;
   1446 IF NOT FOUND OR out_already_confirmed THEN
   1447   out_no_op=NOT FOUND;
   1448   RETURN;
   1449 END IF;
   1450 
   1451 -- Notify status change
   1452 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' aborted');
   1453 END $$;
   1454 COMMENT ON FUNCTION abort_taler_withdrawal IS 'Abort a withdrawal operation.';
   1455 
   1456 CREATE FUNCTION confirm_taler_withdrawal(
   1457   IN in_username TEXT,
   1458   IN in_withdrawal_uuid uuid,
   1459   IN in_timestamp INT8,
   1460   IN in_is_tan BOOLEAN,
   1461   IN in_wire_transfer_fees taler_amount,
   1462   IN in_min_amount taler_amount,
   1463   IN in_max_amount taler_amount,
   1464   IN in_amount taler_amount,
   1465   OUT out_no_op BOOLEAN,
   1466   OUT out_balance_insufficient BOOLEAN,
   1467   OUT out_reserve_pub_reuse BOOLEAN,
   1468   OUT out_bad_amount BOOLEAN,
   1469   OUT out_creditor_not_found BOOLEAN,
   1470   OUT out_not_selected BOOLEAN,
   1471   OUT out_missing_amount BOOLEAN,
   1472   OUT out_amount_differs BOOLEAN,
   1473   OUT out_aborted BOOLEAN,
   1474   OUT out_tan_required BOOLEAN
   1475 )
   1476 LANGUAGE plpgsql AS $$
   1477 DECLARE
   1478   already_confirmed BOOLEAN;
   1479   subject_local TEXT;
   1480   reserve_pub_local BYTEA;
   1481   wallet_bank_account_local INT8;
   1482   amount_local taler_amount;
   1483   exchange_bank_account_id INT8;
   1484   local_type taler_incoming_type;
   1485 BEGIN
   1486 -- Load account info
   1487 SELECT bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0
   1488 INTO wallet_bank_account_local, out_tan_required
   1489 FROM bank_accounts 
   1490 JOIN customers ON owning_customer_id=customer_id
   1491 WHERE username=in_username AND deleted_at IS NULL;
   1492 
   1493 -- Check op exists and conflict
   1494 SELECT
   1495   confirmation_done,
   1496   aborted, NOT selection_done,
   1497   reserve_pub, subject, type,
   1498   exchange_bank_account,
   1499   (amount).val, (amount).frac,
   1500   amount IS NULL AND in_amount IS NULL,
   1501   amount != in_amount
   1502   INTO
   1503     already_confirmed,
   1504     out_aborted, out_not_selected,
   1505     reserve_pub_local, subject_local, local_type,
   1506     exchange_bank_account_id,
   1507     amount_local.val, amount_local.frac,
   1508     out_missing_amount,
   1509     out_amount_differs
   1510   FROM taler_withdrawal_operations AS op
   1511   WHERE op.withdrawal_uuid=in_withdrawal_uuid
   1512     -- Prepared-transfer withdrawals are intentionally unbound until the
   1513     -- first confirmation; ordinary withdrawals are bound at creation.
   1514     AND (op.wallet_bank_account IS NULL
   1515          OR op.wallet_bank_account=wallet_bank_account_local);
   1516 out_no_op=NOT FOUND;
   1517 IF out_no_op OR already_confirmed OR out_aborted OR out_not_selected OR out_missing_amount OR out_amount_differs OR out_tan_required THEN
   1518   RETURN;
   1519 ELSIF in_amount IS NOT NULL THEN
   1520   amount_local = in_amount;
   1521 END IF;
   1522 
   1523 SELECT -- not checking for accounts existence, as it was done above.
   1524   transfer.out_balance_insufficient,
   1525   transfer.out_bad_amount,
   1526   transfer.out_reserve_pub_reuse
   1527   INTO out_balance_insufficient, out_bad_amount, out_reserve_pub_reuse
   1528 FROM make_incoming(
   1529   exchange_bank_account_id,
   1530   wallet_bank_account_local,
   1531   subject_local,
   1532   amount_local,
   1533   in_timestamp,
   1534   local_type,
   1535   reserve_pub_local,
   1536   in_wire_transfer_fees,
   1537   in_min_amount,
   1538   in_max_amount
   1539 ) as transfer;
   1540 IF out_balance_insufficient OR out_reserve_pub_reuse OR out_bad_amount THEN
   1541   RETURN;
   1542 END IF;
   1543 
   1544 -- Confirm operation and update amount
   1545 UPDATE taler_withdrawal_operations
   1546   SET amount=amount_local,
   1547       wallet_bank_account=COALESCE(wallet_bank_account, wallet_bank_account_local),
   1548       confirmation_done=true
   1549   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1550 
   1551 -- Notify status change
   1552 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' confirmed');
   1553 END $$;
   1554 COMMENT ON FUNCTION confirm_taler_withdrawal
   1555   IS 'Set a withdrawal operation as confirmed and wire the funds to the exchange.';
   1556 
   1557 CREATE FUNCTION cashin(
   1558   IN in_timestamp INT8,
   1559   IN in_reserve_pub BYTEA,
   1560   IN in_amount taler_amount,
   1561   IN in_subject TEXT,
   1562   -- Error status
   1563   OUT out_no_account BOOLEAN,
   1564   OUT out_too_small BOOLEAN,
   1565   OUT out_balance_insufficient BOOLEAN
   1566 )
   1567 LANGUAGE plpgsql AS $$ 
   1568 DECLARE
   1569   converted_amount taler_amount;
   1570   admin_account_id INT8;
   1571   exchange_account_id INT8;
   1572   exchange_conversion_rate_class_id INT8;
   1573   tx_row_id INT8;
   1574 BEGIN
   1575 -- TODO check reserve_pub reuse ?
   1576 
   1577 -- Recover exchange account info
   1578 SELECT bank_account_id, conversion_rate_class_id
   1579   INTO exchange_account_id, exchange_conversion_rate_class_id
   1580   FROM bank_accounts
   1581     JOIN customers 
   1582       ON customer_id=owning_customer_id
   1583   WHERE username = 'exchange';
   1584 IF NOT FOUND THEN
   1585   out_no_account = true;
   1586   RETURN;
   1587 END IF;
   1588 
   1589 -- Retrieve admin account id
   1590 SELECT bank_account_id
   1591   INTO admin_account_id
   1592   FROM bank_accounts
   1593     JOIN customers 
   1594       ON customer_id=owning_customer_id
   1595   WHERE username = 'admin';
   1596 
   1597 -- Perform conversion
   1598 SELECT (converted).val, (converted).frac, too_small
   1599   INTO converted_amount.val, converted_amount.frac, out_too_small
   1600   FROM conversion_to(in_amount, 'cashin'::text, exchange_conversion_rate_class_id);
   1601 IF out_too_small THEN
   1602   RETURN;
   1603 END IF;
   1604 
   1605 -- Perform incoming transaction
   1606 SELECT 
   1607   transfer.out_balance_insufficient,
   1608   transfer.out_credit_row_id
   1609   INTO 
   1610     out_balance_insufficient,
   1611     tx_row_id
   1612   FROM make_incoming(
   1613     exchange_account_id,
   1614     admin_account_id,
   1615     in_subject,
   1616     converted_amount,
   1617     in_timestamp,
   1618     'reserve'::taler_incoming_type,
   1619     in_reserve_pub,
   1620     NULL,
   1621     NULL,
   1622     NULL
   1623   ) as transfer;
   1624 IF out_balance_insufficient THEN
   1625   RETURN;
   1626 END IF;
   1627 
   1628 -- update stats
   1629 CALL stats_register_payment('cashin', NULL, converted_amount, in_amount);
   1630 
   1631 END $$;
   1632 COMMENT ON FUNCTION cashin IS 'Perform a cashin operation';
   1633 
   1634 
   1635 CREATE FUNCTION cashout_create(
   1636   IN in_username TEXT,
   1637   IN in_request_uid BYTEA,
   1638   IN in_amount_debit taler_amount,
   1639   IN in_amount_credit taler_amount,
   1640   IN in_subject TEXT,
   1641   IN in_timestamp INT8,
   1642   IN in_is_tan BOOLEAN,
   1643   -- Error status
   1644   OUT out_bad_conversion BOOLEAN,
   1645   OUT out_account_not_found BOOLEAN,
   1646   OUT out_account_is_exchange BOOLEAN,
   1647   OUT out_balance_insufficient BOOLEAN,
   1648   OUT out_request_uid_reuse BOOLEAN,
   1649   OUT out_no_cashout_payto BOOLEAN,
   1650   OUT out_tan_required BOOLEAN,
   1651   OUT out_under_min BOOLEAN,
   1652   -- Success return
   1653   OUT out_cashout_id INT8
   1654 )
   1655 LANGUAGE plpgsql AS $$ 
   1656 DECLARE
   1657 account_id INT8;
   1658 account_conversion_rate_class_id INT8;
   1659 account_cashout_payto TEXT;
   1660 admin_account_id INT8;
   1661 tx_id INT8;
   1662 BEGIN
   1663 
   1664 -- Check account exists, has all info and if 2FA is required
   1665 SELECT 
   1666     bank_account_id, is_taler_exchange, conversion_rate_class_id,
   1667     -- Remove potential residual query string an add the receiver_name
   1668     split_part(cashout_payto, '?', 1) || '?receiver-name=' || url_encode(name),
   1669     NOT in_is_tan AND cardinality(tan_channels) > 0
   1670   INTO 
   1671     account_id, out_account_is_exchange, account_conversion_rate_class_id,
   1672     account_cashout_payto, out_tan_required
   1673   FROM bank_accounts
   1674   JOIN customers ON owning_customer_id=customer_id
   1675   WHERE username=in_username;
   1676 IF NOT FOUND THEN
   1677   out_account_not_found=TRUE;
   1678   RETURN;
   1679 ELSIF account_cashout_payto IS NULL THEN
   1680   out_no_cashout_payto=TRUE;
   1681   RETURN;
   1682 ELSIF out_account_is_exchange THEN
   1683   RETURN;
   1684 END IF;
   1685 
   1686 -- check conversion
   1687 SELECT under_min, too_small OR in_amount_credit!=converted
   1688   INTO out_under_min, out_bad_conversion 
   1689   FROM conversion_to(in_amount_debit, 'cashout'::text, account_conversion_rate_class_id);
   1690 IF out_bad_conversion THEN
   1691   RETURN;
   1692 END IF;
   1693 
   1694 -- Retrieve admin account id
   1695 SELECT bank_account_id
   1696   INTO admin_account_id
   1697   FROM bank_accounts
   1698     JOIN customers 
   1699       ON customer_id=owning_customer_id
   1700   WHERE username = 'admin';
   1701 
   1702 -- Check for idempotence and conflict
   1703 SELECT (amount_debit != in_amount_debit
   1704           OR amount_credit != in_amount_credit
   1705           OR subject != in_subject 
   1706           OR bank_account != account_id)
   1707         , cashout_id
   1708   INTO out_request_uid_reuse, out_cashout_id
   1709   FROM cashout_operations
   1710   WHERE request_uid = in_request_uid;
   1711 IF found OR out_request_uid_reuse OR out_tan_required THEN
   1712   RETURN;
   1713 END IF;
   1714 
   1715 -- Perform bank wire transfer
   1716 SELECT transfer.out_balance_insufficient, out_debit_row_id
   1717 INTO out_balance_insufficient, tx_id
   1718 FROM bank_wire_transfer(
   1719   admin_account_id,
   1720   account_id,
   1721   in_subject,
   1722   in_amount_debit,
   1723   in_timestamp,
   1724   NULL,
   1725   NULL,
   1726   NULL
   1727 ) as transfer;
   1728 IF out_balance_insufficient THEN
   1729   RETURN;
   1730 END IF;
   1731 
   1732 -- Create cashout operation
   1733 INSERT INTO cashout_operations (
   1734   request_uid
   1735   ,amount_debit
   1736   ,amount_credit
   1737   ,creation_time
   1738   ,bank_account
   1739   ,subject
   1740   ,local_transaction
   1741 ) VALUES (
   1742   in_request_uid
   1743   ,in_amount_debit
   1744   ,in_amount_credit
   1745   ,in_timestamp
   1746   ,account_id
   1747   ,in_subject
   1748   ,tx_id
   1749 ) RETURNING cashout_id INTO out_cashout_id;
   1750 
   1751 -- Initiate libeufin-nexus transaction
   1752 INSERT INTO libeufin_nexus.initiated_outgoing_transactions (
   1753   amount
   1754   ,subject
   1755   ,credit_payto
   1756   ,initiation_time
   1757   ,end_to_end_id
   1758 ) VALUES (
   1759   ((in_amount_credit).val, (in_amount_credit).frac)::libeufin_nexus.taler_amount
   1760   ,in_subject
   1761   ,account_cashout_payto
   1762   ,in_timestamp
   1763   ,libeufin_nexus.ebics_id_gen()
   1764 );
   1765 
   1766 -- update stats
   1767 CALL stats_register_payment('cashout', NULL, in_amount_debit, in_amount_credit);
   1768 END $$;
   1769 
   1770 CREATE FUNCTION tan_challenge_mark_sent (
   1771   IN in_uuid UUID,
   1772   IN in_timestamp INT8,
   1773   IN in_retransmission_period INT8
   1774 ) RETURNS void
   1775 LANGUAGE sql AS $$
   1776   UPDATE tan_challenges SET 
   1777     retransmission_date = in_timestamp + in_retransmission_period
   1778   WHERE uuid = in_uuid;
   1779 $$;
   1780 COMMENT ON FUNCTION tan_challenge_mark_sent IS 'Register a challenge as successfully sent';
   1781 
   1782 CREATE FUNCTION tan_challenge_try (
   1783   IN in_uuid UUID,
   1784   IN in_code TEXT,    
   1785   IN in_timestamp INT8,
   1786   -- Error status       
   1787   OUT out_ok BOOLEAN,
   1788   OUT out_no_op BOOLEAN,
   1789   OUT out_no_retry BOOLEAN,
   1790   OUT out_expired BOOLEAN,
   1791   -- Success return
   1792   OUT out_op op_enum,
   1793   OUT out_channel tan_enum,
   1794   OUT out_info TEXT
   1795 )
   1796 LANGUAGE plpgsql as $$
   1797 DECLARE
   1798 account_id INT8;
   1799 token_creation BOOLEAN;
   1800 BEGIN
   1801 
   1802 -- Try to solve challenge
   1803 UPDATE tan_challenges SET 
   1804   confirmation_date = CASE 
   1805     WHEN (retry_counter > 0 AND in_timestamp < expiration_date AND code = in_code) THEN in_timestamp
   1806     ELSE confirmation_date
   1807   END,
   1808   retry_counter = retry_counter - 1
   1809 WHERE uuid = in_uuid
   1810 RETURNING 
   1811   confirmation_date IS NOT NULL, 
   1812   retry_counter <= 0 AND confirmation_date IS NULL,
   1813   in_timestamp >= expiration_date AND confirmation_date IS NULL,
   1814   op = 'create_token',
   1815   customer
   1816 INTO out_ok, out_no_retry, out_expired, token_creation, account_id;
   1817 out_no_op = NOT FOUND;
   1818 
   1819 IF NOT out_ok AND token_creation THEN
   1820   UPDATE customers SET token_creation_counter=token_creation_counter+1 WHERE customer_id=account_id;
   1821 END IF;
   1822 
   1823 IF out_no_op OR NOT out_ok OR out_no_retry OR out_expired THEN
   1824   RETURN;
   1825 END IF;
   1826 
   1827 -- Recover body and op from challenge
   1828 SELECT op, tan_channel, tan_info
   1829   INTO out_op, out_channel, out_info
   1830   FROM tan_challenges WHERE uuid = in_uuid;
   1831 END $$;
   1832 COMMENT ON FUNCTION tan_challenge_try IS 'Try to confirm a challenge, return true if the challenge have been confirmed';
   1833 
   1834 CREATE FUNCTION stats_get_frame(
   1835   IN date TIMESTAMP,
   1836   IN in_timeframe stat_timeframe_enum,
   1837   OUT cashin_count INT8,
   1838   OUT cashin_regional_volume taler_amount,
   1839   OUT cashin_fiat_volume taler_amount,
   1840   OUT cashout_count INT8,
   1841   OUT cashout_regional_volume taler_amount,
   1842   OUT cashout_fiat_volume taler_amount,
   1843   OUT taler_in_count INT8,
   1844   OUT taler_in_volume taler_amount,
   1845   OUT taler_out_count INT8,
   1846   OUT taler_out_volume taler_amount
   1847 )
   1848 LANGUAGE plpgsql AS $$
   1849 BEGIN
   1850   date = date_trunc(in_timeframe::text, date);
   1851   SELECT 
   1852     s.cashin_count
   1853     ,(s.cashin_regional_volume).val
   1854     ,(s.cashin_regional_volume).frac
   1855     ,(s.cashin_fiat_volume).val
   1856     ,(s.cashin_fiat_volume).frac
   1857     ,s.cashout_count
   1858     ,(s.cashout_regional_volume).val
   1859     ,(s.cashout_regional_volume).frac
   1860     ,(s.cashout_fiat_volume).val
   1861     ,(s.cashout_fiat_volume).frac
   1862     ,s.taler_in_count
   1863     ,(s.taler_in_volume).val
   1864     ,(s.taler_in_volume).frac
   1865     ,s.taler_out_count
   1866     ,(s.taler_out_volume).val
   1867     ,(s.taler_out_volume).frac
   1868   INTO
   1869     cashin_count
   1870     ,cashin_regional_volume.val
   1871     ,cashin_regional_volume.frac
   1872     ,cashin_fiat_volume.val
   1873     ,cashin_fiat_volume.frac
   1874     ,cashout_count
   1875     ,cashout_regional_volume.val
   1876     ,cashout_regional_volume.frac
   1877     ,cashout_fiat_volume.val
   1878     ,cashout_fiat_volume.frac
   1879     ,taler_in_count
   1880     ,taler_in_volume.val
   1881     ,taler_in_volume.frac
   1882     ,taler_out_count
   1883     ,taler_out_volume.val
   1884     ,taler_out_volume.frac
   1885   FROM bank_stats AS s
   1886   WHERE s.timeframe = in_timeframe 
   1887     AND s.start_time = date;
   1888 END $$;
   1889 
   1890 CREATE PROCEDURE stats_register_payment(
   1891   IN name TEXT,
   1892   IN now TIMESTAMP,
   1893   IN regional_amount taler_amount,
   1894   IN fiat_amount taler_amount
   1895 )
   1896 LANGUAGE plpgsql AS $$
   1897 BEGIN
   1898   IF now IS NULL THEN
   1899     now = timezone('utc', now())::TIMESTAMP;
   1900   END IF;
   1901   IF name = 'taler_in' THEN
   1902     INSERT INTO bank_stats AS s (
   1903       timeframe,
   1904       start_time,
   1905       taler_in_count,
   1906       taler_in_volume
   1907     ) SELECT
   1908       frame,
   1909       date_trunc(frame::text, now),
   1910       1,
   1911       regional_amount
   1912     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame
   1913     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1914     SET taler_in_count=s.taler_in_count+1,
   1915         taler_in_volume=(SELECT amount_add(s.taler_in_volume, regional_amount));
   1916   ELSIF name = 'taler_out' THEN
   1917     INSERT INTO bank_stats AS s (
   1918       timeframe,
   1919       start_time,
   1920       taler_out_count,
   1921       taler_out_volume
   1922     ) SELECT
   1923       frame,
   1924       date_trunc(frame::text, now),
   1925       1,
   1926       regional_amount
   1927     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame
   1928     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1929     SET taler_out_count=s.taler_out_count+1,
   1930         taler_out_volume=(SELECT amount_add(s.taler_out_volume, regional_amount));
   1931   ELSIF name = 'cashin' THEN
   1932     INSERT INTO bank_stats AS s (
   1933       timeframe,
   1934       start_time,
   1935       cashin_count,
   1936       cashin_regional_volume,
   1937       cashin_fiat_volume
   1938     ) SELECT
   1939       frame,
   1940       date_trunc(frame::text, now),
   1941       1,
   1942       regional_amount,
   1943       fiat_amount
   1944     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 
   1945     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1946     SET cashin_count=s.cashin_count+1,
   1947         cashin_regional_volume=(SELECT amount_add(s.cashin_regional_volume, regional_amount)),
   1948         cashin_fiat_volume=(SELECT amount_add(s.cashin_fiat_volume, fiat_amount));
   1949   ELSIF name = 'cashout' THEN
   1950     INSERT INTO bank_stats AS s (
   1951       timeframe,
   1952       start_time,
   1953       cashout_count,
   1954       cashout_regional_volume,
   1955       cashout_fiat_volume
   1956     ) SELECT
   1957       frame,
   1958       date_trunc(frame::text, now),
   1959       1,
   1960       regional_amount,
   1961       fiat_amount
   1962     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 
   1963     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1964     SET cashout_count=s.cashout_count+1,
   1965         cashout_regional_volume=(SELECT amount_add(s.cashout_regional_volume, regional_amount)),
   1966         cashout_fiat_volume=(SELECT amount_add(s.cashout_fiat_volume, fiat_amount));
   1967   ELSE
   1968     RAISE EXCEPTION 'Unknown stat %', name;
   1969   END IF;
   1970 END $$;
   1971 
   1972 CREATE FUNCTION conversion_apply_ratio(
   1973    IN amount taler_amount
   1974   ,IN ratio taler_amount
   1975   ,IN fee taler_amount
   1976   ,IN tiny taler_amount       -- Result is rounded to this amount
   1977   ,IN rounding rounding_mode  -- With this rounding mode
   1978   ,OUT result taler_amount
   1979   ,OUT out_too_small BOOLEAN
   1980 )
   1981 LANGUAGE plpgsql IMMUTABLE AS $$
   1982 DECLARE
   1983   amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error
   1984   tiny_numeric NUMERIC(24);
   1985 BEGIN
   1986   -- Handle no config case
   1987   IF ratio = (0, 0)::taler_amount THEN
   1988     out_too_small=TRUE;
   1989     RETURN;
   1990   END IF;
   1991 
   1992   -- Perform multiplication using big numbers
   1993   amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) * (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000);
   1994 
   1995   -- Apply fees
   1996   amount_numeric = amount_numeric - (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24));
   1997   IF (sign(amount_numeric) != 1) THEN
   1998     out_too_small = TRUE;
   1999     result = (0, 0);
   2000     RETURN;
   2001   END IF;
   2002 
   2003   -- Round to tiny amounts
   2004   tiny_numeric = (tiny.val::numeric(24) * 100000000 + tiny.frac::numeric(24));
   2005   case rounding
   2006     when 'zero' then amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric;
   2007     when 'up' then amount_numeric = ceil(amount_numeric / tiny_numeric) * tiny_numeric;
   2008     when 'nearest' then amount_numeric = round(amount_numeric / tiny_numeric) * tiny_numeric;
   2009   end case;
   2010 
   2011   -- Extract product parts
   2012   result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4);
   2013 
   2014   IF (result.val > 1::INT8<<52) THEN
   2015     RAISE EXCEPTION 'amount value overflowed';
   2016   END IF;
   2017 END $$;
   2018 COMMENT ON FUNCTION conversion_apply_ratio
   2019   IS 'Apply a ratio to an amount rounding the result to a tiny amount following a rounding mode. It raises an exception when the resulting .val is larger than 2^52';
   2020 
   2021 CREATE FUNCTION conversion_revert_ratio(
   2022    IN amount taler_amount
   2023   ,IN ratio taler_amount
   2024   ,IN fee taler_amount
   2025   ,IN tiny taler_amount       -- Result is rounded to this amount
   2026   ,IN rounding rounding_mode  -- With this rounding mode
   2027   ,IN reverse_tiny taler_amount
   2028   ,OUT result taler_amount
   2029   ,OUT bad_value BOOLEAN
   2030 )
   2031 LANGUAGE plpgsql IMMUTABLE AS $$
   2032 DECLARE
   2033   amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error
   2034   tiny_numeric NUMERIC(24);
   2035   roundtrip BOOLEAN;
   2036 BEGIN
   2037   -- Handle no config case
   2038   IF ratio = (0, 0)::taler_amount THEN
   2039     bad_value=TRUE;
   2040     RETURN;
   2041   END IF;
   2042 
   2043   -- Apply fees
   2044   amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) + (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24));
   2045 
   2046   -- Perform division using big numbers
   2047   amount_numeric = amount_numeric / (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000);
   2048 
   2049   -- Round to input digits
   2050   tiny_numeric = (reverse_tiny.val::numeric(24) * 100000000 + reverse_tiny.frac::numeric(24));
   2051   amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric;
   2052 
   2053   -- Extract division parts
   2054   result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4);
   2055 
   2056   -- Recover potentially lost tiny amount during rounding
   2057   -- There must be a clever way to compute this but I am a little limited with math
   2058   -- and revert ratio computation is not a hot function so I just use the apply ratio
   2059   -- function to be conservative and correct
   2060   SELECT ok INTO roundtrip FROM amount_left_minus_right((SELECT conversion_apply_ratio.result FROM conversion_apply_ratio(result, ratio, fee, tiny, rounding)), amount);
   2061   IF NOT roundtrip THEN
   2062     amount_numeric = amount_numeric + tiny_numeric;
   2063     result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4);
   2064   END IF;
   2065 
   2066   IF (result.val > 1::INT8<<52) THEN
   2067     RAISE EXCEPTION 'amount value overflowed';
   2068   END IF;
   2069 END $$;
   2070 COMMENT ON FUNCTION conversion_revert_ratio
   2071   IS 'Revert the application of a ratio. This function does not always return the smallest possible amount. It raises an exception when the resulting .val is larger than 2^52';
   2072 
   2073 
   2074 CREATE FUNCTION conversion_to(
   2075   IN amount taler_amount,
   2076   IN direction TEXT,
   2077   IN conversion_rate_class_id INT8,
   2078   OUT converted taler_amount,
   2079   OUT too_small BOOLEAN,
   2080   OUT under_min BOOLEAN
   2081 )
   2082 LANGUAGE plpgsql STABLE AS $$
   2083 DECLARE
   2084   at_ratio taler_amount;
   2085   out_fee taler_amount;
   2086   tiny_amount taler_amount;
   2087   min_amount taler_amount;
   2088   mode rounding_mode;
   2089 BEGIN
   2090   -- Load rate
   2091   IF direction='cashin' THEN
   2092     SELECT
   2093       (cashin_ratio).val, (cashin_ratio).frac,
   2094       (cashin_fee).val, (cashin_fee).frac,
   2095       (cashin_tiny_amount).val, (cashin_tiny_amount).frac,
   2096       (cashin_min_amount).val, (cashin_min_amount).frac,
   2097       cashin_rounding_mode
   2098     INTO 
   2099       at_ratio.val, at_ratio.frac,
   2100       out_fee.val, out_fee.frac,
   2101       tiny_amount.val, tiny_amount.frac,
   2102       min_amount.val, min_amount.frac,
   2103       mode
   2104     FROM get_conversion_class_rate(conversion_rate_class_id);
   2105   ELSE
   2106     SELECT
   2107       (cashout_ratio).val, (cashout_ratio).frac,
   2108       (cashout_fee).val, (cashout_fee).frac,
   2109       (cashout_tiny_amount).val, (cashout_tiny_amount).frac,
   2110       (cashout_min_amount).val, (cashout_min_amount).frac,
   2111       cashout_rounding_mode
   2112     INTO 
   2113       at_ratio.val, at_ratio.frac,
   2114       out_fee.val, out_fee.frac,
   2115       tiny_amount.val, tiny_amount.frac,
   2116       min_amount.val, min_amount.frac,
   2117       mode
   2118     FROM get_conversion_class_rate(conversion_rate_class_id);
   2119   END IF;
   2120 
   2121   -- Check min amount
   2122   SELECT NOT ok INTO too_small FROM amount_left_minus_right(amount, min_amount);
   2123   IF too_small THEN
   2124     under_min = true;
   2125     converted = (0, 0);
   2126     RETURN;
   2127   END IF;
   2128 
   2129   -- Perform conversion
   2130   SELECT (result).val, (result).frac, out_too_small INTO converted.val, converted.frac, too_small 
   2131     FROM conversion_apply_ratio(amount, at_ratio, out_fee, tiny_amount, mode);
   2132 END $$;
   2133 
   2134 CREATE FUNCTION conversion_from(
   2135   IN amount taler_amount,
   2136   IN direction TEXT,
   2137   IN conversion_rate_class_id INT8,
   2138   OUT converted taler_amount,
   2139   OUT too_small BOOLEAN,
   2140   OUT under_min BOOLEAN
   2141 )
   2142 LANGUAGE plpgsql STABLE AS $$
   2143 DECLARE
   2144   ratio taler_amount;
   2145   out_fee taler_amount;
   2146   tiny_amount taler_amount;
   2147   reverse_tiny_amount taler_amount;
   2148   min_amount taler_amount;
   2149   mode rounding_mode;
   2150 BEGIN
   2151   -- Load rate
   2152   IF direction='cashin' THEN
   2153     SELECT
   2154       (cashin_ratio).val, (cashin_ratio).frac,
   2155       (cashin_fee).val, (cashin_fee).frac,
   2156       (cashin_tiny_amount).val, (cashin_tiny_amount).frac,
   2157       (cashout_tiny_amount).val, (cashout_tiny_amount).frac,
   2158       (cashin_min_amount).val, (cashin_min_amount).frac,
   2159       cashin_rounding_mode
   2160     INTO 
   2161       ratio.val, ratio.frac,
   2162       out_fee.val, out_fee.frac,
   2163       tiny_amount.val, tiny_amount.frac,
   2164       reverse_tiny_amount.val, reverse_tiny_amount.frac,
   2165       min_amount.val, min_amount.frac,
   2166       mode
   2167     FROM get_conversion_class_rate(conversion_rate_class_id);
   2168   ELSE
   2169     SELECT
   2170       (cashout_ratio).val, (cashout_ratio).frac,
   2171       (cashout_fee).val, (cashout_fee).frac,
   2172       (cashout_tiny_amount).val, (cashout_tiny_amount).frac,
   2173       (cashin_tiny_amount).val, (cashin_tiny_amount).frac,
   2174       (cashout_min_amount).val, (cashout_min_amount).frac,
   2175       cashout_rounding_mode
   2176     INTO 
   2177       ratio.val, ratio.frac,
   2178       out_fee.val, out_fee.frac,
   2179       tiny_amount.val, tiny_amount.frac,
   2180       reverse_tiny_amount.val, reverse_tiny_amount.frac,
   2181       min_amount.val, min_amount.frac,
   2182       mode
   2183     FROM get_conversion_class_rate(conversion_rate_class_id);
   2184   END IF;
   2185 
   2186   -- Perform conversion
   2187   SELECT (result).val, (result).frac, bad_value INTO converted.val, converted.frac, too_small
   2188     FROM conversion_revert_ratio(amount, ratio, out_fee, tiny_amount, mode, reverse_tiny_amount);
   2189   IF too_small THEN
   2190     RETURN;
   2191   END IF;
   2192 
   2193   -- Check min amount
   2194   SELECT NOT ok INTO too_small FROM amount_left_minus_right(converted, min_amount);
   2195   IF too_small THEN
   2196     under_min = true;
   2197     converted = (0, 0);
   2198   END IF;
   2199 END $$;
   2200 
   2201 CREATE FUNCTION config_get_conversion_rate()
   2202 RETURNS TABLE (
   2203   cashin_ratio taler_amount,
   2204   cashin_fee taler_amount,
   2205   cashin_tiny_amount taler_amount,
   2206   cashin_min_amount taler_amount,
   2207   cashin_rounding_mode rounding_mode,
   2208   cashout_ratio taler_amount,
   2209   cashout_fee taler_amount,
   2210   cashout_tiny_amount taler_amount,
   2211   cashout_min_amount taler_amount,
   2212   cashout_rounding_mode rounding_mode
   2213 )
   2214 LANGUAGE sql STABLE AS $$
   2215   SELECT 
   2216     (value->'cashin'->'ratio'->'val', value->'cashin'->'ratio'->'frac')::taler_amount,
   2217     (value->'cashin'->'fee'->'val', value->'cashin'->'fee'->'frac')::taler_amount,
   2218     (value->'cashin'->'tiny_amount'->'val', value->'cashin'->'tiny_amount'->'frac')::taler_amount,
   2219     (value->'cashin'->'min_amount'->'val', value->'cashin'->'min_amount'->'frac')::taler_amount,
   2220     (value->'cashin'->>'rounding_mode')::rounding_mode,
   2221     (value->'cashout'->'ratio'->'val', value->'cashout'->'ratio'->'frac')::taler_amount,
   2222     (value->'cashout'->'fee'->'val', value->'cashout'->'fee'->'frac')::taler_amount,
   2223     (value->'cashout'->'tiny_amount'->'val', value->'cashout'->'tiny_amount'->'frac')::taler_amount,
   2224     (value->'cashout'->'min_amount'->'val', value->'cashout'->'min_amount'->'frac')::taler_amount,
   2225     (value->'cashout'->>'rounding_mode')::rounding_mode
   2226   FROM config WHERE key='conversion_rate'
   2227   UNION ALL
   2228   SELECT (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode,
   2229          (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode
   2230   LIMIT 1
   2231 $$;
   2232 
   2233 CREATE FUNCTION get_conversion_class_rate(
   2234   IN in_conversion_rate_class_id INT8
   2235 )
   2236 RETURNS TABLE (
   2237   cashin_ratio taler_amount,
   2238   cashin_fee taler_amount,
   2239   cashin_tiny_amount taler_amount,
   2240   cashin_min_amount taler_amount,
   2241   cashin_rounding_mode rounding_mode,
   2242   cashout_ratio taler_amount,
   2243   cashout_fee taler_amount,
   2244   cashout_tiny_amount taler_amount,
   2245   cashout_min_amount taler_amount,
   2246   cashout_rounding_mode rounding_mode
   2247 )
   2248 LANGUAGE sql STABLE AS $$
   2249   SELECT
   2250     COALESCE(class.cashin_ratio, cfg.cashin_ratio),
   2251     COALESCE(class.cashin_fee, cfg.cashin_fee),
   2252     cashin_tiny_amount,
   2253     COALESCE(class.cashin_min_amount, cfg.cashin_min_amount),
   2254     COALESCE(class.cashin_rounding_mode, cfg.cashin_rounding_mode),
   2255     COALESCE(class.cashout_ratio, cfg.cashout_ratio),
   2256     COALESCE(class.cashout_fee, cfg.cashout_fee),
   2257     cashout_tiny_amount,
   2258     COALESCE(class.cashout_min_amount, cfg.cashout_min_amount),
   2259     COALESCE(class.cashout_rounding_mode, cfg.cashout_rounding_mode)
   2260   FROM config_get_conversion_rate() as cfg
   2261     LEFT JOIN conversion_rate_classes as class
   2262       ON (conversion_rate_class_id=in_conversion_rate_class_id)
   2263 $$;
   2264 
   2265 CREATE PROCEDURE config_set_conversion_rate(
   2266   IN cashin_ratio taler_amount,
   2267   IN cashin_fee taler_amount,
   2268   IN cashin_tiny_amount taler_amount,
   2269   IN cashin_min_amount taler_amount,
   2270   IN cashin_rounding_mode rounding_mode,
   2271   IN cashout_ratio taler_amount,
   2272   IN cashout_fee taler_amount,
   2273   IN cashout_tiny_amount taler_amount,
   2274   IN cashout_min_amount taler_amount,
   2275   IN cashout_rounding_mode rounding_mode
   2276 )
   2277 LANGUAGE sql AS $$
   2278   INSERT INTO config (key, value) VALUES ('conversion_rate', jsonb_build_object(
   2279     'cashin', jsonb_build_object(
   2280       'ratio', jsonb_build_object('val', cashin_ratio.val, 'frac', cashin_ratio.frac),
   2281       'fee', jsonb_build_object('val', cashin_fee.val, 'frac', cashin_fee.frac),
   2282       'tiny_amount', jsonb_build_object('val', cashin_tiny_amount.val, 'frac', cashin_tiny_amount.frac),
   2283       'min_amount', jsonb_build_object('val', cashin_min_amount.val, 'frac', cashin_min_amount.frac),
   2284       'rounding_mode', cashin_rounding_mode
   2285     ),
   2286     'cashout', jsonb_build_object(
   2287       'ratio', jsonb_build_object('val', cashout_ratio.val, 'frac', cashout_ratio.frac),
   2288       'fee', jsonb_build_object('val', cashout_fee.val, 'frac', cashout_fee.frac),
   2289       'tiny_amount', jsonb_build_object('val', cashout_tiny_amount.val, 'frac', cashout_tiny_amount.frac),
   2290       'min_amount', jsonb_build_object('val', cashout_min_amount.val, 'frac', cashout_min_amount.frac),
   2291       'rounding_mode', cashout_rounding_mode
   2292     )
   2293   )) ON CONFLICT (key) DO UPDATE SET value = excluded.value
   2294 $$;
   2295 
   2296 CREATE FUNCTION register_prepared_transfers (
   2297   IN in_credit_account TEXT,
   2298   IN in_type taler_incoming_type,
   2299   IN in_account_pub BYTEA,
   2300   IN in_authorization_pub BYTEA,
   2301   IN in_authorization_sig BYTEA,
   2302   IN in_recurrent BOOLEAN,
   2303   IN in_amount taler_amount,
   2304   IN in_timestamp INT8,
   2305   IN in_subject TEXT,
   2306   -- Error status
   2307   OUT out_unknown_creditor BOOLEAN,
   2308   OUT out_not_exchange BOOLEAN,
   2309   OUT out_reserve_pub_reuse BOOLEAN,
   2310   -- Success status
   2311   OUT out_withdrawal_uuid UUID
   2312 )
   2313 LANGUAGE plpgsql AS $$
   2314 DECLARE
   2315   local_withdrawal_id INT8;
   2316   exchange_account_id INT8;
   2317   talerable_tx INT8;
   2318   idempotent BOOLEAN;
   2319 BEGIN
   2320 -- Retrieve exchange account if
   2321 SELECT bank_account_id, NOT is_taler_exchange
   2322   INTO exchange_account_id, out_not_exchange
   2323   FROM bank_accounts
   2324     JOIN customers ON customer_id=owning_customer_id
   2325   WHERE internal_payto = in_credit_account;
   2326 out_unknown_creditor=NOT FOUND;
   2327 if out_unknown_creditor OR out_not_exchange THEN RETURN; END IF;
   2328 
   2329 -- Check idempotency 
   2330 SELECT withdrawal_uuid, prepared_transfers.type = in_type 
   2331     AND account_pub = in_account_pub
   2332     AND recurrent = in_recurrent
   2333     AND amount = in_amount
   2334 INTO out_withdrawal_uuid, idempotent
   2335 FROM prepared_transfers
   2336 LEFT JOIN taler_withdrawal_operations USING (withdrawal_id)
   2337 WHERE authorization_pub = in_authorization_pub;
   2338 
   2339 -- Check idempotency and delay garbage collection
   2340 IF FOUND AND idempotent THEN
   2341   UPDATE prepared_transfers
   2342   SET registered_at=in_timestamp
   2343   WHERE authorization_pub=in_authorization_pub;
   2344   RETURN;
   2345 END IF;
   2346 
   2347 -- Check reserve pub reuse
   2348 out_reserve_pub_reuse=in_type = 'reserve' AND (
   2349   EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_account_pub AND type = 'reserve')
   2350 );
   2351 IF out_reserve_pub_reuse THEN
   2352   RETURN;
   2353 END IF;
   2354 
   2355 -- Create/replace withdrawal
   2356 IF out_withdrawal_uuid IS NOT NULL THEN
   2357   PERFORM abort_taler_withdrawal(out_withdrawal_uuid);
   2358 END IF;
   2359 out_withdrawal_uuid=null;
   2360 
   2361 IF in_recurrent THEN
   2362   -- Finalize one pending right now
   2363   DELETE FROM pending_recurrent_incoming_transactions
   2364   WHERE bank_transaction_id = (
   2365     SELECT bank_transaction_id
   2366     FROM pending_recurrent_incoming_transactions
   2367     JOIN bank_account_transactions USING (bank_transaction_id)
   2368     WHERE authorization_pub = in_authorization_pub
   2369     ORDER BY transaction_date ASC
   2370     LIMIT 1
   2371   )
   2372   RETURNING bank_transaction_id
   2373   INTO talerable_tx;
   2374   IF FOUND THEN
   2375     PERFORM register_incoming(talerable_tx, in_type, in_account_pub, exchange_account_id, in_authorization_pub, in_authorization_sig);
   2376   END IF;
   2377 ELSE
   2378   -- Bounce all pending
   2379   PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp)
   2380   FROM pending_recurrent_incoming_transactions
   2381   WHERE authorization_pub = in_authorization_pub;
   2382 
   2383   -- Create withdrawal
   2384   INSERT INTO taler_withdrawal_operations (
   2385     withdrawal_uuid,
   2386     wallet_bank_account,
   2387     amount,
   2388     suggested_amount,
   2389     no_amount_to_wallet,
   2390     exchange_bank_account,
   2391     type,
   2392     reserve_pub,
   2393     subject,
   2394     selection_done,
   2395     creation_date
   2396   ) VALUES (
   2397     gen_random_uuid(),
   2398     NULL,
   2399     in_amount,
   2400     NULL,
   2401     true,
   2402     exchange_account_id,
   2403     'map',
   2404     in_account_pub,
   2405     in_subject,
   2406     true,
   2407     in_timestamp
   2408   ) RETURNING withdrawal_uuid, withdrawal_id
   2409     INTO out_withdrawal_uuid, local_withdrawal_id;
   2410 END IF;
   2411 
   2412 -- Upsert registration
   2413 INSERT INTO prepared_transfers (
   2414   type,
   2415   account_pub,
   2416   authorization_pub,
   2417   authorization_sig,
   2418   recurrent,
   2419   registered_at,
   2420   bank_transaction_id,
   2421   withdrawal_id
   2422 ) VALUES (
   2423   in_type,
   2424   in_account_pub,
   2425   in_authorization_pub,
   2426   in_authorization_sig,
   2427   in_recurrent,
   2428   in_timestamp,
   2429   talerable_tx,
   2430   local_withdrawal_id
   2431 ) ON CONFLICT (authorization_pub)
   2432 DO UPDATE SET
   2433   type = EXCLUDED.type,
   2434   account_pub = EXCLUDED.account_pub,
   2435   recurrent = EXCLUDED.recurrent,
   2436   registered_at = EXCLUDED.registered_at,
   2437   bank_transaction_id = EXCLUDED.bank_transaction_id,
   2438   withdrawal_id = EXCLUDED.withdrawal_id,
   2439   authorization_sig = EXCLUDED.authorization_sig;
   2440 END $$;
   2441 
   2442 CREATE FUNCTION delete_prepared_transfers (
   2443   IN in_authorization_pub BYTEA,
   2444   IN in_timestamp INT8,
   2445   OUT out_found BOOLEAN
   2446 )
   2447 LANGUAGE plpgsql AS $$
   2448 BEGIN
   2449 -- Bounce all pending
   2450 PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp)
   2451 FROM pending_recurrent_incoming_transactions
   2452 WHERE authorization_pub = in_authorization_pub;
   2453 
   2454 -- Delete registration
   2455 DELETE FROM prepared_transfers
   2456 WHERE authorization_pub = in_authorization_pub;
   2457 out_found = FOUND;
   2458 
   2459 -- TODO abort withdrawal
   2460 END $$;
   2461 
   2462 COMMIT;