libeufin

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

libeufin-bank-procedures.sql (71772B)


      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 OR 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_amount,
    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 (bank_transaction_id IS NULL AND account_pub = in_metadata) OR authorization_pub = local_authorization_pub;
    787   IF local_withdrawal_uuid IS NOT NULL THEN
    788     PERFORM abort_taler_withdrawal(local_withdrawal_uuid);
    789   END IF;
    790   PERFORM register_incoming(out_credit_row_id, in_type, in_metadata, in_creditor_account_id, local_authorization_pub, local_authorization_sig);
    791 END IF;
    792 END $$;
    793 
    794 
    795 CREATE FUNCTION taler_transfer(
    796   IN in_request_uid BYTEA,
    797   IN in_wtid BYTEA,
    798   IN in_subject TEXT,
    799   IN in_amount taler_amount,
    800   IN in_exchange_base_url TEXT,
    801   IN in_metadata TEXT,
    802   IN in_credit_account_payto TEXT,
    803   IN in_username TEXT,
    804   IN in_timestamp INT8,
    805   IN in_conversion BOOLEAN,
    806   -- Error status
    807   OUT out_debtor_not_found BOOLEAN,
    808   OUT out_debtor_not_exchange BOOLEAN,
    809   OUT out_both_exchanges BOOLEAN,
    810   OUT out_creditor_admin BOOLEAN,
    811   OUT out_request_uid_reuse BOOLEAN,
    812   OUT out_wtid_reuse BOOLEAN,
    813   OUT out_exchange_balance_insufficient BOOLEAN,
    814   -- Success return
    815   OUT out_tx_row_id INT8,
    816   OUT out_timestamp INT8
    817 )
    818 LANGUAGE plpgsql AS $$
    819 DECLARE
    820 exchange_account_id INT8;
    821 creditor_account_id INT8;
    822 account_conversion_rate_class_id INT8;
    823 creditor_name TEXT;
    824 creditor_admin BOOLEAN;
    825 credit_row_id INT8;
    826 debit_row_id INT8;
    827 outgoing_id INT8;
    828 bounce_tx INT8;
    829 bounce_amount taler_amount;
    830 BEGIN
    831 -- Check for idempotence and conflict
    832 SELECT (amount != in_amount 
    833           OR creditor_payto != in_credit_account_payto
    834           OR exchange_base_url != in_exchange_base_url
    835           OR metadata != in_metadata
    836           OR wtid != in_wtid)
    837         ,transfer_operation_id, transfer_date
    838   INTO out_request_uid_reuse, out_tx_row_id, out_timestamp
    839   FROM transfer_operations
    840   WHERE request_uid = in_request_uid;
    841 IF found THEN
    842   RETURN;
    843 END IF;
    844 out_wtid_reuse = EXISTS(SELECT FROM transfer_operations WHERE wtid = in_wtid);
    845 IF out_wtid_reuse THEN
    846   RETURN;
    847 END IF;
    848 out_timestamp=in_timestamp;
    849 -- Find exchange bank account id
    850 SELECT
    851   bank_account_id, NOT is_taler_exchange, conversion_rate_class_id
    852   INTO exchange_account_id, out_debtor_not_exchange, account_conversion_rate_class_id
    853   FROM bank_accounts 
    854       JOIN customers 
    855         ON customer_id=owning_customer_id
    856   WHERE username = in_username AND deleted_at IS NULL;
    857 out_debtor_not_found=NOT FOUND;
    858 IF out_debtor_not_found OR out_debtor_not_exchange THEN
    859   RETURN;
    860 END IF;
    861 -- Find creditor bank account id
    862 SELECT
    863   bank_account_id, is_taler_exchange, username = 'admin'
    864   INTO creditor_account_id, out_both_exchanges, creditor_admin
    865   FROM bank_accounts
    866   JOIN customers ON owning_customer_id=customer_id
    867   WHERE internal_payto = in_credit_account_payto;
    868 IF NOT FOUND THEN
    869   -- Register failure
    870   INSERT INTO transfer_operations (
    871     request_uid,
    872     wtid,
    873     amount,
    874     exchange_base_url,
    875     metadata,
    876     transfer_date,
    877     exchange_outgoing_id,
    878     creditor_payto,
    879     status,
    880     status_msg,
    881     exchange_id
    882   ) VALUES (
    883     in_request_uid,
    884     in_wtid,
    885     in_amount,
    886     in_exchange_base_url,
    887     in_metadata,
    888     in_timestamp,
    889     NULL,
    890     in_credit_account_payto,
    891     'permanent_failure',
    892     'Unknown account',
    893     exchange_account_id
    894   ) RETURNING transfer_operation_id INTO out_tx_row_id;
    895   RETURN;
    896 ELSIF out_both_exchanges THEN
    897   RETURN;
    898 END IF;
    899 
    900 IF creditor_admin THEN
    901   -- Check if this is a conversion bounce
    902   IF NOT in_conversion THEN
    903     out_creditor_admin=TRUE;
    904     RETURN;
    905   END IF;
    906   
    907   -- Find the bounced transaction
    908   SELECT (amount).val, (amount).frac, incoming_transaction_id
    909     INTO bounce_amount.val, bounce_amount.frac, bounce_tx
    910     FROM libeufin_nexus.incoming_transactions
    911     JOIN libeufin_nexus.talerable_incoming_transactions USING (incoming_transaction_id)
    912     WHERE metadata=in_wtid AND type='reserve';
    913   IF NOT FOUND THEN
    914     -- Register failure
    915     INSERT INTO transfer_operations (
    916       request_uid,
    917       wtid,
    918       amount,
    919       exchange_base_url,
    920       metadata,
    921       transfer_date,
    922       exchange_outgoing_id,
    923       creditor_payto,
    924       status,
    925       status_msg,
    926       exchange_id
    927     ) VALUES (
    928       in_request_uid,
    929       in_wtid,
    930       in_amount,
    931       in_exchange_base_url,
    932       in_metadata,
    933       in_timestamp,
    934       NULL,
    935       in_credit_account_payto,
    936       'permanent_failure',
    937       'Unknown bounced transaction',
    938       exchange_account_id
    939     ) RETURNING transfer_operation_id INTO out_tx_row_id;
    940     RETURN;
    941   END IF;
    942 
    943   -- Bounce the transaction
    944   PERFORM libeufin_nexus.bounce_incoming(
    945     bounce_tx
    946     ,((bounce_amount).val, (bounce_amount).frac)::libeufin_nexus.taler_amount
    947     ,libeufin_nexus.ebics_id_gen()
    948     ,in_timestamp
    949     ,'exchange bounced'
    950   );
    951 END IF;
    952 -- Perform bank transfer
    953 SELECT
    954   out_balance_insufficient,
    955   out_debit_row_id, out_credit_row_id
    956   INTO
    957     out_exchange_balance_insufficient,
    958     debit_row_id, credit_row_id
    959   FROM bank_wire_transfer(
    960     creditor_account_id,
    961     exchange_account_id,
    962     in_subject,
    963     in_amount,
    964     in_timestamp,
    965     NULL,
    966     NULL,
    967     NULL
    968   );
    969 IF out_exchange_balance_insufficient THEN
    970   RETURN;
    971 END IF;
    972 -- Register outgoing transaction
    973 INSERT INTO taler_exchange_outgoing (
    974   bank_transaction
    975 ) VALUES (
    976   debit_row_id
    977 ) RETURNING exchange_outgoing_id INTO outgoing_id;
    978 -- Update stats
    979 CALL stats_register_payment('taler_out', NULL, in_amount, null);
    980 -- Register success
    981 INSERT INTO transfer_operations (
    982   request_uid,
    983   wtid,
    984   amount,
    985   exchange_base_url,
    986   metadata,
    987   transfer_date,
    988   exchange_outgoing_id,
    989   creditor_payto,
    990   status,
    991   status_msg,
    992   exchange_id
    993 ) VALUES (
    994   in_request_uid,
    995   in_wtid,
    996   in_amount,
    997   in_exchange_base_url,
    998   in_metadata,
    999   in_timestamp,
   1000   outgoing_id,
   1001   in_credit_account_payto,
   1002   'success',
   1003   NULL,
   1004   exchange_account_id
   1005 ) RETURNING transfer_operation_id INTO out_tx_row_id;
   1006 
   1007 -- Notify new transaction
   1008 PERFORM pg_notify('bank_outgoing_tx', exchange_account_id || ' ' || creditor_account_id || ' ' || debit_row_id || ' ' || credit_row_id);
   1009 
   1010 IF creditor_admin THEN
   1011   -- Create cashout operation
   1012   INSERT INTO cashout_operations (
   1013     request_uid
   1014     ,amount_debit
   1015     ,amount_credit
   1016     ,creation_time
   1017     ,bank_account
   1018     ,subject
   1019     ,local_transaction
   1020   ) VALUES (
   1021     NULL
   1022     ,in_amount
   1023     ,bounce_amount
   1024     ,in_timestamp
   1025     ,exchange_account_id
   1026     ,in_subject
   1027     ,debit_row_id
   1028   );
   1029 
   1030   -- update stats
   1031   CALL stats_register_payment('cashout', NULL, in_amount, bounce_amount);
   1032 END IF;
   1033 END $$;
   1034 COMMENT ON FUNCTION taler_transfer IS 'Create an outgoing taler transaction and register it';
   1035 
   1036 CREATE FUNCTION taler_add_incoming(
   1037   IN in_key BYTEA,
   1038   IN in_subject TEXT,
   1039   IN in_amount taler_amount,
   1040   IN in_debit_account_payto TEXT,
   1041   IN in_username TEXT,
   1042   IN in_timestamp INT8,
   1043   IN in_type taler_incoming_type,
   1044   -- Error status
   1045   OUT out_creditor_not_found BOOLEAN,
   1046   OUT out_creditor_not_exchange BOOLEAN,
   1047   OUT out_debtor_not_found BOOLEAN,
   1048   OUT out_both_exchanges BOOLEAN,
   1049   OUT out_reserve_pub_reuse BOOLEAN,
   1050   OUT out_mapping_reuse BOOLEAN,
   1051   OUT out_unknown_mapping BOOLEAN,
   1052   OUT out_debitor_balance_insufficient BOOLEAN,
   1053   -- Success return
   1054   OUT out_tx_row_id INT8,
   1055   OUT out_pending INT8
   1056 )
   1057 LANGUAGE plpgsql AS $$
   1058 DECLARE
   1059 exchange_bank_account_id INT8;
   1060 sender_bank_account_id INT8;
   1061 BEGIN
   1062 -- Find exchange bank account id
   1063 SELECT
   1064   bank_account_id, NOT is_taler_exchange
   1065   INTO exchange_bank_account_id, out_creditor_not_exchange
   1066   FROM bank_accounts 
   1067       JOIN customers 
   1068         ON customer_id=owning_customer_id
   1069   WHERE username = in_username AND deleted_at IS NULL;
   1070 IF NOT FOUND OR out_creditor_not_exchange THEN
   1071   out_creditor_not_found=NOT FOUND;
   1072   RETURN;
   1073 END IF;
   1074 -- Find sender bank account id
   1075 SELECT
   1076   bank_account_id, is_taler_exchange
   1077   INTO sender_bank_account_id, out_both_exchanges
   1078   FROM bank_accounts
   1079   WHERE internal_payto = in_debit_account_payto;
   1080 IF NOT FOUND OR out_both_exchanges THEN
   1081   out_debtor_not_found=NOT FOUND;
   1082   RETURN;
   1083 END IF;
   1084 -- Perform bank transfer
   1085 SELECT
   1086   out_balance_insufficient,
   1087   out_credit_row_id,
   1088   t.out_reserve_pub_reuse,
   1089   t.out_mapping_reuse,
   1090   t.out_unknown_mapping
   1091   INTO
   1092     out_debitor_balance_insufficient,
   1093     out_tx_row_id,
   1094     out_reserve_pub_reuse,
   1095     out_mapping_reuse,
   1096     out_unknown_mapping
   1097   FROM make_incoming(
   1098     exchange_bank_account_id,
   1099     sender_bank_account_id,
   1100     in_subject,
   1101     in_amount,
   1102     in_timestamp,
   1103     in_type,
   1104     in_key,
   1105     NULL,
   1106     NULL,
   1107     NULL
   1108   ) as t;
   1109 END $$;
   1110 COMMENT ON FUNCTION taler_add_incoming IS 'Create an incoming taler transaction and register it';
   1111 
   1112 CREATE FUNCTION bank_transaction(
   1113   IN in_credit_account_payto TEXT,
   1114   IN in_debit_account_username TEXT,
   1115   IN in_subject TEXT,
   1116   IN in_amount taler_amount,
   1117   IN in_timestamp INT8,
   1118   IN in_is_tan BOOLEAN,
   1119   IN in_request_uid BYTEA,
   1120   IN in_wire_transfer_fees taler_amount,
   1121   IN in_min_amount taler_amount,
   1122   IN in_max_amount taler_amount,
   1123   IN in_type taler_incoming_type,
   1124   IN in_metadata BYTEA,
   1125   IN in_bounce_cause TEXT,
   1126   -- Error status
   1127   OUT out_creditor_not_found BOOLEAN,
   1128   OUT out_debtor_not_found BOOLEAN,
   1129   OUT out_same_account BOOLEAN,
   1130   OUT out_balance_insufficient BOOLEAN,
   1131   OUT out_creditor_admin BOOLEAN,
   1132   OUT out_tan_required BOOLEAN,
   1133   OUT out_request_uid_reuse BOOLEAN,
   1134   OUT out_bad_amount BOOLEAN,
   1135   -- Success return
   1136   OUT out_credit_bank_account_id INT8,
   1137   OUT out_debit_bank_account_id INT8,
   1138   OUT out_credit_row_id INT8,
   1139   OUT out_debit_row_id INT8,
   1140   OUT out_creditor_is_exchange BOOLEAN,
   1141   OUT out_debtor_is_exchange BOOLEAN,
   1142   OUT out_idempotent BOOLEAN
   1143 )
   1144 LANGUAGE plpgsql AS $$
   1145 DECLARE
   1146 local_reserve_pub_reuse BOOLEAN;
   1147 local_mapping_reuse BOOLEAN;
   1148 local_unknown_mapping BOOLEAN;
   1149 BEGIN
   1150 -- Find credit bank account id and check it's not admin
   1151 SELECT bank_account_id, is_taler_exchange, username='admin'
   1152   INTO out_credit_bank_account_id, out_creditor_is_exchange, out_creditor_admin
   1153   FROM bank_accounts
   1154     JOIN customers ON customer_id=owning_customer_id
   1155   WHERE internal_payto = in_credit_account_payto AND deleted_at IS NULL;
   1156 IF NOT FOUND OR out_creditor_admin THEN
   1157   out_creditor_not_found=NOT FOUND;
   1158   RETURN;
   1159 END IF;
   1160 -- Find debit bank account ID and check it's a different account and if 2FA is required
   1161 SELECT bank_account_id, is_taler_exchange, out_credit_bank_account_id=bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0
   1162   INTO out_debit_bank_account_id, out_debtor_is_exchange, out_same_account, out_tan_required
   1163   FROM bank_accounts 
   1164     JOIN customers ON customer_id=owning_customer_id
   1165   WHERE username = in_debit_account_username AND deleted_at IS NULL;
   1166 IF NOT FOUND OR out_same_account THEN
   1167   out_debtor_not_found=NOT FOUND;
   1168   RETURN;
   1169 END IF;
   1170 -- Check for idempotence and conflict
   1171 IF in_request_uid IS NOT NULL THEN
   1172   SELECT (amount != in_amount
   1173       OR subject != in_subject 
   1174       OR bank_account_id != out_debit_bank_account_id), bank_transaction
   1175     INTO out_request_uid_reuse, out_debit_row_id
   1176     FROM bank_transaction_operations
   1177       JOIN bank_account_transactions ON bank_transaction = bank_transaction_id
   1178     WHERE request_uid = in_request_uid;
   1179   IF found OR out_tan_required THEN
   1180     out_idempotent = found AND NOT out_request_uid_reuse;
   1181     RETURN;
   1182   END IF;
   1183 ELSIF out_tan_required THEN
   1184   RETURN;
   1185 END IF;
   1186 
   1187 -- Try to perform an incoming transfer
   1188 IF out_creditor_is_exchange AND NOT out_debtor_is_exchange AND in_bounce_cause IS NULL THEN
   1189   -- Perform an incoming transfer
   1190   SELECT
   1191     transfer.out_balance_insufficient,
   1192     transfer.out_bad_amount,
   1193     transfer.out_credit_row_id,
   1194     transfer.out_debit_row_id,
   1195     out_reserve_pub_reuse,
   1196     out_mapping_reuse,
   1197     out_unknown_mapping
   1198     INTO
   1199       out_balance_insufficient,
   1200       out_bad_amount,
   1201       out_credit_row_id,
   1202       out_debit_row_id,
   1203       local_reserve_pub_reuse,
   1204       local_mapping_reuse,
   1205       local_unknown_mapping
   1206     FROM make_incoming(
   1207       out_credit_bank_account_id,
   1208       out_debit_bank_account_id,
   1209       in_subject,
   1210       in_amount,
   1211       in_timestamp,
   1212       in_type,
   1213       in_metadata,
   1214       in_wire_transfer_fees,
   1215       in_min_amount,
   1216       in_max_amount
   1217     ) as transfer;
   1218   IF out_balance_insufficient OR out_bad_amount THEN
   1219     RETURN;
   1220   END IF; 
   1221   IF local_reserve_pub_reuse THEN
   1222     in_bounce_cause = 'reserve public key reuse';
   1223   ELSIF local_mapping_reuse THEN
   1224     in_bounce_cause = 'mapping public key reuse';
   1225   ELSIF local_unknown_mapping THEN
   1226     in_bounce_cause = 'unknown mapping public key';
   1227   END IF;
   1228 END IF;
   1229 
   1230 IF out_credit_row_id IS NULL THEN
   1231   -- Perform common bank transfer
   1232   SELECT
   1233     transfer.out_balance_insufficient,
   1234     transfer.out_bad_amount,
   1235     transfer.out_credit_row_id,
   1236     transfer.out_debit_row_id
   1237     INTO
   1238       out_balance_insufficient,
   1239       out_bad_amount,
   1240       out_credit_row_id,
   1241       out_debit_row_id
   1242     FROM bank_wire_transfer(
   1243       out_credit_bank_account_id,
   1244       out_debit_bank_account_id,
   1245       in_subject,
   1246       in_amount,
   1247       in_timestamp,
   1248       in_wire_transfer_fees,
   1249       in_min_amount,
   1250       in_max_amount
   1251     ) as transfer;
   1252   IF out_balance_insufficient OR out_bad_amount THEN
   1253     RETURN;
   1254   END IF; 
   1255 END IF;
   1256 
   1257 -- Bounce if necessary
   1258 IF out_creditor_is_exchange AND in_bounce_cause IS NOT NULL THEN
   1259   PERFORM bounce(out_debit_bank_account_id, out_credit_row_id, in_bounce_cause, in_timestamp);
   1260 END IF;
   1261 
   1262 -- Store operation
   1263 IF in_request_uid IS NOT NULL THEN
   1264   INSERT INTO bank_transaction_operations (request_uid, bank_transaction)  
   1265     VALUES (in_request_uid, out_debit_row_id);
   1266 END IF;
   1267 END $$;
   1268 COMMENT ON FUNCTION bank_transaction IS 'Create a bank transaction';
   1269 
   1270 CREATE FUNCTION create_taler_withdrawal(
   1271   IN in_account_username TEXT,
   1272   IN in_withdrawal_uuid UUID,
   1273   IN in_amount taler_amount,
   1274   IN in_suggested_amount taler_amount,
   1275   IN in_no_amount_to_wallet BOOLEAN,
   1276   IN in_timestamp INT8,
   1277   IN in_wire_transfer_fees taler_amount,
   1278   IN in_min_amount taler_amount,
   1279   IN in_max_amount taler_amount,
   1280    -- Error status
   1281   OUT out_account_not_found BOOLEAN,
   1282   OUT out_account_is_exchange BOOLEAN,
   1283   OUT out_balance_insufficient BOOLEAN,
   1284   OUT out_bad_amount BOOLEAN
   1285 )
   1286 LANGUAGE plpgsql AS $$ 
   1287 DECLARE
   1288 account_id INT8;
   1289 amount_with_fee taler_amount;
   1290 BEGIN
   1291 IF in_account_username IS NOT NULL THEN
   1292   -- Check account exists
   1293   SELECT bank_account_id, is_taler_exchange
   1294     INTO account_id, out_account_is_exchange
   1295     FROM bank_accounts
   1296     JOIN customers ON bank_accounts.owning_customer_id = customers.customer_id
   1297     WHERE username=in_account_username AND deleted_at IS NULL;
   1298   out_account_not_found=NOT FOUND;
   1299   IF out_account_not_found OR out_account_is_exchange THEN
   1300     RETURN;
   1301   END IF;
   1302 
   1303   -- Check enough funds
   1304   IF in_amount IS NOT NULL OR in_suggested_amount IS NOT NULL THEN
   1305     SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient(
   1306       account_id, 
   1307       COALESCE(in_amount, in_suggested_amount), 
   1308       in_wire_transfer_fees,
   1309       in_min_amount,
   1310       in_max_amount
   1311     ) AS test INTO out_balance_insufficient, out_bad_amount;
   1312     IF out_balance_insufficient OR out_bad_amount THEN
   1313       RETURN;
   1314     END IF;
   1315   END IF;
   1316 END IF;
   1317 
   1318 -- Create withdrawal operation
   1319 INSERT INTO taler_withdrawal_operations (
   1320   withdrawal_uuid,
   1321   wallet_bank_account,
   1322   amount,
   1323   suggested_amount,
   1324   no_amount_to_wallet,
   1325   type,
   1326   creation_date
   1327 ) VALUES (
   1328   in_withdrawal_uuid,
   1329   account_id,
   1330   in_amount,
   1331   in_suggested_amount,
   1332   in_no_amount_to_wallet,
   1333   'reserve',
   1334   in_timestamp
   1335 );
   1336 END $$;
   1337 COMMENT ON FUNCTION create_taler_withdrawal IS 'Create a new withdrawal operation';
   1338 
   1339 CREATE FUNCTION select_taler_withdrawal(
   1340   IN in_withdrawal_uuid uuid,
   1341   IN in_reserve_pub BYTEA,
   1342   IN in_subject TEXT,
   1343   IN in_selected_exchange_payto TEXT,
   1344   IN in_amount taler_amount,
   1345   IN in_wire_transfer_fees taler_amount,
   1346   IN in_min_amount taler_amount,
   1347   IN in_max_amount taler_amount,
   1348   -- Error status
   1349   OUT out_no_op BOOLEAN,
   1350   OUT out_already_selected BOOLEAN,
   1351   OUT out_reserve_pub_reuse BOOLEAN,
   1352   OUT out_account_not_found BOOLEAN,
   1353   OUT out_account_is_not_exchange BOOLEAN,
   1354   OUT out_amount_differs BOOLEAN,
   1355   OUT out_balance_insufficient BOOLEAN,
   1356   OUT out_bad_amount BOOLEAN,
   1357   OUT out_aborted BOOLEAN,
   1358   -- Success return
   1359   OUT out_status TEXT
   1360 )
   1361 LANGUAGE plpgsql AS $$ 
   1362 DECLARE
   1363 selected BOOLEAN;
   1364 account_id INT8;
   1365 exchange_account_id INT8;
   1366 amount_with_fee taler_amount;
   1367 BEGIN
   1368 -- Check exchange account
   1369 SELECT bank_account_id, NOT is_taler_exchange
   1370   INTO exchange_account_id, out_account_is_not_exchange
   1371   FROM bank_accounts
   1372   WHERE internal_payto=in_selected_exchange_payto;
   1373 out_account_not_found=NOT FOUND;
   1374 IF out_account_not_found OR out_account_is_not_exchange THEN
   1375   RETURN;
   1376 END IF;
   1377 
   1378 -- Check for conflict and idempotence
   1379 SELECT
   1380   selection_done, 
   1381   aborted,
   1382   CASE 
   1383     WHEN confirmation_done THEN 'confirmed'
   1384     ELSE 'selected'
   1385   END,
   1386   selection_done 
   1387     AND (exchange_bank_account != exchange_account_id OR reserve_pub != in_reserve_pub OR amount != in_amount),
   1388   amount != in_amount,
   1389   wallet_bank_account
   1390   INTO selected, out_aborted, out_status, out_already_selected, out_amount_differs, account_id
   1391   FROM taler_withdrawal_operations
   1392   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1393 out_no_op = NOT FOUND;
   1394 IF out_no_op OR out_aborted OR out_already_selected OR out_amount_differs OR selected THEN
   1395   RETURN;
   1396 END IF;
   1397 
   1398 -- Check reserve_pub reuse
   1399 out_reserve_pub_reuse=EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_reserve_pub AND type = 'reserve') OR
   1400   EXISTS(SELECT FROM taler_withdrawal_operations WHERE reserve_pub = in_reserve_pub AND type = 'reserve');
   1401 IF out_reserve_pub_reuse THEN
   1402   RETURN;
   1403 END IF;
   1404 
   1405 IF in_amount IS NOT NULL THEN
   1406   SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient(
   1407     account_id,
   1408     in_amount,
   1409     in_wire_transfer_fees,
   1410     in_min_amount,
   1411     in_max_amount
   1412   ) AS test INTO out_balance_insufficient, out_bad_amount;
   1413   IF out_balance_insufficient OR out_bad_amount THEN
   1414     RETURN;
   1415   END IF;
   1416 END IF;
   1417 
   1418 -- Update withdrawal operation
   1419 UPDATE taler_withdrawal_operations
   1420   SET exchange_bank_account=exchange_account_id,
   1421       reserve_pub=in_reserve_pub,
   1422       subject=in_subject,
   1423       selection_done=true,
   1424       amount=COALESCE(amount, in_amount)
   1425   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1426 
   1427 -- Notify status change
   1428 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' selected');
   1429 END $$;
   1430 COMMENT ON FUNCTION select_taler_withdrawal IS 'Set details of a withdrawal operation';
   1431 
   1432 CREATE FUNCTION abort_taler_withdrawal(
   1433   IN in_withdrawal_uuid uuid,
   1434   OUT out_no_op BOOLEAN,
   1435   OUT out_already_confirmed BOOLEAN
   1436 )
   1437 LANGUAGE plpgsql AS $$
   1438 BEGIN
   1439 UPDATE taler_withdrawal_operations
   1440   SET aborted = NOT confirmation_done
   1441   WHERE withdrawal_uuid=in_withdrawal_uuid
   1442   RETURNING confirmation_done
   1443   INTO out_already_confirmed;
   1444 IF NOT FOUND OR out_already_confirmed THEN
   1445   out_no_op=NOT FOUND;
   1446   RETURN;
   1447 END IF;
   1448 
   1449 -- Notify status change
   1450 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' aborted');
   1451 END $$;
   1452 COMMENT ON FUNCTION abort_taler_withdrawal IS 'Abort a withdrawal operation.';
   1453 
   1454 CREATE FUNCTION confirm_taler_withdrawal(
   1455   IN in_username TEXT,
   1456   IN in_withdrawal_uuid uuid,
   1457   IN in_timestamp INT8,
   1458   IN in_is_tan BOOLEAN,
   1459   IN in_wire_transfer_fees taler_amount,
   1460   IN in_min_amount taler_amount,
   1461   IN in_max_amount taler_amount,
   1462   IN in_amount taler_amount,
   1463   OUT out_no_op BOOLEAN,
   1464   OUT out_balance_insufficient BOOLEAN,
   1465   OUT out_reserve_pub_reuse BOOLEAN,
   1466   OUT out_bad_amount BOOLEAN,
   1467   OUT out_creditor_not_found BOOLEAN,
   1468   OUT out_not_selected BOOLEAN,
   1469   OUT out_missing_amount BOOLEAN,
   1470   OUT out_amount_differs BOOLEAN,
   1471   OUT out_aborted BOOLEAN,
   1472   OUT out_tan_required BOOLEAN
   1473 )
   1474 LANGUAGE plpgsql AS $$
   1475 DECLARE
   1476   already_confirmed BOOLEAN;
   1477   subject_local TEXT;
   1478   reserve_pub_local BYTEA;
   1479   wallet_bank_account_local INT8;
   1480   amount_local taler_amount;
   1481   exchange_bank_account_id INT8;
   1482   tx_row_id INT8;
   1483   local_type taler_incoming_type;
   1484 BEGIN
   1485 -- Load account info
   1486 SELECT bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0
   1487 INTO wallet_bank_account_local, out_tan_required
   1488 FROM bank_accounts 
   1489 JOIN customers ON owning_customer_id=customer_id
   1490 WHERE username=in_username AND deleted_at IS NULL;
   1491 
   1492 -- Check op exists and conflict
   1493 SELECT
   1494   confirmation_done,
   1495   aborted, NOT selection_done,
   1496   reserve_pub, subject, type,
   1497   exchange_bank_account,
   1498   (amount).val, (amount).frac,
   1499   amount IS NULL AND in_amount IS NULL,
   1500   amount != in_amount
   1501   INTO
   1502     already_confirmed,
   1503     out_aborted, out_not_selected,
   1504     reserve_pub_local, subject_local, local_type,
   1505     exchange_bank_account_id,
   1506     amount_local.val, amount_local.frac,
   1507     out_missing_amount,
   1508     out_amount_differs
   1509   FROM taler_withdrawal_operations
   1510   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1511 out_no_op=NOT FOUND;
   1512 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
   1513   RETURN;
   1514 ELSIF in_amount IS NOT NULL THEN
   1515   amount_local = in_amount;
   1516 END IF;
   1517 
   1518 SELECT -- not checking for accounts existence, as it was done above.
   1519   transfer.out_balance_insufficient,
   1520   transfer.out_bad_amount,
   1521   transfer.out_reserve_pub_reuse
   1522   out_credit_row_id
   1523   INTO out_balance_insufficient, out_bad_amount, out_reserve_pub_reuse, tx_row_id
   1524 FROM make_incoming(
   1525   exchange_bank_account_id,
   1526   wallet_bank_account_local,
   1527   subject_local,
   1528   amount_local,
   1529   in_timestamp,
   1530   local_type,
   1531   reserve_pub_local,
   1532   in_wire_transfer_fees,
   1533   in_min_amount,
   1534   in_max_amount
   1535 ) as transfer;
   1536 IF out_balance_insufficient OR out_reserve_pub_reuse OR out_bad_amount THEN
   1537   RETURN;
   1538 END IF;
   1539 
   1540 -- Confirm operation and update amount
   1541 UPDATE taler_withdrawal_operations
   1542   SET amount=amount_local, confirmation_done=true
   1543   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1544 
   1545 -- Notify status change
   1546 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' confirmed');
   1547 END $$;
   1548 COMMENT ON FUNCTION confirm_taler_withdrawal
   1549   IS 'Set a withdrawal operation as confirmed and wire the funds to the exchange.';
   1550 
   1551 CREATE FUNCTION cashin(
   1552   IN in_timestamp INT8,
   1553   IN in_reserve_pub BYTEA,
   1554   IN in_amount taler_amount,
   1555   IN in_subject TEXT,
   1556   -- Error status
   1557   OUT out_no_account BOOLEAN,
   1558   OUT out_too_small BOOLEAN,
   1559   OUT out_balance_insufficient BOOLEAN
   1560 )
   1561 LANGUAGE plpgsql AS $$ 
   1562 DECLARE
   1563   converted_amount taler_amount;
   1564   admin_account_id INT8;
   1565   exchange_account_id INT8;
   1566   exchange_conversion_rate_class_id INT8;
   1567   tx_row_id INT8;
   1568 BEGIN
   1569 -- TODO check reserve_pub reuse ?
   1570 
   1571 -- Recover exchange account info
   1572 SELECT bank_account_id, conversion_rate_class_id
   1573   INTO exchange_account_id, exchange_conversion_rate_class_id
   1574   FROM bank_accounts
   1575     JOIN customers 
   1576       ON customer_id=owning_customer_id
   1577   WHERE username = 'exchange';
   1578 IF NOT FOUND THEN
   1579   out_no_account = true;
   1580   RETURN;
   1581 END IF;
   1582 
   1583 -- Retrieve admin account id
   1584 SELECT bank_account_id
   1585   INTO admin_account_id
   1586   FROM bank_accounts
   1587     JOIN customers 
   1588       ON customer_id=owning_customer_id
   1589   WHERE username = 'admin';
   1590 
   1591 -- Perform conversion
   1592 SELECT (converted).val, (converted).frac, too_small
   1593   INTO converted_amount.val, converted_amount.frac, out_too_small
   1594   FROM conversion_to(in_amount, 'cashin'::text, exchange_conversion_rate_class_id);
   1595 IF out_too_small THEN
   1596   RETURN;
   1597 END IF;
   1598 
   1599 -- Perform incoming transaction
   1600 SELECT 
   1601   transfer.out_balance_insufficient,
   1602   transfer.out_credit_row_id
   1603   INTO 
   1604     out_balance_insufficient,
   1605     tx_row_id
   1606   FROM make_incoming(
   1607     exchange_account_id,
   1608     admin_account_id,
   1609     in_subject,
   1610     converted_amount,
   1611     in_timestamp,
   1612     'reserve'::taler_incoming_type,
   1613     in_reserve_pub,
   1614     NULL,
   1615     NULL,
   1616     NULL
   1617   ) as transfer;
   1618 IF out_balance_insufficient THEN
   1619   RETURN;
   1620 END IF;
   1621 
   1622 -- update stats
   1623 CALL stats_register_payment('cashin', NULL, converted_amount, in_amount);
   1624 
   1625 END $$;
   1626 COMMENT ON FUNCTION cashin IS 'Perform a cashin operation';
   1627 
   1628 
   1629 CREATE FUNCTION cashout_create(
   1630   IN in_username TEXT,
   1631   IN in_request_uid BYTEA,
   1632   IN in_amount_debit taler_amount,
   1633   IN in_amount_credit taler_amount,
   1634   IN in_subject TEXT,
   1635   IN in_timestamp INT8,
   1636   IN in_is_tan BOOLEAN,
   1637   -- Error status
   1638   OUT out_bad_conversion BOOLEAN,
   1639   OUT out_account_not_found BOOLEAN,
   1640   OUT out_account_is_exchange BOOLEAN,
   1641   OUT out_balance_insufficient BOOLEAN,
   1642   OUT out_request_uid_reuse BOOLEAN,
   1643   OUT out_no_cashout_payto BOOLEAN,
   1644   OUT out_tan_required BOOLEAN,
   1645   OUT out_under_min BOOLEAN,
   1646   -- Success return
   1647   OUT out_cashout_id INT8
   1648 )
   1649 LANGUAGE plpgsql AS $$ 
   1650 DECLARE
   1651 account_id INT8;
   1652 account_conversion_rate_class_id INT8;
   1653 account_cashout_payto TEXT;
   1654 admin_account_id INT8;
   1655 tx_id INT8;
   1656 BEGIN
   1657 
   1658 -- Check account exists, has all info and if 2FA is required
   1659 SELECT 
   1660     bank_account_id, is_taler_exchange, conversion_rate_class_id,
   1661     -- Remove potential residual query string an add the receiver_name
   1662     split_part(cashout_payto, '?', 1) || '?receiver-name=' || url_encode(name),
   1663     NOT in_is_tan AND cardinality(tan_channels) > 0
   1664   INTO 
   1665     account_id, out_account_is_exchange, account_conversion_rate_class_id,
   1666     account_cashout_payto, out_tan_required
   1667   FROM bank_accounts
   1668   JOIN customers ON owning_customer_id=customer_id
   1669   WHERE username=in_username;
   1670 IF NOT FOUND THEN
   1671   out_account_not_found=TRUE;
   1672   RETURN;
   1673 ELSIF account_cashout_payto IS NULL THEN
   1674   out_no_cashout_payto=TRUE;
   1675   RETURN;
   1676 ELSIF out_account_is_exchange THEN
   1677   RETURN;
   1678 END IF;
   1679 
   1680 -- check conversion
   1681 SELECT under_min, too_small OR in_amount_credit!=converted
   1682   INTO out_under_min, out_bad_conversion 
   1683   FROM conversion_to(in_amount_debit, 'cashout'::text, account_conversion_rate_class_id);
   1684 IF out_bad_conversion THEN
   1685   RETURN;
   1686 END IF;
   1687 
   1688 -- Retrieve admin account id
   1689 SELECT bank_account_id
   1690   INTO admin_account_id
   1691   FROM bank_accounts
   1692     JOIN customers 
   1693       ON customer_id=owning_customer_id
   1694   WHERE username = 'admin';
   1695 
   1696 -- Check for idempotence and conflict
   1697 SELECT (amount_debit != in_amount_debit
   1698           OR subject != in_subject 
   1699           OR bank_account != account_id)
   1700         , cashout_id
   1701   INTO out_request_uid_reuse, out_cashout_id
   1702   FROM cashout_operations
   1703   WHERE request_uid = in_request_uid;
   1704 IF found OR out_request_uid_reuse OR out_tan_required THEN
   1705   RETURN;
   1706 END IF;
   1707 
   1708 -- Perform bank wire transfer
   1709 SELECT transfer.out_balance_insufficient, out_debit_row_id
   1710 INTO out_balance_insufficient, tx_id
   1711 FROM bank_wire_transfer(
   1712   admin_account_id,
   1713   account_id,
   1714   in_subject,
   1715   in_amount_debit,
   1716   in_timestamp,
   1717   NULL,
   1718   NULL,
   1719   NULL
   1720 ) as transfer;
   1721 IF out_balance_insufficient THEN
   1722   RETURN;
   1723 END IF;
   1724 
   1725 -- Create cashout operation
   1726 INSERT INTO cashout_operations (
   1727   request_uid
   1728   ,amount_debit
   1729   ,amount_credit
   1730   ,creation_time
   1731   ,bank_account
   1732   ,subject
   1733   ,local_transaction
   1734 ) VALUES (
   1735   in_request_uid
   1736   ,in_amount_debit
   1737   ,in_amount_credit
   1738   ,in_timestamp
   1739   ,account_id
   1740   ,in_subject
   1741   ,tx_id
   1742 ) RETURNING cashout_id INTO out_cashout_id;
   1743 
   1744 -- Initiate libeufin-nexus transaction
   1745 INSERT INTO libeufin_nexus.initiated_outgoing_transactions (
   1746   amount
   1747   ,subject
   1748   ,credit_payto
   1749   ,initiation_time
   1750   ,end_to_end_id
   1751 ) VALUES (
   1752   ((in_amount_credit).val, (in_amount_credit).frac)::libeufin_nexus.taler_amount
   1753   ,in_subject
   1754   ,account_cashout_payto
   1755   ,in_timestamp
   1756   ,libeufin_nexus.ebics_id_gen()
   1757 );
   1758 
   1759 -- update stats
   1760 CALL stats_register_payment('cashout', NULL, in_amount_debit, in_amount_credit);
   1761 END $$;
   1762 
   1763 CREATE FUNCTION tan_challenge_mark_sent (
   1764   IN in_uuid UUID,
   1765   IN in_timestamp INT8,
   1766   IN in_retransmission_period INT8
   1767 ) RETURNS void
   1768 LANGUAGE sql AS $$
   1769   UPDATE tan_challenges SET 
   1770     retransmission_date = in_timestamp + in_retransmission_period
   1771   WHERE uuid = in_uuid;
   1772 $$;
   1773 COMMENT ON FUNCTION tan_challenge_mark_sent IS 'Register a challenge as successfully sent';
   1774 
   1775 CREATE FUNCTION tan_challenge_try (
   1776   IN in_uuid UUID,
   1777   IN in_code TEXT,    
   1778   IN in_timestamp INT8,
   1779   -- Error status       
   1780   OUT out_ok BOOLEAN,
   1781   OUT out_no_op BOOLEAN,
   1782   OUT out_no_retry BOOLEAN,
   1783   OUT out_expired BOOLEAN,
   1784   -- Success return
   1785   OUT out_op op_enum,
   1786   OUT out_channel tan_enum,
   1787   OUT out_info TEXT
   1788 )
   1789 LANGUAGE plpgsql as $$
   1790 DECLARE
   1791 account_id INT8;
   1792 token_creation BOOLEAN;
   1793 BEGIN
   1794 
   1795 -- Try to solve challenge
   1796 UPDATE tan_challenges SET 
   1797   confirmation_date = CASE 
   1798     WHEN (retry_counter > 0 AND in_timestamp < expiration_date AND code = in_code) THEN in_timestamp
   1799     ELSE confirmation_date
   1800   END,
   1801   retry_counter = retry_counter - 1
   1802 WHERE uuid = in_uuid
   1803 RETURNING 
   1804   confirmation_date IS NOT NULL, 
   1805   retry_counter <= 0 AND confirmation_date IS NULL,
   1806   in_timestamp >= expiration_date AND confirmation_date IS NULL,
   1807   op = 'create_token',
   1808   customer
   1809 INTO out_ok, out_no_retry, out_expired, token_creation, account_id;
   1810 out_no_op = NOT FOUND;
   1811 
   1812 IF NOT out_ok AND token_creation THEN
   1813   UPDATE customers SET token_creation_counter=token_creation_counter+1 WHERE customer_id=account_id;
   1814 END IF;
   1815 
   1816 IF out_no_op OR NOT out_ok OR out_no_retry OR out_expired THEN
   1817   RETURN;
   1818 END IF;
   1819 
   1820 -- Recover body and op from challenge
   1821 SELECT op, tan_channel, tan_info
   1822   INTO out_op, out_channel, out_info
   1823   FROM tan_challenges WHERE uuid = in_uuid;
   1824 END $$;
   1825 COMMENT ON FUNCTION tan_challenge_try IS 'Try to confirm a challenge, return true if the challenge have been confirmed';
   1826 
   1827 CREATE FUNCTION stats_get_frame(
   1828   IN date TIMESTAMP,
   1829   IN in_timeframe stat_timeframe_enum,
   1830   OUT cashin_count INT8,
   1831   OUT cashin_regional_volume taler_amount,
   1832   OUT cashin_fiat_volume taler_amount,
   1833   OUT cashout_count INT8,
   1834   OUT cashout_regional_volume taler_amount,
   1835   OUT cashout_fiat_volume taler_amount,
   1836   OUT taler_in_count INT8,
   1837   OUT taler_in_volume taler_amount,
   1838   OUT taler_out_count INT8,
   1839   OUT taler_out_volume taler_amount
   1840 )
   1841 LANGUAGE plpgsql AS $$
   1842 BEGIN
   1843   date = date_trunc(in_timeframe::text, date);
   1844   SELECT 
   1845     s.cashin_count
   1846     ,(s.cashin_regional_volume).val
   1847     ,(s.cashin_regional_volume).frac
   1848     ,(s.cashin_fiat_volume).val
   1849     ,(s.cashin_fiat_volume).frac
   1850     ,s.cashout_count
   1851     ,(s.cashout_regional_volume).val
   1852     ,(s.cashout_regional_volume).frac
   1853     ,(s.cashout_fiat_volume).val
   1854     ,(s.cashout_fiat_volume).frac
   1855     ,s.taler_in_count
   1856     ,(s.taler_in_volume).val
   1857     ,(s.taler_in_volume).frac
   1858     ,s.taler_out_count
   1859     ,(s.taler_out_volume).val
   1860     ,(s.taler_out_volume).frac
   1861   INTO
   1862     cashin_count
   1863     ,cashin_regional_volume.val
   1864     ,cashin_regional_volume.frac
   1865     ,cashin_fiat_volume.val
   1866     ,cashin_fiat_volume.frac
   1867     ,cashout_count
   1868     ,cashout_regional_volume.val
   1869     ,cashout_regional_volume.frac
   1870     ,cashout_fiat_volume.val
   1871     ,cashout_fiat_volume.frac
   1872     ,taler_in_count
   1873     ,taler_in_volume.val
   1874     ,taler_in_volume.frac
   1875     ,taler_out_count
   1876     ,taler_out_volume.val
   1877     ,taler_out_volume.frac
   1878   FROM bank_stats AS s
   1879   WHERE s.timeframe = in_timeframe 
   1880     AND s.start_time = date;
   1881 END $$;
   1882 
   1883 CREATE PROCEDURE stats_register_payment(
   1884   IN name TEXT,
   1885   IN now TIMESTAMP,
   1886   IN regional_amount taler_amount,
   1887   IN fiat_amount taler_amount
   1888 )
   1889 LANGUAGE plpgsql AS $$
   1890 BEGIN
   1891   IF now IS NULL THEN
   1892     now = timezone('utc', now())::TIMESTAMP;
   1893   END IF;
   1894   IF name = 'taler_in' THEN
   1895     INSERT INTO bank_stats AS s (
   1896       timeframe,
   1897       start_time,
   1898       taler_in_count,
   1899       taler_in_volume
   1900     ) SELECT
   1901       frame,
   1902       date_trunc(frame::text, now),
   1903       1,
   1904       regional_amount
   1905     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame
   1906     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1907     SET taler_in_count=s.taler_in_count+1,
   1908         taler_in_volume=(SELECT amount_add(s.taler_in_volume, regional_amount));
   1909   ELSIF name = 'taler_out' THEN
   1910     INSERT INTO bank_stats AS s (
   1911       timeframe,
   1912       start_time,
   1913       taler_out_count,
   1914       taler_out_volume
   1915     ) SELECT
   1916       frame,
   1917       date_trunc(frame::text, now),
   1918       1,
   1919       regional_amount
   1920     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame
   1921     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1922     SET taler_out_count=s.taler_out_count+1,
   1923         taler_out_volume=(SELECT amount_add(s.taler_out_volume, regional_amount));
   1924   ELSIF name = 'cashin' THEN
   1925     INSERT INTO bank_stats AS s (
   1926       timeframe,
   1927       start_time,
   1928       cashin_count,
   1929       cashin_regional_volume,
   1930       cashin_fiat_volume
   1931     ) SELECT
   1932       frame,
   1933       date_trunc(frame::text, now),
   1934       1,
   1935       regional_amount,
   1936       fiat_amount
   1937     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 
   1938     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1939     SET cashin_count=s.cashin_count+1,
   1940         cashin_regional_volume=(SELECT amount_add(s.cashin_regional_volume, regional_amount)),
   1941         cashin_fiat_volume=(SELECT amount_add(s.cashin_fiat_volume, fiat_amount));
   1942   ELSIF name = 'cashout' THEN
   1943     INSERT INTO bank_stats AS s (
   1944       timeframe,
   1945       start_time,
   1946       cashout_count,
   1947       cashout_regional_volume,
   1948       cashout_fiat_volume
   1949     ) SELECT
   1950       frame,
   1951       date_trunc(frame::text, now),
   1952       1,
   1953       regional_amount,
   1954       fiat_amount
   1955     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 
   1956     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1957     SET cashout_count=s.cashout_count+1,
   1958         cashout_regional_volume=(SELECT amount_add(s.cashout_regional_volume, regional_amount)),
   1959         cashout_fiat_volume=(SELECT amount_add(s.cashout_fiat_volume, fiat_amount));
   1960   ELSE
   1961     RAISE EXCEPTION 'Unknown stat %', name;
   1962   END IF;
   1963 END $$;
   1964 
   1965 CREATE FUNCTION conversion_apply_ratio(
   1966    IN amount taler_amount
   1967   ,IN ratio taler_amount
   1968   ,IN fee taler_amount
   1969   ,IN tiny taler_amount       -- Result is rounded to this amount
   1970   ,IN rounding rounding_mode  -- With this rounding mode
   1971   ,OUT result taler_amount
   1972   ,OUT out_too_small BOOLEAN
   1973 )
   1974 LANGUAGE plpgsql IMMUTABLE AS $$
   1975 DECLARE
   1976   amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error
   1977   tiny_numeric NUMERIC(24);
   1978 BEGIN
   1979   -- Handle no config case
   1980   IF ratio = (0, 0)::taler_amount THEN
   1981     out_too_small=TRUE;
   1982     RETURN;
   1983   END IF;
   1984 
   1985   -- Perform multiplication using big numbers
   1986   amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) * (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000);
   1987 
   1988   -- Apply fees
   1989   amount_numeric = amount_numeric - (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24));
   1990   IF (sign(amount_numeric) != 1) THEN
   1991     out_too_small = TRUE;
   1992     result = (0, 0);
   1993     RETURN;
   1994   END IF;
   1995 
   1996   -- Round to tiny amounts
   1997   tiny_numeric = (tiny.val::numeric(24) * 100000000 + tiny.frac::numeric(24));
   1998   case rounding
   1999     when 'zero' then amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric;
   2000     when 'up' then amount_numeric = ceil(amount_numeric / tiny_numeric) * tiny_numeric;
   2001     when 'nearest' then amount_numeric = round(amount_numeric / tiny_numeric) * tiny_numeric;
   2002   end case;
   2003 
   2004   -- Extract product parts
   2005   result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4);
   2006 
   2007   IF (result.val > 1::INT8<<52) THEN
   2008     RAISE EXCEPTION 'amount value overflowed';
   2009   END IF;
   2010 END $$;
   2011 COMMENT ON FUNCTION conversion_apply_ratio
   2012   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';
   2013 
   2014 CREATE FUNCTION conversion_revert_ratio(
   2015    IN amount taler_amount
   2016   ,IN ratio taler_amount
   2017   ,IN fee taler_amount
   2018   ,IN tiny taler_amount       -- Result is rounded to this amount
   2019   ,IN rounding rounding_mode  -- With this rounding mode
   2020   ,IN reverse_tiny taler_amount
   2021   ,OUT result taler_amount
   2022   ,OUT bad_value BOOLEAN
   2023 )
   2024 LANGUAGE plpgsql IMMUTABLE AS $$
   2025 DECLARE
   2026   amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error
   2027   tiny_numeric NUMERIC(24);
   2028   roundtrip BOOLEAN;
   2029 BEGIN
   2030   -- Handle no config case
   2031   IF ratio = (0, 0)::taler_amount THEN
   2032     bad_value=TRUE;
   2033     RETURN;
   2034   END IF;
   2035 
   2036   -- Apply fees
   2037   amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) + (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24));
   2038 
   2039   -- Perform division using big numbers
   2040   amount_numeric = amount_numeric / (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000);
   2041 
   2042   -- Round to input digits
   2043   tiny_numeric = (reverse_tiny.val::numeric(24) * 100000000 + reverse_tiny.frac::numeric(24));
   2044   amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric;
   2045 
   2046   -- Extract division parts
   2047   result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4);
   2048 
   2049   -- Recover potentially lost tiny amount during rounding
   2050   -- There must be a clever way to compute this but I am a little limited with math
   2051   -- and revert ratio computation is not a hot function so I just use the apply ratio
   2052   -- function to be conservative and correct
   2053   SELECT ok INTO roundtrip FROM amount_left_minus_right((SELECT conversion_apply_ratio.result FROM conversion_apply_ratio(result, ratio, fee, tiny, rounding)), amount);
   2054   IF NOT roundtrip THEN
   2055     amount_numeric = amount_numeric + tiny_numeric;
   2056     result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4);
   2057   END IF;
   2058 
   2059   IF (result.val > 1::INT8<<52) THEN
   2060     RAISE EXCEPTION 'amount value overflowed';
   2061   END IF;
   2062 END $$;
   2063 COMMENT ON FUNCTION conversion_revert_ratio
   2064   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';
   2065 
   2066 
   2067 CREATE FUNCTION conversion_to(
   2068   IN amount taler_amount,
   2069   IN direction TEXT,
   2070   IN conversion_rate_class_id INT8,
   2071   OUT converted taler_amount,
   2072   OUT too_small BOOLEAN,
   2073   OUT under_min BOOLEAN
   2074 )
   2075 LANGUAGE plpgsql STABLE AS $$
   2076 DECLARE
   2077   at_ratio taler_amount;
   2078   out_fee taler_amount;
   2079   tiny_amount taler_amount;
   2080   min_amount taler_amount;
   2081   mode rounding_mode;
   2082 BEGIN
   2083   -- Load rate
   2084   IF direction='cashin' THEN
   2085     SELECT
   2086       (cashin_ratio).val, (cashin_ratio).frac,
   2087       (cashin_fee).val, (cashin_fee).frac,
   2088       (cashin_tiny_amount).val, (cashin_tiny_amount).frac,
   2089       (cashin_min_amount).val, (cashin_min_amount).frac,
   2090       cashin_rounding_mode
   2091     INTO 
   2092       at_ratio.val, at_ratio.frac,
   2093       out_fee.val, out_fee.frac,
   2094       tiny_amount.val, tiny_amount.frac,
   2095       min_amount.val, min_amount.frac,
   2096       mode
   2097     FROM get_conversion_class_rate(conversion_rate_class_id);
   2098   ELSE
   2099     SELECT
   2100       (cashout_ratio).val, (cashout_ratio).frac,
   2101       (cashout_fee).val, (cashout_fee).frac,
   2102       (cashout_tiny_amount).val, (cashout_tiny_amount).frac,
   2103       (cashout_min_amount).val, (cashout_min_amount).frac,
   2104       cashout_rounding_mode
   2105     INTO 
   2106       at_ratio.val, at_ratio.frac,
   2107       out_fee.val, out_fee.frac,
   2108       tiny_amount.val, tiny_amount.frac,
   2109       min_amount.val, min_amount.frac,
   2110       mode
   2111     FROM get_conversion_class_rate(conversion_rate_class_id);
   2112   END IF;
   2113 
   2114   -- Check min amount
   2115   SELECT NOT ok INTO too_small FROM amount_left_minus_right(amount, min_amount);
   2116   IF too_small THEN
   2117     under_min = true;
   2118     converted = (0, 0);
   2119     RETURN;
   2120   END IF;
   2121 
   2122   -- Perform conversion
   2123   SELECT (result).val, (result).frac, out_too_small INTO converted.val, converted.frac, too_small 
   2124     FROM conversion_apply_ratio(amount, at_ratio, out_fee, tiny_amount, mode);
   2125 END $$;
   2126 
   2127 CREATE FUNCTION conversion_from(
   2128   IN amount taler_amount,
   2129   IN direction TEXT,
   2130   IN conversion_rate_class_id INT8,
   2131   OUT converted taler_amount,
   2132   OUT too_small BOOLEAN,
   2133   OUT under_min BOOLEAN
   2134 )
   2135 LANGUAGE plpgsql STABLE AS $$
   2136 DECLARE
   2137   ratio taler_amount;
   2138   out_fee taler_amount;
   2139   tiny_amount taler_amount;
   2140   reverse_tiny_amount taler_amount;
   2141   min_amount taler_amount;
   2142   mode rounding_mode;
   2143 BEGIN
   2144   -- Load rate
   2145   IF direction='cashin' THEN
   2146     SELECT
   2147       (cashin_ratio).val, (cashin_ratio).frac,
   2148       (cashin_fee).val, (cashin_fee).frac,
   2149       (cashin_tiny_amount).val, (cashin_tiny_amount).frac,
   2150       (cashout_tiny_amount).val, (cashout_tiny_amount).frac,
   2151       (cashin_min_amount).val, (cashin_min_amount).frac,
   2152       cashin_rounding_mode
   2153     INTO 
   2154       ratio.val, ratio.frac,
   2155       out_fee.val, out_fee.frac,
   2156       tiny_amount.val, tiny_amount.frac,
   2157       reverse_tiny_amount.val, reverse_tiny_amount.frac,
   2158       min_amount.val, min_amount.frac,
   2159       mode
   2160     FROM get_conversion_class_rate(conversion_rate_class_id);
   2161   ELSE
   2162     SELECT
   2163       (cashout_ratio).val, (cashout_ratio).frac,
   2164       (cashout_fee).val, (cashout_fee).frac,
   2165       (cashout_tiny_amount).val, (cashout_tiny_amount).frac,
   2166       (cashin_tiny_amount).val, (cashin_tiny_amount).frac,
   2167       (cashout_min_amount).val, (cashout_min_amount).frac,
   2168       cashout_rounding_mode
   2169     INTO 
   2170       ratio.val, ratio.frac,
   2171       out_fee.val, out_fee.frac,
   2172       tiny_amount.val, tiny_amount.frac,
   2173       reverse_tiny_amount.val, reverse_tiny_amount.frac,
   2174       min_amount.val, min_amount.frac,
   2175       mode
   2176     FROM get_conversion_class_rate(conversion_rate_class_id);
   2177   END IF;
   2178 
   2179   -- Perform conversion
   2180   SELECT (result).val, (result).frac, bad_value INTO converted.val, converted.frac, too_small
   2181     FROM conversion_revert_ratio(amount, ratio, out_fee, tiny_amount, mode, reverse_tiny_amount);
   2182   IF too_small THEN
   2183     RETURN;
   2184   END IF;
   2185 
   2186   -- Check min amount
   2187   SELECT NOT ok INTO too_small FROM amount_left_minus_right(converted, min_amount);
   2188   IF too_small THEN
   2189     under_min = true;
   2190     converted = (0, 0);
   2191   END IF;
   2192 END $$;
   2193 
   2194 CREATE FUNCTION config_get_conversion_rate()
   2195 RETURNS TABLE (
   2196   cashin_ratio taler_amount,
   2197   cashin_fee taler_amount,
   2198   cashin_tiny_amount taler_amount,
   2199   cashin_min_amount taler_amount,
   2200   cashin_rounding_mode rounding_mode,
   2201   cashout_ratio taler_amount,
   2202   cashout_fee taler_amount,
   2203   cashout_tiny_amount taler_amount,
   2204   cashout_min_amount taler_amount,
   2205   cashout_rounding_mode rounding_mode
   2206 )
   2207 LANGUAGE sql STABLE AS $$
   2208   SELECT 
   2209     (value->'cashin'->'ratio'->'val', value->'cashin'->'ratio'->'frac')::taler_amount,
   2210     (value->'cashin'->'fee'->'val', value->'cashin'->'fee'->'frac')::taler_amount,
   2211     (value->'cashin'->'tiny_amount'->'val', value->'cashin'->'tiny_amount'->'frac')::taler_amount,
   2212     (value->'cashin'->'min_amount'->'val', value->'cashin'->'min_amount'->'frac')::taler_amount,
   2213     (value->'cashin'->>'rounding_mode')::rounding_mode,
   2214     (value->'cashout'->'ratio'->'val', value->'cashout'->'ratio'->'frac')::taler_amount,
   2215     (value->'cashout'->'fee'->'val', value->'cashout'->'fee'->'frac')::taler_amount,
   2216     (value->'cashout'->'tiny_amount'->'val', value->'cashout'->'tiny_amount'->'frac')::taler_amount,
   2217     (value->'cashout'->'min_amount'->'val', value->'cashout'->'min_amount'->'frac')::taler_amount,
   2218     (value->'cashout'->>'rounding_mode')::rounding_mode
   2219   FROM config WHERE key='conversion_rate'
   2220   UNION ALL
   2221   SELECT (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode,
   2222          (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode
   2223   LIMIT 1
   2224 $$;
   2225 
   2226 CREATE FUNCTION get_conversion_class_rate(
   2227   IN in_conversion_rate_class_id INT8
   2228 )
   2229 RETURNS TABLE (
   2230   cashin_ratio taler_amount,
   2231   cashin_fee taler_amount,
   2232   cashin_tiny_amount taler_amount,
   2233   cashin_min_amount taler_amount,
   2234   cashin_rounding_mode rounding_mode,
   2235   cashout_ratio taler_amount,
   2236   cashout_fee taler_amount,
   2237   cashout_tiny_amount taler_amount,
   2238   cashout_min_amount taler_amount,
   2239   cashout_rounding_mode rounding_mode
   2240 )
   2241 LANGUAGE sql STABLE AS $$
   2242   SELECT
   2243     COALESCE(class.cashin_ratio, cfg.cashin_ratio),
   2244     COALESCE(class.cashin_fee, cfg.cashin_fee),
   2245     cashin_tiny_amount,
   2246     COALESCE(class.cashin_min_amount, cfg.cashin_min_amount),
   2247     COALESCE(class.cashin_rounding_mode, cfg.cashin_rounding_mode),
   2248     COALESCE(class.cashout_ratio, cfg.cashout_ratio),
   2249     COALESCE(class.cashout_fee, cfg.cashout_fee),
   2250     cashout_tiny_amount,
   2251     COALESCE(class.cashout_min_amount, cfg.cashout_min_amount),
   2252     COALESCE(class.cashout_rounding_mode, cfg.cashout_rounding_mode)
   2253   FROM config_get_conversion_rate() as cfg
   2254     LEFT JOIN conversion_rate_classes as class
   2255       ON (conversion_rate_class_id=in_conversion_rate_class_id)
   2256 $$;
   2257 
   2258 CREATE PROCEDURE config_set_conversion_rate(
   2259   IN cashin_ratio taler_amount,
   2260   IN cashin_fee taler_amount,
   2261   IN cashin_tiny_amount taler_amount,
   2262   IN cashin_min_amount taler_amount,
   2263   IN cashin_rounding_mode rounding_mode,
   2264   IN cashout_ratio taler_amount,
   2265   IN cashout_fee taler_amount,
   2266   IN cashout_tiny_amount taler_amount,
   2267   IN cashout_min_amount taler_amount,
   2268   IN cashout_rounding_mode rounding_mode
   2269 )
   2270 LANGUAGE sql AS $$
   2271   INSERT INTO config (key, value) VALUES ('conversion_rate', jsonb_build_object(
   2272     'cashin', jsonb_build_object(
   2273       'ratio', jsonb_build_object('val', cashin_ratio.val, 'frac', cashin_ratio.frac),
   2274       'fee', jsonb_build_object('val', cashin_fee.val, 'frac', cashin_fee.frac),
   2275       'tiny_amount', jsonb_build_object('val', cashin_tiny_amount.val, 'frac', cashin_tiny_amount.frac),
   2276       'min_amount', jsonb_build_object('val', cashin_min_amount.val, 'frac', cashin_min_amount.frac),
   2277       'rounding_mode', cashin_rounding_mode
   2278     ),
   2279     'cashout', jsonb_build_object(
   2280       'ratio', jsonb_build_object('val', cashout_ratio.val, 'frac', cashout_ratio.frac),
   2281       'fee', jsonb_build_object('val', cashout_fee.val, 'frac', cashout_fee.frac),
   2282       'tiny_amount', jsonb_build_object('val', cashout_tiny_amount.val, 'frac', cashout_tiny_amount.frac),
   2283       'min_amount', jsonb_build_object('val', cashout_min_amount.val, 'frac', cashout_min_amount.frac),
   2284       'rounding_mode', cashout_rounding_mode
   2285     )
   2286   )) ON CONFLICT (key) DO UPDATE SET value = excluded.value
   2287 $$;
   2288 
   2289 CREATE FUNCTION register_prepared_transfers (
   2290   IN in_exchange_username TEXT,
   2291   IN in_type taler_incoming_type,
   2292   IN in_account_pub BYTEA,
   2293   IN in_authorization_pub BYTEA,
   2294   IN in_authorization_sig BYTEA,
   2295   IN in_recurrent BOOLEAN,
   2296   IN in_amount taler_amount,
   2297   IN in_timestamp INT8,
   2298   IN in_subject TEXT,
   2299   -- Error status
   2300   OUT out_unknown_account BOOLEAN,
   2301   OUT out_not_exchange BOOLEAN,
   2302   OUT out_reserve_pub_reuse BOOLEAN,
   2303   -- Success status
   2304   OUT out_withdrawal_uuid UUID
   2305 )
   2306 LANGUAGE plpgsql AS $$
   2307 DECLARE
   2308   local_withdrawal_id INT8;
   2309   exchange_account_id INT8;
   2310   talerable_tx INT8;
   2311   idempotent BOOLEAN;
   2312 BEGIN
   2313 -- Retrieve exchange account if
   2314 SELECT bank_account_id, NOT is_taler_exchange
   2315   INTO exchange_account_id, out_not_exchange
   2316   FROM bank_accounts
   2317     JOIN customers ON customer_id=owning_customer_id
   2318   WHERE username = in_exchange_username;
   2319 out_unknown_account=NOT FOUND;
   2320 if out_unknown_account OR out_not_exchange THEN RETURN; END IF;
   2321 
   2322 -- Check idempotency 
   2323 SELECT withdrawal_uuid, prepared_transfers.type = in_type 
   2324     AND account_pub = in_account_pub
   2325     AND recurrent = in_recurrent
   2326     AND amount = in_amount
   2327 INTO out_withdrawal_uuid, idempotent
   2328 FROM prepared_transfers
   2329 LEFT JOIN taler_withdrawal_operations USING (withdrawal_id)
   2330 WHERE authorization_pub = in_authorization_pub;
   2331 
   2332 -- Check idempotency and delay garbage collection
   2333 IF FOUND AND idempotent THEN
   2334   UPDATE prepared_transfers
   2335   SET registered_at=in_timestamp
   2336   WHERE authorization_pub=in_authorization_pub;
   2337   RETURN;
   2338 END IF;
   2339 
   2340 -- Check reserve pub reuse
   2341 out_reserve_pub_reuse=in_type = 'reserve' AND (
   2342   EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_account_pub AND type = 'reserve')
   2343 );
   2344 IF out_reserve_pub_reuse THEN
   2345   RETURN;
   2346 END IF;
   2347 
   2348 -- Create/replace withdrawal
   2349 IF out_withdrawal_uuid IS NOT NULL THEN
   2350   PERFORM abort_taler_withdrawal(out_withdrawal_uuid);
   2351 END IF;
   2352 out_withdrawal_uuid=null;
   2353 
   2354 IF in_recurrent THEN
   2355   -- Finalize one pending right now
   2356   DELETE FROM pending_recurrent_incoming_transactions
   2357   WHERE bank_transaction_id = (
   2358     SELECT bank_transaction_id
   2359     FROM pending_recurrent_incoming_transactions
   2360     JOIN bank_account_transactions USING (bank_transaction_id)
   2361     WHERE authorization_pub = in_authorization_pub
   2362     ORDER BY transaction_date ASC
   2363     LIMIT 1
   2364   )
   2365   RETURNING bank_transaction_id
   2366   INTO talerable_tx;
   2367   IF FOUND THEN
   2368     PERFORM register_incoming(talerable_tx, in_type, in_account_pub, exchange_account_id, in_authorization_pub, in_authorization_sig);
   2369   END IF;
   2370 ELSE
   2371   -- Bounce all pending
   2372   PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp)
   2373   FROM pending_recurrent_incoming_transactions
   2374   WHERE authorization_pub = in_authorization_pub;
   2375 
   2376   -- Create withdrawal
   2377   INSERT INTO taler_withdrawal_operations (
   2378     withdrawal_uuid,
   2379     wallet_bank_account,
   2380     amount,
   2381     suggested_amount,
   2382     no_amount_to_wallet,
   2383     exchange_bank_account,
   2384     type,
   2385     reserve_pub,
   2386     subject,
   2387     selection_done,
   2388     creation_date
   2389   ) VALUES (
   2390     gen_random_uuid(),
   2391     NULL,
   2392     in_amount,
   2393     NULL,
   2394     true,
   2395     exchange_account_id,
   2396     'map',
   2397     in_account_pub,
   2398     in_subject,
   2399     true,
   2400     in_timestamp
   2401   ) RETURNING withdrawal_uuid, withdrawal_id
   2402     INTO out_withdrawal_uuid, local_withdrawal_id;
   2403 END IF;
   2404 
   2405 -- Upsert registration
   2406 INSERT INTO prepared_transfers (
   2407   type,
   2408   account_pub,
   2409   authorization_pub,
   2410   authorization_sig,
   2411   recurrent,
   2412   registered_at,
   2413   bank_transaction_id,
   2414   withdrawal_id
   2415 ) VALUES (
   2416   in_type,
   2417   in_account_pub,
   2418   in_authorization_pub,
   2419   in_authorization_sig,
   2420   in_recurrent,
   2421   in_timestamp,
   2422   talerable_tx,
   2423   local_withdrawal_id
   2424 ) ON CONFLICT (authorization_pub)
   2425 DO UPDATE SET
   2426   type = EXCLUDED.type,
   2427   account_pub = EXCLUDED.account_pub,
   2428   recurrent = EXCLUDED.recurrent,
   2429   registered_at = EXCLUDED.registered_at,
   2430   bank_transaction_id = EXCLUDED.bank_transaction_id,
   2431   withdrawal_id = EXCLUDED.withdrawal_id,
   2432   authorization_sig = EXCLUDED.authorization_sig;
   2433 END $$;
   2434 
   2435 CREATE FUNCTION delete_prepared_transfers (
   2436   IN in_authorization_pub BYTEA,
   2437   IN in_timestamp INT8,
   2438   OUT out_found BOOLEAN
   2439 )
   2440 LANGUAGE plpgsql AS $$
   2441 BEGIN
   2442 -- Bounce all pending
   2443 PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp)
   2444 FROM pending_recurrent_incoming_transactions
   2445 WHERE authorization_pub = in_authorization_pub;
   2446 
   2447 -- Delete registration
   2448 DELETE FROM prepared_transfers
   2449 WHERE authorization_pub = in_authorization_pub;
   2450 out_found = FOUND;
   2451 
   2452 -- TODO abort withdrawal
   2453 END $$;
   2454 
   2455 COMMIT;