libeufin

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

libeufin-bank-procedures.sql (71913B)


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