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