increase_refund.c (21219B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022-2024 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 /** 17 * @file src/backenddb/increase_refund.c 18 * @brief Implementation of the increase_refund function for Postgres 19 * @author Christian Grothoff 20 */ 21 #include "platform.h" 22 #include <taler/taler_pq_lib.h> 23 #include "merchant-database/increase_refund.h" 24 #include "helper.h" 25 26 27 /** 28 * Information about refund limits per exchange. 29 */ 30 struct ExchangeLimit 31 { 32 /** 33 * Kept in a DLL. 34 */ 35 struct ExchangeLimit *next; 36 37 /** 38 * Kept in a DLL. 39 */ 40 struct ExchangeLimit *prev; 41 42 /** 43 * Exchange the limit is about. 44 */ 45 char *exchange_url; 46 47 /** 48 * Refund amount remaining at this exchange. 49 */ 50 struct TALER_Amount remaining_refund_limit; 51 52 }; 53 54 55 /** 56 * Closure for #process_refund_cb(). 57 */ 58 struct FindRefundContext 59 { 60 61 /** 62 * Plugin context. 63 */ 64 struct TALER_MERCHANTDB_PostgresContext *pg; 65 66 /** 67 * Updated to reflect total amount refunded so far. 68 */ 69 struct TALER_Amount refunded_amount; 70 71 /** 72 * Set to the largest refund transaction ID encountered. 73 */ 74 uint64_t max_rtransaction_id; 75 76 /** 77 * Set to true on hard errors. 78 */ 79 bool err; 80 }; 81 82 83 /** 84 * Closure for #process_deposits_for_refund_cb(). 85 */ 86 struct InsertRefundContext 87 { 88 /** 89 * Used to provide a connection to the db 90 */ 91 struct TALER_MERCHANTDB_PostgresContext *pg; 92 93 /** 94 * Head of DLL of per-exchange refund limits. 95 */ 96 struct ExchangeLimit *el_head; 97 98 /** 99 * Tail of DLL of per-exchange refund limits. 100 */ 101 struct ExchangeLimit *el_tail; 102 103 /** 104 * Amount to which increase the refund for this contract 105 */ 106 const struct TALER_Amount *refund; 107 108 /** 109 * Human-readable reason behind this refund 110 */ 111 const char *reason; 112 113 /** 114 * Function to call to determine per-exchange limits. 115 * NULL for no limits. 116 */ 117 TALER_MERCHANTDB_OperationLimitCallback olc; 118 119 /** 120 * Closure for @e olc. 121 */ 122 void *olc_cls; 123 124 /** 125 * Transaction status code. 126 */ 127 enum TALER_MERCHANTDB_RefundStatus rs; 128 129 /** 130 * Did we have to cap refunds of any coin 131 * due to legal limits? 132 */ 133 bool legal_capped; 134 135 }; 136 137 138 /** 139 * Data extracted per coin. 140 */ 141 struct RefundCoinData 142 { 143 144 /** 145 * Public key of a coin. 146 */ 147 struct TALER_CoinSpendPublicKeyP coin_pub; 148 149 /** 150 * Amount deposited for this coin. 151 */ 152 struct TALER_Amount deposited_with_fee; 153 154 /** 155 * Amount refunded already for this coin. 156 */ 157 struct TALER_Amount refund_amount; 158 159 /** 160 * Order serial (actually not really per-coin). 161 */ 162 uint64_t order_serial; 163 164 /** 165 * Maximum rtransaction_id for this coin so far. 166 */ 167 uint64_t max_rtransaction_id; 168 169 /** 170 * Exchange this coin was issued by. 171 */ 172 char *exchange_url; 173 174 }; 175 176 177 /** 178 * Find an exchange record for the refund limit enforcement. 179 * 180 * @param irc refund context 181 * @param exchange_url base URL of the exchange 182 */ 183 static struct ExchangeLimit * 184 find_exchange (struct InsertRefundContext *irc, 185 const char *exchange_url) 186 { 187 if (NULL == irc->olc) 188 return NULL; /* no limits */ 189 /* Check if entry exists, if so, do nothing */ 190 for (struct ExchangeLimit *el = irc->el_head; 191 NULL != el; 192 el = el->next) 193 if (0 == strcmp (exchange_url, 194 el->exchange_url)) 195 return el; 196 return NULL; 197 } 198 199 200 /** 201 * Setup an exchange for the refund limit enforcement and initialize the 202 * original refund limit for the exchange. 203 * 204 * @param irc refund context 205 * @param exchange_url base URL of the exchange 206 * @return limiting data structure 207 */ 208 static struct ExchangeLimit * 209 setup_exchange (struct InsertRefundContext *irc, 210 const char *exchange_url) 211 { 212 struct ExchangeLimit *el; 213 214 if (NULL == irc->olc) 215 return NULL; /* no limits */ 216 /* Check if entry exists, if so, do nothing */ 217 if (NULL != 218 (el = find_exchange (irc, 219 exchange_url))) 220 return el; 221 el = GNUNET_new (struct ExchangeLimit); 222 el->exchange_url = GNUNET_strdup (exchange_url); 223 /* olc only lowers, so set to the maximum amount we care about */ 224 el->remaining_refund_limit = *irc->refund; 225 irc->olc (irc->olc_cls, 226 exchange_url, 227 &el->remaining_refund_limit); 228 GNUNET_CONTAINER_DLL_insert (irc->el_head, 229 irc->el_tail, 230 el); 231 return el; 232 } 233 234 235 /** 236 * Lower the remaining refund limit in @a el by @a val. 237 * 238 * @param[in,out] el exchange limit to lower 239 * @param val amount to lower limit by 240 * @return true on success, false on failure 241 */ 242 static bool 243 lower_balance (struct ExchangeLimit *el, 244 const struct TALER_Amount *val) 245 { 246 if (NULL == el) 247 return true; 248 return 0 <= TALER_amount_subtract (&el->remaining_refund_limit, 249 &el->remaining_refund_limit, 250 val); 251 } 252 253 254 /** 255 * Function to be called with the results of a SELECT statement 256 * that has returned @a num_results results. 257 * 258 * @param cls closure, our `struct FindRefundContext` 259 * @param result the postgres result 260 * @param num_results the number of results in @a result 261 */ 262 static void 263 process_refund_cb (void *cls, 264 PGresult *result, 265 unsigned int num_results) 266 { 267 struct FindRefundContext *ictx = cls; 268 269 for (unsigned int i = 0; i<num_results; i++) 270 { 271 /* Sum up existing refunds */ 272 struct TALER_Amount acc; 273 uint64_t rtransaction_id; 274 struct GNUNET_PQ_ResultSpec rs[] = { 275 TALER_PQ_result_spec_amount_with_currency ("refund_amount", 276 &acc), 277 GNUNET_PQ_result_spec_uint64 ("rtransaction_id", 278 &rtransaction_id), 279 GNUNET_PQ_result_spec_end 280 }; 281 282 if (GNUNET_OK != 283 GNUNET_PQ_extract_result (result, 284 rs, 285 i)) 286 { 287 GNUNET_break (0); 288 ictx->err = true; 289 return; 290 } 291 if (GNUNET_OK != 292 TALER_amount_cmp_currency (&ictx->refunded_amount, 293 &acc)) 294 { 295 GNUNET_break (0); 296 ictx->err = true; 297 return; 298 } 299 if (0 > 300 TALER_amount_add (&ictx->refunded_amount, 301 &ictx->refunded_amount, 302 &acc)) 303 { 304 GNUNET_break (0); 305 ictx->err = true; 306 return; 307 } 308 ictx->max_rtransaction_id = GNUNET_MAX (ictx->max_rtransaction_id, 309 rtransaction_id); 310 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 311 "Found refund of %s\n", 312 TALER_amount2s (&acc)); 313 } 314 } 315 316 317 /** 318 * Helper function to prepare statement to select refunds 319 * 320 * @param pg context to prepare statement in 321 * @return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS on success 322 */ 323 static enum GNUNET_DB_QueryStatus 324 prep_select_refund (struct TALER_MERCHANTDB_PostgresContext *pg) 325 { 326 TMH_PQ_prepare_anon (pg, 327 "SELECT" 328 " refund_amount" 329 ",rtransaction_id" 330 " FROM merchant_refunds" 331 " WHERE coin_pub=$1" 332 " AND order_serial=$2"); 333 return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; 334 } 335 336 337 /** 338 * Helper function to prepare statement to insert refund 339 * 340 * @param pg context to prepare statement in 341 * @return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS on success 342 */ 343 static enum GNUNET_DB_QueryStatus 344 prep_insert_refund (struct TALER_MERCHANTDB_PostgresContext *pg) 345 { 346 // FIXME: return 'refund_serial' from this INSERT statement for #10577 347 TMH_PQ_prepare_anon (pg, 348 "INSERT INTO merchant_refunds" 349 "(order_serial" 350 ",rtransaction_id" 351 ",refund_timestamp" 352 ",coin_pub" 353 ",reason" 354 ",refund_amount" 355 ") VALUES" 356 "($1, $2, $3, $4, $5, $6)"); 357 return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; 358 } 359 360 361 /** 362 * Function to be called with the results of a SELECT statement 363 * that has returned @a num_results results. 364 * 365 * @param cls closure, our `struct InsertRefundContext` 366 * @param result the postgres result 367 * @param num_results the number of results in @a result 368 */ 369 static void 370 process_deposits_for_refund_cb (void *cls, 371 PGresult *result, 372 unsigned int num_results) 373 { 374 struct InsertRefundContext *ctx = cls; 375 struct TALER_MERCHANTDB_PostgresContext *pg = ctx->pg; 376 struct TALER_Amount current_refund; 377 struct RefundCoinData rcd[GNUNET_NZL (num_results)]; 378 struct GNUNET_TIME_Timestamp now; 379 380 now = GNUNET_TIME_timestamp_get (); 381 GNUNET_assert (GNUNET_OK == 382 TALER_amount_set_zero (ctx->refund->currency, 383 ¤t_refund)); 384 memset (rcd, 385 0, 386 sizeof (rcd)); 387 /* Pass 1: Collect amount of existing refunds into current_refund. 388 * Also store existing refunded amount for each deposit in deposit_refund. */ 389 for (unsigned int i = 0; i<num_results; i++) 390 { 391 struct RefundCoinData *rcdi = &rcd[i]; 392 struct GNUNET_PQ_ResultSpec rs[] = { 393 GNUNET_PQ_result_spec_auto_from_type ("coin_pub", 394 &rcdi->coin_pub), 395 GNUNET_PQ_result_spec_uint64 ("order_serial", 396 &rcdi->order_serial), 397 GNUNET_PQ_result_spec_string ("exchange_url", 398 &rcdi->exchange_url), 399 TALER_PQ_result_spec_amount_with_currency ("amount_with_fee", 400 &rcdi->deposited_with_fee), 401 GNUNET_PQ_result_spec_end 402 }; 403 struct FindRefundContext ictx = { 404 .pg = pg, 405 }; 406 struct ExchangeLimit *el; 407 408 if (GNUNET_OK != 409 GNUNET_PQ_extract_result (result, 410 rs, 411 i)) 412 { 413 GNUNET_break (0); 414 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 415 goto cleanup; 416 } 417 el = setup_exchange (ctx, 418 rcdi->exchange_url); 419 if (0 != strcmp (rcdi->deposited_with_fee.currency, 420 ctx->refund->currency)) 421 { 422 GNUNET_break_op (0); 423 ctx->rs = TALER_MERCHANTDB_RS_BAD_CURRENCY; 424 goto cleanup; 425 } 426 427 { 428 enum GNUNET_DB_QueryStatus ires; 429 struct GNUNET_PQ_QueryParam params[] = { 430 GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub), 431 GNUNET_PQ_query_param_uint64 (&rcdi->order_serial), 432 GNUNET_PQ_query_param_end 433 }; 434 435 GNUNET_assert (GNUNET_OK == 436 TALER_amount_set_zero ( 437 ctx->refund->currency, 438 &ictx.refunded_amount)); 439 ires = prep_select_refund (pg); 440 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != ires) 441 { 442 GNUNET_break (0); 443 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 444 goto cleanup; 445 } 446 ires = GNUNET_PQ_eval_prepared_multi_select ( 447 pg->conn, 448 "", 449 params, 450 &process_refund_cb, 451 &ictx); 452 if ( (ictx.err) || 453 (GNUNET_DB_STATUS_HARD_ERROR == ires) ) 454 { 455 GNUNET_break (0); 456 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 457 goto cleanup; 458 } 459 if (GNUNET_DB_STATUS_SOFT_ERROR == ires) 460 { 461 ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR; 462 goto cleanup; 463 } 464 } 465 if (0 > 466 TALER_amount_add (¤t_refund, 467 ¤t_refund, 468 &ictx.refunded_amount)) 469 { 470 GNUNET_break (0); 471 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 472 goto cleanup; 473 } 474 rcdi->refund_amount = ictx.refunded_amount; 475 rcdi->max_rtransaction_id = ictx.max_rtransaction_id; 476 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 477 "Existing refund for coin %s is %s\n", 478 TALER_B2S (&rcdi->coin_pub), 479 TALER_amount2s (&ictx.refunded_amount)); 480 GNUNET_break (lower_balance (el, 481 &ictx.refunded_amount)); 482 } /* end for all deposited coins */ 483 484 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 485 "Total existing refund is %s\n", 486 TALER_amount2s (¤t_refund)); 487 488 /* stop immediately if we are 'done' === amount already 489 * refunded. */ 490 if (0 >= TALER_amount_cmp (ctx->refund, 491 ¤t_refund)) 492 { 493 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 494 "Existing refund of %s at or above requested refund. Finished early.\n", 495 TALER_amount2s (¤t_refund)); 496 ctx->rs = TALER_MERCHANTDB_RS_SUCCESS; 497 goto cleanup; 498 } 499 500 /* Phase 2: Try to increase current refund until it matches desired refund */ 501 for (unsigned int i = 0; i<num_results; i++) 502 { 503 struct RefundCoinData *rcdi = &rcd[i]; 504 const struct TALER_Amount *increment; 505 struct TALER_Amount left; 506 struct TALER_Amount remaining_refund; 507 struct ExchangeLimit *el; 508 509 /* How much of the coin is left after the existing refunds? */ 510 if (0 > 511 TALER_amount_subtract (&left, 512 &rcdi->deposited_with_fee, 513 &rcdi->refund_amount)) 514 { 515 GNUNET_break (0); 516 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 517 goto cleanup; 518 } 519 520 if (TALER_amount_is_zero (&left)) 521 { 522 /* coin was fully refunded, move to next coin */ 523 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 524 "Coin %s fully refunded, moving to next coin\n", 525 TALER_B2S (&rcdi->coin_pub)); 526 continue; 527 } 528 el = find_exchange (ctx, 529 rcdi->exchange_url); 530 if ( (NULL != el) && 531 (TALER_amount_is_zero (&el->remaining_refund_limit)) ) 532 { 533 /* legal limit reached, move to next coin */ 534 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 535 "Exchange %s legal limit reached, moving to next coin\n", 536 rcdi->exchange_url); 537 continue; 538 } 539 540 rcdi->max_rtransaction_id++; 541 /* How much of the refund is still to be paid back? */ 542 if (0 > 543 TALER_amount_subtract (&remaining_refund, 544 ctx->refund, 545 ¤t_refund)) 546 { 547 GNUNET_break (0); 548 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 549 goto cleanup; 550 } 551 /* cap by legal limit */ 552 if (NULL != el) 553 { 554 struct TALER_Amount new_limit; 555 556 TALER_amount_min (&new_limit, 557 &remaining_refund, 558 &el->remaining_refund_limit); 559 if (0 != TALER_amount_cmp (&new_limit, 560 &remaining_refund)) 561 { 562 remaining_refund = new_limit; 563 ctx->legal_capped = true; 564 } 565 } 566 /* By how much will we increase the refund for this coin? */ 567 if (0 >= TALER_amount_cmp (&remaining_refund, 568 &left)) 569 { 570 /* remaining_refund <= left */ 571 increment = &remaining_refund; 572 } 573 else 574 { 575 increment = &left; 576 } 577 578 if (0 > 579 TALER_amount_add (¤t_refund, 580 ¤t_refund, 581 increment)) 582 { 583 GNUNET_break (0); 584 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 585 goto cleanup; 586 } 587 GNUNET_break (lower_balance (el, 588 increment)); 589 /* actually run the refund */ 590 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 591 "Coin %s deposit amount is %s\n", 592 TALER_B2S (&rcdi->coin_pub), 593 TALER_amount2s (&rcdi->deposited_with_fee)); 594 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 595 "Coin %s refund will be incremented by %s\n", 596 TALER_B2S (&rcdi->coin_pub), 597 TALER_amount2s (increment)); 598 { 599 enum GNUNET_DB_QueryStatus qs; 600 struct GNUNET_PQ_QueryParam params[] = { 601 GNUNET_PQ_query_param_uint64 (&rcdi->order_serial), 602 GNUNET_PQ_query_param_uint64 (&rcdi->max_rtransaction_id), /* already inc'ed */ 603 GNUNET_PQ_query_param_timestamp (&now), 604 GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub), 605 GNUNET_PQ_query_param_string (ctx->reason), 606 TALER_PQ_query_param_amount_with_currency (pg->conn, 607 increment), 608 GNUNET_PQ_query_param_end 609 }; 610 611 check_connection (pg); 612 qs = prep_insert_refund (pg); 613 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) 614 { 615 GNUNET_break (0); 616 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 617 goto cleanup; 618 } 619 qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, 620 "", 621 params); 622 switch (qs) 623 { 624 case GNUNET_DB_STATUS_HARD_ERROR: 625 GNUNET_break (0); 626 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 627 goto cleanup; 628 case GNUNET_DB_STATUS_SOFT_ERROR: 629 ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR; 630 goto cleanup; 631 default: 632 ctx->rs = (enum TALER_MERCHANTDB_RefundStatus) qs; 633 break; 634 } 635 } 636 637 /* stop immediately if we are done */ 638 if (0 == TALER_amount_cmp (ctx->refund, 639 ¤t_refund)) 640 { 641 ctx->rs = TALER_MERCHANTDB_RS_SUCCESS; 642 goto cleanup; 643 } 644 } 645 646 if (ctx->legal_capped) 647 { 648 ctx->rs = TALER_MERCHANTDB_RS_LEGAL_FAILURE; 649 goto cleanup; 650 } 651 /** 652 * We end up here if not all of the refund has been covered. 653 * Although this should be checked as the business should never 654 * issue a refund bigger than the contract's actual price, we cannot 655 * rely upon the frontend being correct. 656 */ 657 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 658 "The refund of %s is bigger than the order's value\n", 659 TALER_amount2s (ctx->refund)); 660 ctx->rs = TALER_MERCHANTDB_RS_TOO_HIGH; 661 cleanup: 662 for (unsigned int i = 0; i<num_results; i++) 663 GNUNET_free (rcd[i].exchange_url); 664 } 665 666 667 enum TALER_MERCHANTDB_RefundStatus 668 TALER_MERCHANTDB_increase_refund ( 669 struct TALER_MERCHANTDB_PostgresContext *pg, 670 const char *instance_id, 671 const char *order_id, 672 const struct TALER_Amount *refund, 673 TALER_MERCHANTDB_OperationLimitCallback olc, 674 void *olc_cls, 675 const char *reason) 676 { 677 enum GNUNET_DB_QueryStatus qs; 678 struct GNUNET_PQ_QueryParam params[] = { 679 GNUNET_PQ_query_param_string (order_id), 680 GNUNET_PQ_query_param_end 681 }; 682 struct InsertRefundContext ctx = { 683 .pg = pg, 684 .refund = refund, 685 .olc = olc, 686 .olc_cls = olc_cls, 687 .reason = reason, 688 }; 689 690 GNUNET_assert (NULL != pg->current_merchant_id); 691 GNUNET_assert (0 == strcmp (instance_id, 692 pg->current_merchant_id)); 693 TMH_PQ_prepare_anon (pg, 694 "SELECT" 695 " dep.coin_pub" 696 ",dco.order_serial" 697 ",dep.amount_with_fee" 698 ",dco.exchange_url" 699 " FROM merchant_deposits dep" 700 " JOIN merchant_deposit_confirmations dco" 701 " USING (deposit_confirmation_serial)" 702 " WHERE order_serial=" 703 " (SELECT order_serial" 704 " FROM merchant_contract_terms" 705 " WHERE order_id=$1" 706 " AND paid)"); 707 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 708 "Asked to refund %s on order %s\n", 709 TALER_amount2s (refund), 710 order_id); 711 qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, 712 "", 713 params, 714 &process_deposits_for_refund_cb, 715 &ctx); 716 { 717 struct ExchangeLimit *el; 718 719 while (NULL != (el = ctx.el_head)) 720 { 721 GNUNET_CONTAINER_DLL_remove (ctx.el_head, 722 ctx.el_tail, 723 el); 724 GNUNET_free (el->exchange_url); 725 GNUNET_free (el); 726 } 727 } 728 switch (qs) 729 { 730 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 731 /* never paid, means we clearly cannot refund anything */ 732 return TALER_MERCHANTDB_RS_NO_SUCH_ORDER; 733 case GNUNET_DB_STATUS_SOFT_ERROR: 734 return TALER_MERCHANTDB_RS_SOFT_ERROR; 735 case GNUNET_DB_STATUS_HARD_ERROR: 736 return TALER_MERCHANTDB_RS_HARD_ERROR; 737 default: 738 /* Got one or more deposits */ 739 return ctx.rs; 740 } 741 }