libeufin

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

libeufin-bank-procedures.sql (71643B)


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