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