do_increase_refund.c (22051B)
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/do_increase_refund.c 18 * @brief Implementation of the do_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/do_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 " ON CONFLICT (order_serial" 358 ",coin_pub" 359 ",rtransaction_id)" 360 " DO NOTHING"); 361 return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; 362 } 363 364 365 /** 366 * Function to be called with the results of a SELECT statement 367 * that has returned @a num_results results. 368 * 369 * @param cls closure, our `struct InsertRefundContext` 370 * @param result the postgres result 371 * @param num_results the number of results in @a result 372 */ 373 static void 374 process_deposits_for_refund_cb (void *cls, 375 PGresult *result, 376 unsigned int num_results) 377 { 378 struct InsertRefundContext *ctx = cls; 379 struct TALER_MERCHANTDB_PostgresContext *pg = ctx->pg; 380 struct TALER_Amount current_refund; 381 struct RefundCoinData rcd[GNUNET_NZL (num_results)]; 382 struct GNUNET_TIME_Timestamp now; 383 384 now = GNUNET_TIME_timestamp_get (); 385 GNUNET_assert (GNUNET_OK == 386 TALER_amount_set_zero (ctx->refund->currency, 387 ¤t_refund)); 388 memset (rcd, 389 0, 390 sizeof (rcd)); 391 /* Pass 1: Collect amount of existing refunds into current_refund. 392 * Also store existing refunded amount for each deposit in deposit_refund. */ 393 for (unsigned int i = 0; i<num_results; i++) 394 { 395 struct RefundCoinData *rcdi = &rcd[i]; 396 struct GNUNET_PQ_ResultSpec rs[] = { 397 GNUNET_PQ_result_spec_auto_from_type ("coin_pub", 398 &rcdi->coin_pub), 399 GNUNET_PQ_result_spec_uint64 ("order_serial", 400 &rcdi->order_serial), 401 GNUNET_PQ_result_spec_string ("exchange_url", 402 &rcdi->exchange_url), 403 TALER_PQ_result_spec_amount_with_currency ("amount_with_fee", 404 &rcdi->deposited_with_fee), 405 GNUNET_PQ_result_spec_end 406 }; 407 struct FindRefundContext ictx = { 408 .pg = pg, 409 }; 410 struct ExchangeLimit *el; 411 412 if (GNUNET_OK != 413 GNUNET_PQ_extract_result (result, 414 rs, 415 i)) 416 { 417 GNUNET_break (0); 418 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 419 goto cleanup; 420 } 421 el = setup_exchange (ctx, 422 rcdi->exchange_url); 423 if (GNUNET_YES != 424 TALER_amount_cmp_currency (&rcdi->deposited_with_fee, 425 ctx->refund)) 426 { 427 GNUNET_break_op (0); 428 ctx->rs = TALER_MERCHANTDB_RS_BAD_CURRENCY; 429 goto cleanup; 430 } 431 432 { 433 enum GNUNET_DB_QueryStatus ires; 434 struct GNUNET_PQ_QueryParam params[] = { 435 GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub), 436 GNUNET_PQ_query_param_uint64 (&rcdi->order_serial), 437 GNUNET_PQ_query_param_end 438 }; 439 440 GNUNET_assert (GNUNET_OK == 441 TALER_amount_set_zero ( 442 ctx->refund->currency, 443 &ictx.refunded_amount)); 444 ires = prep_select_refund (pg); 445 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != ires) 446 { 447 GNUNET_break (0); 448 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 449 goto cleanup; 450 } 451 ires = GNUNET_PQ_eval_prepared_multi_select ( 452 pg->conn, 453 "", 454 params, 455 &process_refund_cb, 456 &ictx); 457 if ( (ictx.err) || 458 (GNUNET_DB_STATUS_HARD_ERROR == ires) ) 459 { 460 GNUNET_break (0); 461 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 462 goto cleanup; 463 } 464 if (GNUNET_DB_STATUS_SOFT_ERROR == ires) 465 { 466 ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR; 467 goto cleanup; 468 } 469 } 470 if (0 > 471 TALER_amount_add (¤t_refund, 472 ¤t_refund, 473 &ictx.refunded_amount)) 474 { 475 GNUNET_break (0); 476 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 477 goto cleanup; 478 } 479 rcdi->refund_amount = ictx.refunded_amount; 480 rcdi->max_rtransaction_id = ictx.max_rtransaction_id; 481 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 482 "Existing refund for coin %s is %s\n", 483 TALER_B2S (&rcdi->coin_pub), 484 TALER_amount2s (&ictx.refunded_amount)); 485 GNUNET_break (lower_balance (el, 486 &ictx.refunded_amount)); 487 } /* end for all deposited coins */ 488 489 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 490 "Total existing refund is %s\n", 491 TALER_amount2s (¤t_refund)); 492 493 /* stop immediately if we are 'done' === amount already 494 * refunded. */ 495 if (0 >= TALER_amount_cmp (ctx->refund, 496 ¤t_refund)) 497 { 498 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 499 "Existing refund of %s at or above requested refund. Finished early.\n", 500 TALER_amount2s (¤t_refund)); 501 ctx->rs = TALER_MERCHANTDB_RS_SUCCESS; 502 goto cleanup; 503 } 504 505 /* Phase 2: Try to increase current refund until it matches desired refund */ 506 for (unsigned int i = 0; i<num_results; i++) 507 { 508 struct RefundCoinData *rcdi = &rcd[i]; 509 const struct TALER_Amount *increment; 510 struct TALER_Amount left; 511 struct TALER_Amount remaining_refund; 512 struct ExchangeLimit *el; 513 514 /* How much of the coin is left after the existing refunds? */ 515 if (0 > 516 TALER_amount_subtract (&left, 517 &rcdi->deposited_with_fee, 518 &rcdi->refund_amount)) 519 { 520 GNUNET_break (0); 521 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 522 goto cleanup; 523 } 524 525 if (TALER_amount_is_zero (&left)) 526 { 527 /* coin was fully refunded, move to next coin */ 528 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 529 "Coin %s fully refunded, moving to next coin\n", 530 TALER_B2S (&rcdi->coin_pub)); 531 continue; 532 } 533 el = find_exchange (ctx, 534 rcdi->exchange_url); 535 if ( (NULL != el) && 536 (TALER_amount_is_zero (&el->remaining_refund_limit)) ) 537 { 538 /* legal limit reached, move to next coin */ 539 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 540 "Exchange %s legal limit reached, moving to next coin\n", 541 rcdi->exchange_url); 542 continue; 543 } 544 545 rcdi->max_rtransaction_id++; 546 /* How much of the refund is still to be paid back? */ 547 if (0 > 548 TALER_amount_subtract (&remaining_refund, 549 ctx->refund, 550 ¤t_refund)) 551 { 552 GNUNET_break (0); 553 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 554 goto cleanup; 555 } 556 /* cap by legal limit */ 557 if (NULL != el) 558 { 559 struct TALER_Amount new_limit; 560 561 TALER_amount_min (&new_limit, 562 &remaining_refund, 563 &el->remaining_refund_limit); 564 if (0 != TALER_amount_cmp (&new_limit, 565 &remaining_refund)) 566 { 567 remaining_refund = new_limit; 568 ctx->legal_capped = true; 569 } 570 } 571 /* By how much will we increase the refund for this coin? */ 572 if (0 >= TALER_amount_cmp (&remaining_refund, 573 &left)) 574 { 575 /* remaining_refund <= left */ 576 increment = &remaining_refund; 577 } 578 else 579 { 580 increment = &left; 581 } 582 583 if (0 > 584 TALER_amount_add (¤t_refund, 585 ¤t_refund, 586 increment)) 587 { 588 GNUNET_break (0); 589 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 590 goto cleanup; 591 } 592 GNUNET_break (lower_balance (el, 593 increment)); 594 /* actually run the refund */ 595 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 596 "Coin %s deposit amount is %s\n", 597 TALER_B2S (&rcdi->coin_pub), 598 TALER_amount2s (&rcdi->deposited_with_fee)); 599 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 600 "Coin %s refund will be incremented by %s\n", 601 TALER_B2S (&rcdi->coin_pub), 602 TALER_amount2s (increment)); 603 { 604 enum GNUNET_DB_QueryStatus qs; 605 struct GNUNET_PQ_QueryParam params[] = { 606 GNUNET_PQ_query_param_uint64 (&rcdi->order_serial), 607 GNUNET_PQ_query_param_uint64 (&rcdi->max_rtransaction_id), /* already inc'ed */ 608 GNUNET_PQ_query_param_timestamp (&now), 609 GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub), 610 GNUNET_PQ_query_param_string (ctx->reason), 611 TALER_PQ_query_param_amount_with_currency (pg->conn, 612 increment), 613 GNUNET_PQ_query_param_end 614 }; 615 616 qs = prep_insert_refund (pg); 617 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) 618 { 619 GNUNET_break (0); 620 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 621 goto cleanup; 622 } 623 qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, 624 "", 625 params); 626 switch (qs) 627 { 628 case GNUNET_DB_STATUS_HARD_ERROR: 629 GNUNET_break (0); 630 ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; 631 goto cleanup; 632 case GNUNET_DB_STATUS_SOFT_ERROR: 633 ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR; 634 goto cleanup; 635 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 636 /* Another transaction concurrently created a refund with the 637 very rtransaction_id we picked (we saw its predecessors, but 638 not it). Without the ON CONFLICT above this would raise 639 23505, which maps to 0 == #TALER_MERCHANTDB_RS_NO_SUCH_ORDER 640 (a bogus 404 for an order we just read) and additionally 641 poisons the enclosing transaction. Report a serialization 642 failure instead, so that the caller retries and then observes 643 the concurrent refund. */ 644 ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR; 645 goto cleanup; 646 default: 647 ctx->rs = (enum TALER_MERCHANTDB_RefundStatus) qs; 648 break; 649 } 650 } 651 652 /* stop immediately if we are done */ 653 if (0 == TALER_amount_cmp (ctx->refund, 654 ¤t_refund)) 655 { 656 ctx->rs = TALER_MERCHANTDB_RS_SUCCESS; 657 goto cleanup; 658 } 659 } 660 661 if (ctx->legal_capped) 662 { 663 ctx->rs = TALER_MERCHANTDB_RS_LEGAL_FAILURE; 664 goto cleanup; 665 } 666 /** 667 * We end up here if not all of the refund has been covered. 668 * Although this should be checked as the business should never 669 * issue a refund bigger than the contract's actual price, we cannot 670 * rely upon the frontend being correct. 671 */ 672 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 673 "The refund of %s is bigger than the order's value\n", 674 TALER_amount2s (ctx->refund)); 675 ctx->rs = TALER_MERCHANTDB_RS_TOO_HIGH; 676 cleanup: 677 for (unsigned int i = 0; i<num_results; i++) 678 GNUNET_free (rcd[i].exchange_url); 679 } 680 681 682 enum TALER_MERCHANTDB_RefundStatus 683 TALER_MERCHANTDB_do_increase_refund ( 684 struct TALER_MERCHANTDB_PostgresContext *pg, 685 const char *instance_id, 686 const char *order_id, 687 const struct TALER_Amount *refund, 688 TALER_MERCHANTDB_OperationLimitCallback olc, 689 void *olc_cls, 690 const char *reason) 691 { 692 enum GNUNET_DB_QueryStatus qs; 693 struct GNUNET_PQ_QueryParam params[] = { 694 GNUNET_PQ_query_param_string (order_id), 695 GNUNET_PQ_query_param_end 696 }; 697 struct InsertRefundContext ctx = { 698 .pg = pg, 699 .refund = refund, 700 .olc = olc, 701 .olc_cls = olc_cls, 702 .reason = reason, 703 }; 704 705 GNUNET_assert (NULL != pg->current_merchant_id); 706 GNUNET_assert (0 == strcmp (instance_id, 707 pg->current_merchant_id)); 708 TMH_PQ_prepare_anon (pg, 709 "SELECT" 710 " dep.coin_pub" 711 ",dco.order_serial" 712 ",dep.amount_with_fee" 713 ",dco.exchange_url" 714 " FROM merchant_deposits dep" 715 " JOIN merchant_deposit_confirmations dco" 716 " USING (deposit_confirmation_serial)" 717 " WHERE order_serial=" 718 " (SELECT order_serial" 719 " FROM merchant_contract_terms" 720 " WHERE order_id=$1" 721 " AND paid)"); 722 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 723 "Asked to refund %s on order %s\n", 724 TALER_amount2s (refund), 725 order_id); 726 qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, 727 "", 728 params, 729 &process_deposits_for_refund_cb, 730 &ctx); 731 { 732 struct ExchangeLimit *el; 733 734 while (NULL != (el = ctx.el_head)) 735 { 736 GNUNET_CONTAINER_DLL_remove (ctx.el_head, 737 ctx.el_tail, 738 el); 739 GNUNET_free (el->exchange_url); 740 GNUNET_free (el); 741 } 742 } 743 switch (qs) 744 { 745 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 746 /* never paid, means we clearly cannot refund anything */ 747 return TALER_MERCHANTDB_RS_NO_SUCH_ORDER; 748 case GNUNET_DB_STATUS_SOFT_ERROR: 749 return TALER_MERCHANTDB_RS_SOFT_ERROR; 750 case GNUNET_DB_STATUS_HARD_ERROR: 751 return TALER_MERCHANTDB_RS_HARD_ERROR; 752 default: 753 /* Got one or more deposits */ 754 return ctx.rs; 755 } 756 }