libeufin

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

libeufin-bank-procedures.sql (71766B)


      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
   1511   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1512 out_no_op=NOT FOUND;
   1513 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
   1514   RETURN;
   1515 ELSIF in_amount IS NOT NULL THEN
   1516   amount_local = in_amount;
   1517 END IF;
   1518 
   1519 SELECT -- not checking for accounts existence, as it was done above.
   1520   transfer.out_balance_insufficient,
   1521   transfer.out_bad_amount,
   1522   transfer.out_reserve_pub_reuse
   1523   INTO out_balance_insufficient, out_bad_amount, out_reserve_pub_reuse
   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_credit_account 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_creditor 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 internal_payto = in_credit_account;
   2319 out_unknown_creditor=NOT FOUND;
   2320 if out_unknown_creditor 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;