libeufin

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

libeufin-bank-procedures.sql (71672B)


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