pg_increase_refund.c (19970B)
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 backenddb/pg_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 "pg_increase_refund.h" 26 #include "pg_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 PostgresClosure *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 PostgresClosure *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 ( 328 void *cls, 329 PGresult *result, 330 unsigned int num_results) 331 { 332 struct InsertRefundContext *ctx = cls; 333 struct PostgresClosure *pg = ctx->pg; 334 struct TALER_Amount current_refund; 335 struct RefundCoinData rcd[GNUNET_NZL (num_results)]; 336 struct GNUNET_TIME_Timestamp now; 337 338 now = GNUNET_TIME_timestamp_get (); 339 GNUNET_assert (GNUNET_OK == 340 TALER_amount_set_zero (ctx->refund->currency, 341 ¤t_refund)); 342 memset (rcd, 343 0, 344 sizeof (rcd)); 345 /* Pass 1: Collect amount of existing refunds into current_refund. 346 * Also store existing refunded amount for each deposit in deposit_refund. */ 347 for (unsigned int i = 0; i<num_results; i++) 348 { 349 struct RefundCoinData *rcdi = &rcd[i]; 350 struct GNUNET_PQ_ResultSpec rs[] = { 351 GNUNET_PQ_result_spec_auto_from_type ("coin_pub", 352 &rcdi->coin_pub), 353 GNUNET_PQ_result_spec_uint64 ("order_serial", 354 &rcdi->order_serial), 355 GNUNET_PQ_result_spec_string ("exchange_url", 356 &rcdi->exchange_url), 357 TALER_PQ_result_spec_amount_with_currency ("amount_with_fee", 358 &rcdi->deposited_with_fee), 359 GNUNET_PQ_result_spec_end 360 }; 361 struct FindRefundContext ictx = { 362 .pg = pg 363 }; 364 struct ExchangeLimit *el; 365 366 if (GNUNET_OK != 367 GNUNET_PQ_extract_result (result, 368 rs, 369 i)) 370 { 371 GNUNET_break (0); 372 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 373 goto cleanup; 374 } 375 el = setup_exchange (ctx, 376 rcdi->exchange_url); 377 if (0 != strcmp (rcdi->deposited_with_fee.currency, 378 ctx->refund->currency)) 379 { 380 GNUNET_break_op (0); 381 ctx->rs = TALER_MERCHANTDB_RS_BAD_CURRENCY; 382 goto cleanup; 383 } 384 385 { 386 enum GNUNET_DB_QueryStatus ires; 387 struct GNUNET_PQ_QueryParam params[] = { 388 GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub), 389 GNUNET_PQ_query_param_uint64 (&rcdi->order_serial), 390 GNUNET_PQ_query_param_end 391 }; 392 393 GNUNET_assert (GNUNET_OK == 394 TALER_amount_set_zero ( 395 ctx->refund->currency, 396 &ictx.refunded_amount)); 397 ires = GNUNET_PQ_eval_prepared_multi_select ( 398 ctx->pg->conn, 399 "find_refunds_by_coin", 400 params, 401 &process_refund_cb, 402 &ictx); 403 if ( (ictx.err) || 404 (GNUNET_DB_STATUS_HARD_ERROR == ires) ) 405 { 406 GNUNET_break (0); 407 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 408 goto cleanup; 409 } 410 if (GNUNET_DB_STATUS_SOFT_ERROR == ires) 411 { 412 ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR; 413 goto cleanup; 414 } 415 } 416 if (0 > 417 TALER_amount_add (¤t_refund, 418 ¤t_refund, 419 &ictx.refunded_amount)) 420 { 421 GNUNET_break (0); 422 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 423 goto cleanup; 424 } 425 rcdi->refund_amount = ictx.refunded_amount; 426 rcdi->max_rtransaction_id = ictx.max_rtransaction_id; 427 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 428 "Existing refund for coin %s is %s\n", 429 TALER_B2S (&rcdi->coin_pub), 430 TALER_amount2s (&ictx.refunded_amount)); 431 GNUNET_break (lower_balance (el, 432 &ictx.refunded_amount)); 433 } /* end for all deposited coins */ 434 435 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 436 "Total existing refund is %s\n", 437 TALER_amount2s (¤t_refund)); 438 439 /* stop immediately if we are 'done' === amount already 440 * refunded. */ 441 if (0 >= TALER_amount_cmp (ctx->refund, 442 ¤t_refund)) 443 { 444 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 445 "Existing refund of %s at or above requested refund. Finished early.\n", 446 TALER_amount2s (¤t_refund)); 447 ctx->rs = TALER_MERCHANTDB_RS_SUCCESS; 448 goto cleanup; 449 } 450 451 /* Phase 2: Try to increase current refund until it matches desired refund */ 452 for (unsigned int i = 0; i<num_results; i++) 453 { 454 struct RefundCoinData *rcdi = &rcd[i]; 455 const struct TALER_Amount *increment; 456 struct TALER_Amount left; 457 struct TALER_Amount remaining_refund; 458 struct ExchangeLimit *el; 459 460 /* How much of the coin is left after the existing refunds? */ 461 if (0 > 462 TALER_amount_subtract (&left, 463 &rcdi->deposited_with_fee, 464 &rcdi->refund_amount)) 465 { 466 GNUNET_break (0); 467 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 468 goto cleanup; 469 } 470 471 if (TALER_amount_is_zero (&left)) 472 { 473 /* coin was fully refunded, move to next coin */ 474 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 475 "Coin %s fully refunded, moving to next coin\n", 476 TALER_B2S (&rcdi->coin_pub)); 477 continue; 478 } 479 el = find_exchange (ctx, 480 rcdi->exchange_url); 481 if ( (NULL != el) && 482 (TALER_amount_is_zero (&el->remaining_refund_limit)) ) 483 { 484 /* legal limit reached, move to next coin */ 485 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 486 "Exchange %s legal limit reached, moving to next coin\n", 487 rcdi->exchange_url); 488 continue; 489 } 490 491 rcdi->max_rtransaction_id++; 492 /* How much of the refund is still to be paid back? */ 493 if (0 > 494 TALER_amount_subtract (&remaining_refund, 495 ctx->refund, 496 ¤t_refund)) 497 { 498 GNUNET_break (0); 499 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 500 goto cleanup; 501 } 502 /* cap by legal limit */ 503 if (NULL != el) 504 { 505 struct TALER_Amount new_limit; 506 507 TALER_amount_min (&new_limit, 508 &remaining_refund, 509 &el->remaining_refund_limit); 510 if (0 != TALER_amount_cmp (&new_limit, 511 &remaining_refund)) 512 { 513 remaining_refund = new_limit; 514 ctx->legal_capped = true; 515 } 516 } 517 /* By how much will we increase the refund for this coin? */ 518 if (0 >= TALER_amount_cmp (&remaining_refund, 519 &left)) 520 { 521 /* remaining_refund <= left */ 522 increment = &remaining_refund; 523 } 524 else 525 { 526 increment = &left; 527 } 528 529 if (0 > 530 TALER_amount_add (¤t_refund, 531 ¤t_refund, 532 increment)) 533 { 534 GNUNET_break (0); 535 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 536 goto cleanup; 537 } 538 GNUNET_break (lower_balance (el, 539 increment)); 540 /* actually run the refund */ 541 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 542 "Coin %s deposit amount is %s\n", 543 TALER_B2S (&rcdi->coin_pub), 544 TALER_amount2s (&rcdi->deposited_with_fee)); 545 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 546 "Coin %s refund will be incremented by %s\n", 547 TALER_B2S (&rcdi->coin_pub), 548 TALER_amount2s (increment)); 549 { 550 enum GNUNET_DB_QueryStatus qs; 551 struct GNUNET_PQ_QueryParam params[] = { 552 GNUNET_PQ_query_param_uint64 (&rcdi->order_serial), 553 GNUNET_PQ_query_param_uint64 (&rcdi->max_rtransaction_id), /* already inc'ed */ 554 GNUNET_PQ_query_param_timestamp (&now), 555 GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub), 556 GNUNET_PQ_query_param_string (ctx->reason), 557 TALER_PQ_query_param_amount_with_currency (pg->conn, 558 increment), 559 GNUNET_PQ_query_param_end 560 }; 561 562 check_connection (pg); 563 qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, 564 "insert_refund", 565 params); 566 switch (qs) 567 { 568 case GNUNET_DB_STATUS_HARD_ERROR: 569 GNUNET_break (0); 570 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 571 goto cleanup; 572 case GNUNET_DB_STATUS_SOFT_ERROR: 573 ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR; 574 goto cleanup; 575 default: 576 ctx->rs = (enum TALER_MERCHANTDB_RefundStatus) qs; 577 break; 578 } 579 } 580 581 /* stop immediately if we are done */ 582 if (0 == TALER_amount_cmp (ctx->refund, 583 ¤t_refund)) 584 { 585 ctx->rs = TALER_MERCHANTDB_RS_SUCCESS; 586 goto cleanup; 587 } 588 } 589 590 if (ctx->legal_capped) 591 { 592 ctx->rs = TALER_MERCHANTDB_RS_LEGAL_FAILURE; 593 goto cleanup; 594 } 595 /** 596 * We end up here if not all of the refund has been covered. 597 * Although this should be checked as the business should never 598 * issue a refund bigger than the contract's actual price, we cannot 599 * rely upon the frontend being correct. 600 */ 601 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 602 "The refund of %s is bigger than the order's value\n", 603 TALER_amount2s (ctx->refund)); 604 ctx->rs = TALER_MERCHANTDB_RS_TOO_HIGH; 605 cleanup: 606 for (unsigned int i = 0; i<num_results; i++) 607 GNUNET_free (rcd[i].exchange_url); 608 } 609 610 611 enum TALER_MERCHANTDB_RefundStatus 612 TMH_PG_increase_refund ( 613 void *cls, 614 const char *instance_id, 615 const char *order_id, 616 const struct TALER_Amount *refund, 617 TALER_MERCHANTDB_OperationLimitCallback olc, 618 void *olc_cls, 619 const char *reason) 620 { 621 struct PostgresClosure *pg = cls; 622 enum GNUNET_DB_QueryStatus qs; 623 struct GNUNET_PQ_QueryParam params[] = { 624 GNUNET_PQ_query_param_string (instance_id), 625 GNUNET_PQ_query_param_string (order_id), 626 GNUNET_PQ_query_param_end 627 }; 628 struct InsertRefundContext ctx = { 629 .pg = pg, 630 .refund = refund, 631 .olc = olc, 632 .olc_cls = olc_cls, 633 .reason = reason 634 }; 635 636 // FIXME: return 'refund_serial' from this INSERT statement for #10577 637 PREPARE (pg, 638 "insert_refund", 639 "INSERT INTO merchant_refunds" 640 "(order_serial" 641 ",rtransaction_id" 642 ",refund_timestamp" 643 ",coin_pub" 644 ",reason" 645 ",refund_amount" 646 ") VALUES" 647 "($1, $2, $3, $4, $5, $6)"); 648 PREPARE (pg, 649 "find_refunds_by_coin", 650 "SELECT" 651 " refund_amount" 652 ",rtransaction_id" 653 " FROM merchant_refunds" 654 " WHERE coin_pub=$1" 655 " AND order_serial=$2"); 656 PREPARE (pg, 657 "find_deposits_for_refund", 658 "SELECT" 659 " dep.coin_pub" 660 ",dco.order_serial" 661 ",dep.amount_with_fee" 662 ",dco.exchange_url" 663 " FROM merchant_deposits dep" 664 " JOIN merchant_deposit_confirmations dco" 665 " USING (deposit_confirmation_serial)" 666 " WHERE order_serial=" 667 " (SELECT order_serial" 668 " FROM merchant_contract_terms" 669 " WHERE order_id=$2" 670 " AND paid" 671 " AND merchant_serial=" 672 " (SELECT merchant_serial" 673 " FROM merchant_instances" 674 " WHERE merchant_id=$1))"); 675 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 676 "Asked to refund %s on order %s\n", 677 TALER_amount2s (refund), 678 order_id); 679 qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, 680 "find_deposits_for_refund", 681 params, 682 &process_deposits_for_refund_cb, 683 &ctx); 684 { 685 struct ExchangeLimit *el; 686 687 while (NULL != (el = ctx.el_head)) 688 { 689 GNUNET_CONTAINER_DLL_remove (ctx.el_head, 690 ctx.el_tail, 691 el); 692 GNUNET_free (el->exchange_url); 693 GNUNET_free (el); 694 } 695 } 696 switch (qs) 697 { 698 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 699 /* never paid, means we clearly cannot refund anything */ 700 return TALER_MERCHANTDB_RS_NO_SUCH_ORDER; 701 case GNUNET_DB_STATUS_SOFT_ERROR: 702 return TALER_MERCHANTDB_RS_SOFT_ERROR; 703 case GNUNET_DB_STATUS_HARD_ERROR: 704 return TALER_MERCHANTDB_RS_HARD_ERROR; 705 default: 706 /* Got one or more deposits */ 707 return ctx.rs; 708 } 709 }