libeufin

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

libeufin-bank-procedures.sql (71736B)


      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 (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   local_type taler_incoming_type;
   1483 BEGIN
   1484 -- Load account info
   1485 SELECT bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0
   1486 INTO wallet_bank_account_local, out_tan_required
   1487 FROM bank_accounts 
   1488 JOIN customers ON owning_customer_id=customer_id
   1489 WHERE username=in_username AND deleted_at IS NULL;
   1490 
   1491 -- Check op exists and conflict
   1492 SELECT
   1493   confirmation_done,
   1494   aborted, NOT selection_done,
   1495   reserve_pub, subject, type,
   1496   exchange_bank_account,
   1497   (amount).val, (amount).frac,
   1498   amount IS NULL AND in_amount IS NULL,
   1499   amount != in_amount
   1500   INTO
   1501     already_confirmed,
   1502     out_aborted, out_not_selected,
   1503     reserve_pub_local, subject_local, local_type,
   1504     exchange_bank_account_id,
   1505     amount_local.val, amount_local.frac,
   1506     out_missing_amount,
   1507     out_amount_differs
   1508   FROM taler_withdrawal_operations
   1509   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1510 out_no_op=NOT FOUND;
   1511 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
   1512   RETURN;
   1513 ELSIF in_amount IS NOT NULL THEN
   1514   amount_local = in_amount;
   1515 END IF;
   1516 
   1517 SELECT -- not checking for accounts existence, as it was done above.
   1518   transfer.out_balance_insufficient,
   1519   transfer.out_bad_amount,
   1520   transfer.out_reserve_pub_reuse
   1521   INTO out_balance_insufficient, out_bad_amount, out_reserve_pub_reuse
   1522 FROM make_incoming(
   1523   exchange_bank_account_id,
   1524   wallet_bank_account_local,
   1525   subject_local,
   1526   amount_local,
   1527   in_timestamp,
   1528   local_type,
   1529   reserve_pub_local,
   1530   in_wire_transfer_fees,
   1531   in_min_amount,
   1532   in_max_amount
   1533 ) as transfer;
   1534 IF out_balance_insufficient OR out_reserve_pub_reuse OR out_bad_amount THEN
   1535   RETURN;
   1536 END IF;
   1537 
   1538 -- Confirm operation and update amount
   1539 UPDATE taler_withdrawal_operations
   1540   SET amount=amount_local, confirmation_done=true
   1541   WHERE withdrawal_uuid=in_withdrawal_uuid;
   1542 
   1543 -- Notify status change
   1544 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' confirmed');
   1545 END $$;
   1546 COMMENT ON FUNCTION confirm_taler_withdrawal
   1547   IS 'Set a withdrawal operation as confirmed and wire the funds to the exchange.';
   1548 
   1549 CREATE FUNCTION cashin(
   1550   IN in_timestamp INT8,
   1551   IN in_reserve_pub BYTEA,
   1552   IN in_amount taler_amount,
   1553   IN in_subject TEXT,
   1554   -- Error status
   1555   OUT out_no_account BOOLEAN,
   1556   OUT out_too_small BOOLEAN,
   1557   OUT out_balance_insufficient BOOLEAN
   1558 )
   1559 LANGUAGE plpgsql AS $$ 
   1560 DECLARE
   1561   converted_amount taler_amount;
   1562   admin_account_id INT8;
   1563   exchange_account_id INT8;
   1564   exchange_conversion_rate_class_id INT8;
   1565   tx_row_id INT8;
   1566 BEGIN
   1567 -- TODO check reserve_pub reuse ?
   1568 
   1569 -- Recover exchange account info
   1570 SELECT bank_account_id, conversion_rate_class_id
   1571   INTO exchange_account_id, exchange_conversion_rate_class_id
   1572   FROM bank_accounts
   1573     JOIN customers 
   1574       ON customer_id=owning_customer_id
   1575   WHERE username = 'exchange';
   1576 IF NOT FOUND THEN
   1577   out_no_account = true;
   1578   RETURN;
   1579 END IF;
   1580 
   1581 -- Retrieve admin account id
   1582 SELECT bank_account_id
   1583   INTO admin_account_id
   1584   FROM bank_accounts
   1585     JOIN customers 
   1586       ON customer_id=owning_customer_id
   1587   WHERE username = 'admin';
   1588 
   1589 -- Perform conversion
   1590 SELECT (converted).val, (converted).frac, too_small
   1591   INTO converted_amount.val, converted_amount.frac, out_too_small
   1592   FROM conversion_to(in_amount, 'cashin'::text, exchange_conversion_rate_class_id);
   1593 IF out_too_small THEN
   1594   RETURN;
   1595 END IF;
   1596 
   1597 -- Perform incoming transaction
   1598 SELECT 
   1599   transfer.out_balance_insufficient,
   1600   transfer.out_credit_row_id
   1601   INTO 
   1602     out_balance_insufficient,
   1603     tx_row_id
   1604   FROM make_incoming(
   1605     exchange_account_id,
   1606     admin_account_id,
   1607     in_subject,
   1608     converted_amount,
   1609     in_timestamp,
   1610     'reserve'::taler_incoming_type,
   1611     in_reserve_pub,
   1612     NULL,
   1613     NULL,
   1614     NULL
   1615   ) as transfer;
   1616 IF out_balance_insufficient THEN
   1617   RETURN;
   1618 END IF;
   1619 
   1620 -- update stats
   1621 CALL stats_register_payment('cashin', NULL, converted_amount, in_amount);
   1622 
   1623 END $$;
   1624 COMMENT ON FUNCTION cashin IS 'Perform a cashin operation';
   1625 
   1626 
   1627 CREATE FUNCTION cashout_create(
   1628   IN in_username TEXT,
   1629   IN in_request_uid BYTEA,
   1630   IN in_amount_debit taler_amount,
   1631   IN in_amount_credit taler_amount,
   1632   IN in_subject TEXT,
   1633   IN in_timestamp INT8,
   1634   IN in_is_tan BOOLEAN,
   1635   -- Error status
   1636   OUT out_bad_conversion BOOLEAN,
   1637   OUT out_account_not_found BOOLEAN,
   1638   OUT out_account_is_exchange BOOLEAN,
   1639   OUT out_balance_insufficient BOOLEAN,
   1640   OUT out_request_uid_reuse BOOLEAN,
   1641   OUT out_no_cashout_payto BOOLEAN,
   1642   OUT out_tan_required BOOLEAN,
   1643   OUT out_under_min BOOLEAN,
   1644   -- Success return
   1645   OUT out_cashout_id INT8
   1646 )
   1647 LANGUAGE plpgsql AS $$ 
   1648 DECLARE
   1649 account_id INT8;
   1650 account_conversion_rate_class_id INT8;
   1651 account_cashout_payto TEXT;
   1652 admin_account_id INT8;
   1653 tx_id INT8;
   1654 BEGIN
   1655 
   1656 -- Check account exists, has all info and if 2FA is required
   1657 SELECT 
   1658     bank_account_id, is_taler_exchange, conversion_rate_class_id,
   1659     -- Remove potential residual query string an add the receiver_name
   1660     split_part(cashout_payto, '?', 1) || '?receiver-name=' || url_encode(name),
   1661     NOT in_is_tan AND cardinality(tan_channels) > 0
   1662   INTO 
   1663     account_id, out_account_is_exchange, account_conversion_rate_class_id,
   1664     account_cashout_payto, out_tan_required
   1665   FROM bank_accounts
   1666   JOIN customers ON owning_customer_id=customer_id
   1667   WHERE username=in_username;
   1668 IF NOT FOUND THEN
   1669   out_account_not_found=TRUE;
   1670   RETURN;
   1671 ELSIF account_cashout_payto IS NULL THEN
   1672   out_no_cashout_payto=TRUE;
   1673   RETURN;
   1674 ELSIF out_account_is_exchange THEN
   1675   RETURN;
   1676 END IF;
   1677 
   1678 -- check conversion
   1679 SELECT under_min, too_small OR in_amount_credit!=converted
   1680   INTO out_under_min, out_bad_conversion 
   1681   FROM conversion_to(in_amount_debit, 'cashout'::text, account_conversion_rate_class_id);
   1682 IF out_bad_conversion THEN
   1683   RETURN;
   1684 END IF;
   1685 
   1686 -- Retrieve admin account id
   1687 SELECT bank_account_id
   1688   INTO admin_account_id
   1689   FROM bank_accounts
   1690     JOIN customers 
   1691       ON customer_id=owning_customer_id
   1692   WHERE username = 'admin';
   1693 
   1694 -- Check for idempotence and conflict
   1695 SELECT (amount_debit != in_amount_debit
   1696           OR subject != in_subject 
   1697           OR bank_account != account_id)
   1698         , cashout_id
   1699   INTO out_request_uid_reuse, out_cashout_id
   1700   FROM cashout_operations
   1701   WHERE request_uid = in_request_uid;
   1702 IF found OR out_request_uid_reuse OR out_tan_required THEN
   1703   RETURN;
   1704 END IF;
   1705 
   1706 -- Perform bank wire transfer
   1707 SELECT transfer.out_balance_insufficient, out_debit_row_id
   1708 INTO out_balance_insufficient, tx_id
   1709 FROM bank_wire_transfer(
   1710   admin_account_id,
   1711   account_id,
   1712   in_subject,
   1713   in_amount_debit,
   1714   in_timestamp,
   1715   NULL,
   1716   NULL,
   1717   NULL
   1718 ) as transfer;
   1719 IF out_balance_insufficient THEN
   1720   RETURN;
   1721 END IF;
   1722 
   1723 -- Create cashout operation
   1724 INSERT INTO cashout_operations (
   1725   request_uid
   1726   ,amount_debit
   1727   ,amount_credit
   1728   ,creation_time
   1729   ,bank_account
   1730   ,subject
   1731   ,local_transaction
   1732 ) VALUES (
   1733   in_request_uid
   1734   ,in_amount_debit
   1735   ,in_amount_credit
   1736   ,in_timestamp
   1737   ,account_id
   1738   ,in_subject
   1739   ,tx_id
   1740 ) RETURNING cashout_id INTO out_cashout_id;
   1741 
   1742 -- Initiate libeufin-nexus transaction
   1743 INSERT INTO libeufin_nexus.initiated_outgoing_transactions (
   1744   amount
   1745   ,subject
   1746   ,credit_payto
   1747   ,initiation_time
   1748   ,end_to_end_id
   1749 ) VALUES (
   1750   ((in_amount_credit).val, (in_amount_credit).frac)::libeufin_nexus.taler_amount
   1751   ,in_subject
   1752   ,account_cashout_payto
   1753   ,in_timestamp
   1754   ,libeufin_nexus.ebics_id_gen()
   1755 );
   1756 
   1757 -- update stats
   1758 CALL stats_register_payment('cashout', NULL, in_amount_debit, in_amount_credit);
   1759 END $$;
   1760 
   1761 CREATE FUNCTION tan_challenge_mark_sent (
   1762   IN in_uuid UUID,
   1763   IN in_timestamp INT8,
   1764   IN in_retransmission_period INT8
   1765 ) RETURNS void
   1766 LANGUAGE sql AS $$
   1767   UPDATE tan_challenges SET 
   1768     retransmission_date = in_timestamp + in_retransmission_period
   1769   WHERE uuid = in_uuid;
   1770 $$;
   1771 COMMENT ON FUNCTION tan_challenge_mark_sent IS 'Register a challenge as successfully sent';
   1772 
   1773 CREATE FUNCTION tan_challenge_try (
   1774   IN in_uuid UUID,
   1775   IN in_code TEXT,    
   1776   IN in_timestamp INT8,
   1777   -- Error status       
   1778   OUT out_ok BOOLEAN,
   1779   OUT out_no_op BOOLEAN,
   1780   OUT out_no_retry BOOLEAN,
   1781   OUT out_expired BOOLEAN,
   1782   -- Success return
   1783   OUT out_op op_enum,
   1784   OUT out_channel tan_enum,
   1785   OUT out_info TEXT
   1786 )
   1787 LANGUAGE plpgsql as $$
   1788 DECLARE
   1789 account_id INT8;
   1790 token_creation BOOLEAN;
   1791 BEGIN
   1792 
   1793 -- Try to solve challenge
   1794 UPDATE tan_challenges SET 
   1795   confirmation_date = CASE 
   1796     WHEN (retry_counter > 0 AND in_timestamp < expiration_date AND code = in_code) THEN in_timestamp
   1797     ELSE confirmation_date
   1798   END,
   1799   retry_counter = retry_counter - 1
   1800 WHERE uuid = in_uuid
   1801 RETURNING 
   1802   confirmation_date IS NOT NULL, 
   1803   retry_counter <= 0 AND confirmation_date IS NULL,
   1804   in_timestamp >= expiration_date AND confirmation_date IS NULL,
   1805   op = 'create_token',
   1806   customer
   1807 INTO out_ok, out_no_retry, out_expired, token_creation, account_id;
   1808 out_no_op = NOT FOUND;
   1809 
   1810 IF NOT out_ok AND token_creation THEN
   1811   UPDATE customers SET token_creation_counter=token_creation_counter+1 WHERE customer_id=account_id;
   1812 END IF;
   1813 
   1814 IF out_no_op OR NOT out_ok OR out_no_retry OR out_expired THEN
   1815   RETURN;
   1816 END IF;
   1817 
   1818 -- Recover body and op from challenge
   1819 SELECT op, tan_channel, tan_info
   1820   INTO out_op, out_channel, out_info
   1821   FROM tan_challenges WHERE uuid = in_uuid;
   1822 END $$;
   1823 COMMENT ON FUNCTION tan_challenge_try IS 'Try to confirm a challenge, return true if the challenge have been confirmed';
   1824 
   1825 CREATE FUNCTION stats_get_frame(
   1826   IN date TIMESTAMP,
   1827   IN in_timeframe stat_timeframe_enum,
   1828   OUT cashin_count INT8,
   1829   OUT cashin_regional_volume taler_amount,
   1830   OUT cashin_fiat_volume taler_amount,
   1831   OUT cashout_count INT8,
   1832   OUT cashout_regional_volume taler_amount,
   1833   OUT cashout_fiat_volume taler_amount,
   1834   OUT taler_in_count INT8,
   1835   OUT taler_in_volume taler_amount,
   1836   OUT taler_out_count INT8,
   1837   OUT taler_out_volume taler_amount
   1838 )
   1839 LANGUAGE plpgsql AS $$
   1840 BEGIN
   1841   date = date_trunc(in_timeframe::text, date);
   1842   SELECT 
   1843     s.cashin_count
   1844     ,(s.cashin_regional_volume).val
   1845     ,(s.cashin_regional_volume).frac
   1846     ,(s.cashin_fiat_volume).val
   1847     ,(s.cashin_fiat_volume).frac
   1848     ,s.cashout_count
   1849     ,(s.cashout_regional_volume).val
   1850     ,(s.cashout_regional_volume).frac
   1851     ,(s.cashout_fiat_volume).val
   1852     ,(s.cashout_fiat_volume).frac
   1853     ,s.taler_in_count
   1854     ,(s.taler_in_volume).val
   1855     ,(s.taler_in_volume).frac
   1856     ,s.taler_out_count
   1857     ,(s.taler_out_volume).val
   1858     ,(s.taler_out_volume).frac
   1859   INTO
   1860     cashin_count
   1861     ,cashin_regional_volume.val
   1862     ,cashin_regional_volume.frac
   1863     ,cashin_fiat_volume.val
   1864     ,cashin_fiat_volume.frac
   1865     ,cashout_count
   1866     ,cashout_regional_volume.val
   1867     ,cashout_regional_volume.frac
   1868     ,cashout_fiat_volume.val
   1869     ,cashout_fiat_volume.frac
   1870     ,taler_in_count
   1871     ,taler_in_volume.val
   1872     ,taler_in_volume.frac
   1873     ,taler_out_count
   1874     ,taler_out_volume.val
   1875     ,taler_out_volume.frac
   1876   FROM bank_stats AS s
   1877   WHERE s.timeframe = in_timeframe 
   1878     AND s.start_time = date;
   1879 END $$;
   1880 
   1881 CREATE PROCEDURE stats_register_payment(
   1882   IN name TEXT,
   1883   IN now TIMESTAMP,
   1884   IN regional_amount taler_amount,
   1885   IN fiat_amount taler_amount
   1886 )
   1887 LANGUAGE plpgsql AS $$
   1888 BEGIN
   1889   IF now IS NULL THEN
   1890     now = timezone('utc', now())::TIMESTAMP;
   1891   END IF;
   1892   IF name = 'taler_in' THEN
   1893     INSERT INTO bank_stats AS s (
   1894       timeframe,
   1895       start_time,
   1896       taler_in_count,
   1897       taler_in_volume
   1898     ) SELECT
   1899       frame,
   1900       date_trunc(frame::text, now),
   1901       1,
   1902       regional_amount
   1903     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame
   1904     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1905     SET taler_in_count=s.taler_in_count+1,
   1906         taler_in_volume=(SELECT amount_add(s.taler_in_volume, regional_amount));
   1907   ELSIF name = 'taler_out' THEN
   1908     INSERT INTO bank_stats AS s (
   1909       timeframe,
   1910       start_time,
   1911       taler_out_count,
   1912       taler_out_volume
   1913     ) SELECT
   1914       frame,
   1915       date_trunc(frame::text, now),
   1916       1,
   1917       regional_amount
   1918     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame
   1919     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1920     SET taler_out_count=s.taler_out_count+1,
   1921         taler_out_volume=(SELECT amount_add(s.taler_out_volume, regional_amount));
   1922   ELSIF name = 'cashin' THEN
   1923     INSERT INTO bank_stats AS s (
   1924       timeframe,
   1925       start_time,
   1926       cashin_count,
   1927       cashin_regional_volume,
   1928       cashin_fiat_volume
   1929     ) SELECT
   1930       frame,
   1931       date_trunc(frame::text, now),
   1932       1,
   1933       regional_amount,
   1934       fiat_amount
   1935     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 
   1936     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1937     SET cashin_count=s.cashin_count+1,
   1938         cashin_regional_volume=(SELECT amount_add(s.cashin_regional_volume, regional_amount)),
   1939         cashin_fiat_volume=(SELECT amount_add(s.cashin_fiat_volume, fiat_amount));
   1940   ELSIF name = 'cashout' THEN
   1941     INSERT INTO bank_stats AS s (
   1942       timeframe,
   1943       start_time,
   1944       cashout_count,
   1945       cashout_regional_volume,
   1946       cashout_fiat_volume
   1947     ) SELECT
   1948       frame,
   1949       date_trunc(frame::text, now),
   1950       1,
   1951       regional_amount,
   1952       fiat_amount
   1953     FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 
   1954     ON CONFLICT (timeframe, start_time) DO UPDATE 
   1955     SET cashout_count=s.cashout_count+1,
   1956         cashout_regional_volume=(SELECT amount_add(s.cashout_regional_volume, regional_amount)),
   1957         cashout_fiat_volume=(SELECT amount_add(s.cashout_fiat_volume, fiat_amount));
   1958   ELSE
   1959     RAISE EXCEPTION 'Unknown stat %', name;
   1960   END IF;
   1961 END $$;
   1962 
   1963 CREATE FUNCTION conversion_apply_ratio(
   1964    IN amount taler_amount
   1965   ,IN ratio taler_amount
   1966   ,IN fee taler_amount
   1967   ,IN tiny taler_amount       -- Result is rounded to this amount
   1968   ,IN rounding rounding_mode  -- With this rounding mode
   1969   ,OUT result taler_amount
   1970   ,OUT out_too_small BOOLEAN
   1971 )
   1972 LANGUAGE plpgsql IMMUTABLE AS $$
   1973 DECLARE
   1974   amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error
   1975   tiny_numeric NUMERIC(24);
   1976 BEGIN
   1977   -- Handle no config case
   1978   IF ratio = (0, 0)::taler_amount THEN
   1979     out_too_small=TRUE;
   1980     RETURN;
   1981   END IF;
   1982 
   1983   -- Perform multiplication using big numbers
   1984   amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) * (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000);
   1985 
   1986   -- Apply fees
   1987   amount_numeric = amount_numeric - (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24));
   1988   IF (sign(amount_numeric) != 1) THEN
   1989     out_too_small = TRUE;
   1990     result = (0, 0);
   1991     RETURN;
   1992   END IF;
   1993 
   1994   -- Round to tiny amounts
   1995   tiny_numeric = (tiny.val::numeric(24) * 100000000 + tiny.frac::numeric(24));
   1996   case rounding
   1997     when 'zero' then amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric;
   1998     when 'up' then amount_numeric = ceil(amount_numeric / tiny_numeric) * tiny_numeric;
   1999     when 'nearest' then amount_numeric = round(amount_numeric / tiny_numeric) * tiny_numeric;
   2000   end case;
   2001 
   2002   -- Extract product parts
   2003   result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4);
   2004 
   2005   IF (result.val > 1::INT8<<52) THEN
   2006     RAISE EXCEPTION 'amount value overflowed';
   2007   END IF;
   2008 END $$;
   2009 COMMENT ON FUNCTION conversion_apply_ratio
   2010   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';
   2011 
   2012 CREATE FUNCTION conversion_revert_ratio(
   2013    IN amount taler_amount
   2014   ,IN ratio taler_amount
   2015   ,IN fee taler_amount
   2016   ,IN tiny taler_amount       -- Result is rounded to this amount
   2017   ,IN rounding rounding_mode  -- With this rounding mode
   2018   ,IN reverse_tiny taler_amount
   2019   ,OUT result taler_amount
   2020   ,OUT bad_value BOOLEAN
   2021 )
   2022 LANGUAGE plpgsql IMMUTABLE AS $$
   2023 DECLARE
   2024   amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error
   2025   tiny_numeric NUMERIC(24);
   2026   roundtrip BOOLEAN;
   2027 BEGIN
   2028   -- Handle no config case
   2029   IF ratio = (0, 0)::taler_amount THEN
   2030     bad_value=TRUE;
   2031     RETURN;
   2032   END IF;
   2033 
   2034   -- Apply fees
   2035   amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) + (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24));
   2036 
   2037   -- Perform division using big numbers
   2038   amount_numeric = amount_numeric / (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000);
   2039 
   2040   -- Round to input digits
   2041   tiny_numeric = (reverse_tiny.val::numeric(24) * 100000000 + reverse_tiny.frac::numeric(24));
   2042   amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric;
   2043 
   2044   -- Extract division parts
   2045   result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4);
   2046 
   2047   -- Recover potentially lost tiny amount during rounding
   2048   -- There must be a clever way to compute this but I am a little limited with math
   2049   -- and revert ratio computation is not a hot function so I just use the apply ratio
   2050   -- function to be conservative and correct
   2051   SELECT ok INTO roundtrip FROM amount_left_minus_right((SELECT conversion_apply_ratio.result FROM conversion_apply_ratio(result, ratio, fee, tiny, rounding)), amount);
   2052   IF NOT roundtrip THEN
   2053     amount_numeric = amount_numeric + tiny_numeric;
   2054     result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4);
   2055   END IF;
   2056 
   2057   IF (result.val > 1::INT8<<52) THEN
   2058     RAISE EXCEPTION 'amount value overflowed';
   2059   END IF;
   2060 END $$;
   2061 COMMENT ON FUNCTION conversion_revert_ratio
   2062   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';
   2063 
   2064 
   2065 CREATE FUNCTION conversion_to(
   2066   IN amount taler_amount,
   2067   IN direction TEXT,
   2068   IN conversion_rate_class_id INT8,
   2069   OUT converted taler_amount,
   2070   OUT too_small BOOLEAN,
   2071   OUT under_min BOOLEAN
   2072 )
   2073 LANGUAGE plpgsql STABLE AS $$
   2074 DECLARE
   2075   at_ratio taler_amount;
   2076   out_fee taler_amount;
   2077   tiny_amount taler_amount;
   2078   min_amount taler_amount;
   2079   mode rounding_mode;
   2080 BEGIN
   2081   -- Load rate
   2082   IF direction='cashin' THEN
   2083     SELECT
   2084       (cashin_ratio).val, (cashin_ratio).frac,
   2085       (cashin_fee).val, (cashin_fee).frac,
   2086       (cashin_tiny_amount).val, (cashin_tiny_amount).frac,
   2087       (cashin_min_amount).val, (cashin_min_amount).frac,
   2088       cashin_rounding_mode
   2089     INTO 
   2090       at_ratio.val, at_ratio.frac,
   2091       out_fee.val, out_fee.frac,
   2092       tiny_amount.val, tiny_amount.frac,
   2093       min_amount.val, min_amount.frac,
   2094       mode
   2095     FROM get_conversion_class_rate(conversion_rate_class_id);
   2096   ELSE
   2097     SELECT
   2098       (cashout_ratio).val, (cashout_ratio).frac,
   2099       (cashout_fee).val, (cashout_fee).frac,
   2100       (cashout_tiny_amount).val, (cashout_tiny_amount).frac,
   2101       (cashout_min_amount).val, (cashout_min_amount).frac,
   2102       cashout_rounding_mode
   2103     INTO 
   2104       at_ratio.val, at_ratio.frac,
   2105       out_fee.val, out_fee.frac,
   2106       tiny_amount.val, tiny_amount.frac,
   2107       min_amount.val, min_amount.frac,
   2108       mode
   2109     FROM get_conversion_class_rate(conversion_rate_class_id);
   2110   END IF;
   2111 
   2112   -- Check min amount
   2113   SELECT NOT ok INTO too_small FROM amount_left_minus_right(amount, min_amount);
   2114   IF too_small THEN
   2115     under_min = true;
   2116     converted = (0, 0);
   2117     RETURN;
   2118   END IF;
   2119 
   2120   -- Perform conversion
   2121   SELECT (result).val, (result).frac, out_too_small INTO converted.val, converted.frac, too_small 
   2122     FROM conversion_apply_ratio(amount, at_ratio, out_fee, tiny_amount, mode);
   2123 END $$;
   2124 
   2125 CREATE FUNCTION conversion_from(
   2126   IN amount taler_amount,
   2127   IN direction TEXT,
   2128   IN conversion_rate_class_id INT8,
   2129   OUT converted taler_amount,
   2130   OUT too_small BOOLEAN,
   2131   OUT under_min BOOLEAN
   2132 )
   2133 LANGUAGE plpgsql STABLE AS $$
   2134 DECLARE
   2135   ratio taler_amount;
   2136   out_fee taler_amount;
   2137   tiny_amount taler_amount;
   2138   reverse_tiny_amount taler_amount;
   2139   min_amount taler_amount;
   2140   mode rounding_mode;
   2141 BEGIN
   2142   -- Load rate
   2143   IF direction='cashin' THEN
   2144     SELECT
   2145       (cashin_ratio).val, (cashin_ratio).frac,
   2146       (cashin_fee).val, (cashin_fee).frac,
   2147       (cashin_tiny_amount).val, (cashin_tiny_amount).frac,
   2148       (cashout_tiny_amount).val, (cashout_tiny_amount).frac,
   2149       (cashin_min_amount).val, (cashin_min_amount).frac,
   2150       cashin_rounding_mode
   2151     INTO 
   2152       ratio.val, ratio.frac,
   2153       out_fee.val, out_fee.frac,
   2154       tiny_amount.val, tiny_amount.frac,
   2155       reverse_tiny_amount.val, reverse_tiny_amount.frac,
   2156       min_amount.val, min_amount.frac,
   2157       mode
   2158     FROM get_conversion_class_rate(conversion_rate_class_id);
   2159   ELSE
   2160     SELECT
   2161       (cashout_ratio).val, (cashout_ratio).frac,
   2162       (cashout_fee).val, (cashout_fee).frac,
   2163       (cashout_tiny_amount).val, (cashout_tiny_amount).frac,
   2164       (cashin_tiny_amount).val, (cashin_tiny_amount).frac,
   2165       (cashout_min_amount).val, (cashout_min_amount).frac,
   2166       cashout_rounding_mode
   2167     INTO 
   2168       ratio.val, ratio.frac,
   2169       out_fee.val, out_fee.frac,
   2170       tiny_amount.val, tiny_amount.frac,
   2171       reverse_tiny_amount.val, reverse_tiny_amount.frac,
   2172       min_amount.val, min_amount.frac,
   2173       mode
   2174     FROM get_conversion_class_rate(conversion_rate_class_id);
   2175   END IF;
   2176 
   2177   -- Perform conversion
   2178   SELECT (result).val, (result).frac, bad_value INTO converted.val, converted.frac, too_small
   2179     FROM conversion_revert_ratio(amount, ratio, out_fee, tiny_amount, mode, reverse_tiny_amount);
   2180   IF too_small THEN
   2181     RETURN;
   2182   END IF;
   2183 
   2184   -- Check min amount
   2185   SELECT NOT ok INTO too_small FROM amount_left_minus_right(converted, min_amount);
   2186   IF too_small THEN
   2187     under_min = true;
   2188     converted = (0, 0);
   2189   END IF;
   2190 END $$;
   2191 
   2192 CREATE FUNCTION config_get_conversion_rate()
   2193 RETURNS TABLE (
   2194   cashin_ratio taler_amount,
   2195   cashin_fee taler_amount,
   2196   cashin_tiny_amount taler_amount,
   2197   cashin_min_amount taler_amount,
   2198   cashin_rounding_mode rounding_mode,
   2199   cashout_ratio taler_amount,
   2200   cashout_fee taler_amount,
   2201   cashout_tiny_amount taler_amount,
   2202   cashout_min_amount taler_amount,
   2203   cashout_rounding_mode rounding_mode
   2204 )
   2205 LANGUAGE sql STABLE AS $$
   2206   SELECT 
   2207     (value->'cashin'->'ratio'->'val', value->'cashin'->'ratio'->'frac')::taler_amount,
   2208     (value->'cashin'->'fee'->'val', value->'cashin'->'fee'->'frac')::taler_amount,
   2209     (value->'cashin'->'tiny_amount'->'val', value->'cashin'->'tiny_amount'->'frac')::taler_amount,
   2210     (value->'cashin'->'min_amount'->'val', value->'cashin'->'min_amount'->'frac')::taler_amount,
   2211     (value->'cashin'->>'rounding_mode')::rounding_mode,
   2212     (value->'cashout'->'ratio'->'val', value->'cashout'->'ratio'->'frac')::taler_amount,
   2213     (value->'cashout'->'fee'->'val', value->'cashout'->'fee'->'frac')::taler_amount,
   2214     (value->'cashout'->'tiny_amount'->'val', value->'cashout'->'tiny_amount'->'frac')::taler_amount,
   2215     (value->'cashout'->'min_amount'->'val', value->'cashout'->'min_amount'->'frac')::taler_amount,
   2216     (value->'cashout'->>'rounding_mode')::rounding_mode
   2217   FROM config WHERE key='conversion_rate'
   2218   UNION ALL
   2219   SELECT (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode,
   2220          (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode
   2221   LIMIT 1
   2222 $$;
   2223 
   2224 CREATE FUNCTION get_conversion_class_rate(
   2225   IN in_conversion_rate_class_id INT8
   2226 )
   2227 RETURNS TABLE (
   2228   cashin_ratio taler_amount,
   2229   cashin_fee taler_amount,
   2230   cashin_tiny_amount taler_amount,
   2231   cashin_min_amount taler_amount,
   2232   cashin_rounding_mode rounding_mode,
   2233   cashout_ratio taler_amount,
   2234   cashout_fee taler_amount,
   2235   cashout_tiny_amount taler_amount,
   2236   cashout_min_amount taler_amount,
   2237   cashout_rounding_mode rounding_mode
   2238 )
   2239 LANGUAGE sql STABLE AS $$
   2240   SELECT
   2241     COALESCE(class.cashin_ratio, cfg.cashin_ratio),
   2242     COALESCE(class.cashin_fee, cfg.cashin_fee),
   2243     cashin_tiny_amount,
   2244     COALESCE(class.cashin_min_amount, cfg.cashin_min_amount),
   2245     COALESCE(class.cashin_rounding_mode, cfg.cashin_rounding_mode),
   2246     COALESCE(class.cashout_ratio, cfg.cashout_ratio),
   2247     COALESCE(class.cashout_fee, cfg.cashout_fee),
   2248     cashout_tiny_amount,
   2249     COALESCE(class.cashout_min_amount, cfg.cashout_min_amount),
   2250     COALESCE(class.cashout_rounding_mode, cfg.cashout_rounding_mode)
   2251   FROM config_get_conversion_rate() as cfg
   2252     LEFT JOIN conversion_rate_classes as class
   2253       ON (conversion_rate_class_id=in_conversion_rate_class_id)
   2254 $$;
   2255 
   2256 CREATE PROCEDURE config_set_conversion_rate(
   2257   IN cashin_ratio taler_amount,
   2258   IN cashin_fee taler_amount,
   2259   IN cashin_tiny_amount taler_amount,
   2260   IN cashin_min_amount taler_amount,
   2261   IN cashin_rounding_mode rounding_mode,
   2262   IN cashout_ratio taler_amount,
   2263   IN cashout_fee taler_amount,
   2264   IN cashout_tiny_amount taler_amount,
   2265   IN cashout_min_amount taler_amount,
   2266   IN cashout_rounding_mode rounding_mode
   2267 )
   2268 LANGUAGE sql AS $$
   2269   INSERT INTO config (key, value) VALUES ('conversion_rate', jsonb_build_object(
   2270     'cashin', jsonb_build_object(
   2271       'ratio', jsonb_build_object('val', cashin_ratio.val, 'frac', cashin_ratio.frac),
   2272       'fee', jsonb_build_object('val', cashin_fee.val, 'frac', cashin_fee.frac),
   2273       'tiny_amount', jsonb_build_object('val', cashin_tiny_amount.val, 'frac', cashin_tiny_amount.frac),
   2274       'min_amount', jsonb_build_object('val', cashin_min_amount.val, 'frac', cashin_min_amount.frac),
   2275       'rounding_mode', cashin_rounding_mode
   2276     ),
   2277     'cashout', jsonb_build_object(
   2278       'ratio', jsonb_build_object('val', cashout_ratio.val, 'frac', cashout_ratio.frac),
   2279       'fee', jsonb_build_object('val', cashout_fee.val, 'frac', cashout_fee.frac),
   2280       'tiny_amount', jsonb_build_object('val', cashout_tiny_amount.val, 'frac', cashout_tiny_amount.frac),
   2281       'min_amount', jsonb_build_object('val', cashout_min_amount.val, 'frac', cashout_min_amount.frac),
   2282       'rounding_mode', cashout_rounding_mode
   2283     )
   2284   )) ON CONFLICT (key) DO UPDATE SET value = excluded.value
   2285 $$;
   2286 
   2287 CREATE FUNCTION register_prepared_transfers (
   2288   IN in_exchange_username TEXT,
   2289   IN in_type taler_incoming_type,
   2290   IN in_account_pub BYTEA,
   2291   IN in_authorization_pub BYTEA,
   2292   IN in_authorization_sig BYTEA,
   2293   IN in_recurrent BOOLEAN,
   2294   IN in_amount taler_amount,
   2295   IN in_timestamp INT8,
   2296   IN in_subject TEXT,
   2297   -- Error status
   2298   OUT out_unknown_account BOOLEAN,
   2299   OUT out_not_exchange BOOLEAN,
   2300   OUT out_reserve_pub_reuse BOOLEAN,
   2301   -- Success status
   2302   OUT out_withdrawal_uuid UUID
   2303 )
   2304 LANGUAGE plpgsql AS $$
   2305 DECLARE
   2306   local_withdrawal_id INT8;
   2307   exchange_account_id INT8;
   2308   talerable_tx INT8;
   2309   idempotent BOOLEAN;
   2310 BEGIN
   2311 -- Retrieve exchange account if
   2312 SELECT bank_account_id, NOT is_taler_exchange
   2313   INTO exchange_account_id, out_not_exchange
   2314   FROM bank_accounts
   2315     JOIN customers ON customer_id=owning_customer_id
   2316   WHERE username = in_exchange_username;
   2317 out_unknown_account=NOT FOUND;
   2318 if out_unknown_account OR out_not_exchange THEN RETURN; END IF;
   2319 
   2320 -- Check idempotency 
   2321 SELECT withdrawal_uuid, prepared_transfers.type = in_type 
   2322     AND account_pub = in_account_pub
   2323     AND recurrent = in_recurrent
   2324     AND amount = in_amount
   2325 INTO out_withdrawal_uuid, idempotent
   2326 FROM prepared_transfers
   2327 LEFT JOIN taler_withdrawal_operations USING (withdrawal_id)
   2328 WHERE authorization_pub = in_authorization_pub;
   2329 
   2330 -- Check idempotency and delay garbage collection
   2331 IF FOUND AND idempotent THEN
   2332   UPDATE prepared_transfers
   2333   SET registered_at=in_timestamp
   2334   WHERE authorization_pub=in_authorization_pub;
   2335   RETURN;
   2336 END IF;
   2337 
   2338 -- Check reserve pub reuse
   2339 out_reserve_pub_reuse=in_type = 'reserve' AND (
   2340   EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_account_pub AND type = 'reserve')
   2341 );
   2342 IF out_reserve_pub_reuse THEN
   2343   RETURN;
   2344 END IF;
   2345 
   2346 -- Create/replace withdrawal
   2347 IF out_withdrawal_uuid IS NOT NULL THEN
   2348   PERFORM abort_taler_withdrawal(out_withdrawal_uuid);
   2349 END IF;
   2350 out_withdrawal_uuid=null;
   2351 
   2352 IF in_recurrent THEN
   2353   -- Finalize one pending right now
   2354   DELETE FROM pending_recurrent_incoming_transactions
   2355   WHERE bank_transaction_id = (
   2356     SELECT bank_transaction_id
   2357     FROM pending_recurrent_incoming_transactions
   2358     JOIN bank_account_transactions USING (bank_transaction_id)
   2359     WHERE authorization_pub = in_authorization_pub
   2360     ORDER BY transaction_date ASC
   2361     LIMIT 1
   2362   )
   2363   RETURNING bank_transaction_id
   2364   INTO talerable_tx;
   2365   IF FOUND THEN
   2366     PERFORM register_incoming(talerable_tx, in_type, in_account_pub, exchange_account_id, in_authorization_pub, in_authorization_sig);
   2367   END IF;
   2368 ELSE
   2369   -- Bounce all pending
   2370   PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp)
   2371   FROM pending_recurrent_incoming_transactions
   2372   WHERE authorization_pub = in_authorization_pub;
   2373 
   2374   -- Create withdrawal
   2375   INSERT INTO taler_withdrawal_operations (
   2376     withdrawal_uuid,
   2377     wallet_bank_account,
   2378     amount,
   2379     suggested_amount,
   2380     no_amount_to_wallet,
   2381     exchange_bank_account,
   2382     type,
   2383     reserve_pub,
   2384     subject,
   2385     selection_done,
   2386     creation_date
   2387   ) VALUES (
   2388     gen_random_uuid(),
   2389     NULL,
   2390     in_amount,
   2391     NULL,
   2392     true,
   2393     exchange_account_id,
   2394     'map',
   2395     in_account_pub,
   2396     in_subject,
   2397     true,
   2398     in_timestamp
   2399   ) RETURNING withdrawal_uuid, withdrawal_id
   2400     INTO out_withdrawal_uuid, local_withdrawal_id;
   2401 END IF;
   2402 
   2403 -- Upsert registration
   2404 INSERT INTO prepared_transfers (
   2405   type,
   2406   account_pub,
   2407   authorization_pub,
   2408   authorization_sig,
   2409   recurrent,
   2410   registered_at,
   2411   bank_transaction_id,
   2412   withdrawal_id
   2413 ) VALUES (
   2414   in_type,
   2415   in_account_pub,
   2416   in_authorization_pub,
   2417   in_authorization_sig,
   2418   in_recurrent,
   2419   in_timestamp,
   2420   talerable_tx,
   2421   local_withdrawal_id
   2422 ) ON CONFLICT (authorization_pub)
   2423 DO UPDATE SET
   2424   type = EXCLUDED.type,
   2425   account_pub = EXCLUDED.account_pub,
   2426   recurrent = EXCLUDED.recurrent,
   2427   registered_at = EXCLUDED.registered_at,
   2428   bank_transaction_id = EXCLUDED.bank_transaction_id,
   2429   withdrawal_id = EXCLUDED.withdrawal_id,
   2430   authorization_sig = EXCLUDED.authorization_sig;
   2431 END $$;
   2432 
   2433 CREATE FUNCTION delete_prepared_transfers (
   2434   IN in_authorization_pub BYTEA,
   2435   IN in_timestamp INT8,
   2436   OUT out_found BOOLEAN
   2437 )
   2438 LANGUAGE plpgsql AS $$
   2439 BEGIN
   2440 -- Bounce all pending
   2441 PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp)
   2442 FROM pending_recurrent_incoming_transactions
   2443 WHERE authorization_pub = in_authorization_pub;
   2444 
   2445 -- Delete registration
   2446 DELETE FROM prepared_transfers
   2447 WHERE authorization_pub = in_authorization_pub;
   2448 out_found = FOUND;
   2449 
   2450 -- TODO abort withdrawal
   2451 END $$;
   2452 
   2453 COMMIT;