taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c (19269B)
1 /* 2 This file is part of TALER 3 (C) 2014-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 Affero 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/backend/taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c 18 * @brief Handle request to increase the refund for an order 19 * @author Marcello Stanisci 20 * @author Christian Grothoff 21 */ 22 #include "platform.h" 23 #include <jansson.h> 24 #include <taler/taler_dbevents.h> 25 #include <taler/taler_signatures.h> 26 #include <taler/taler_json_lib.h> 27 #include "taler-merchant-httpd_exchanges.h" 28 #include "taler-merchant-httpd_post-private-orders-ORDER_ID-refund.h" 29 #include "taler-merchant-httpd_get-private-orders.h" 30 #include "taler-merchant-httpd_helper.h" 31 #include "taler-merchant-httpd_get-exchanges.h" 32 #include "merchant-database/do_increase_refund.h" 33 #include "merchant-database/get_contract_terms_status.h" 34 #include "merchant-database/get_order_summary.h" 35 #include "merchant-database/start.h" 36 #include "merchant-database/preflight.h" 37 #include "merchant-database/event_notify.h" 38 39 /** 40 * How often do we retry the non-trivial refund INSERT database 41 * transaction? 42 */ 43 #define MAX_RETRIES 5 44 45 46 /** 47 * Use database to notify other clients about the 48 * @a order_id being refunded 49 * 50 * @param hc handler context we operate in 51 * @param amount the (total) refunded amount 52 */ 53 static void 54 trigger_refund_notification ( 55 struct TMH_HandlerContext *hc, 56 const struct TALER_Amount *amount) 57 { 58 { 59 const char *as; 60 struct TMH_OrderRefundEventP refund_eh = { 61 .header.size = htons (sizeof (refund_eh)), 62 .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND), 63 .merchant_pub = hc->instance->merchant_pub 64 }; 65 66 /* Resume clients that may wait for this refund */ 67 as = TALER_amount2s (amount); 68 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 69 "Awakening clients on %s waiting for refund of no more than %s\n", 70 hc->infix, 71 as); 72 GNUNET_CRYPTO_hash (hc->infix, 73 strlen (hc->infix), 74 &refund_eh.h_order_id); 75 TALER_MERCHANTDB_event_notify (TMH_db, 76 &refund_eh.header, 77 as, 78 strlen (as)); 79 } 80 { 81 struct TMH_OrderPayEventP pay_eh = { 82 .header.size = htons (sizeof (pay_eh)), 83 .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), 84 .merchant_pub = hc->instance->merchant_pub 85 }; 86 87 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 88 "Notifying clients about status change of order %s\n", 89 hc->infix); 90 GNUNET_CRYPTO_hash (hc->infix, 91 strlen (hc->infix), 92 &pay_eh.h_order_id); 93 TALER_MERCHANTDB_event_notify (TMH_db, 94 &pay_eh.header, 95 NULL, 96 0); 97 } 98 } 99 100 101 /** 102 * Make a taler://refund URI 103 * 104 * @param connection MHD connection to take host and path from 105 * @param instance_id merchant's instance ID, must not be NULL 106 * @param order_id order ID to show a refund for, must not be NULL 107 * @returns the URI, must be freed with #GNUNET_free 108 */ 109 static char * 110 make_taler_refund_uri (struct MHD_Connection *connection, 111 const char *instance_id, 112 const char *order_id) 113 { 114 struct GNUNET_Buffer buf; 115 116 GNUNET_assert (NULL != instance_id); 117 GNUNET_assert (NULL != order_id); 118 if (GNUNET_OK != 119 TMH_taler_uri_by_connection (connection, 120 "refund", 121 instance_id, 122 &buf)) 123 { 124 GNUNET_break (0); 125 return NULL; 126 } 127 GNUNET_buffer_write_path (&buf, 128 order_id); 129 GNUNET_buffer_write_path (&buf, 130 ""); /* Trailing slash */ 131 return GNUNET_buffer_reap_str (&buf); 132 } 133 134 135 /** 136 * Wrapper around #TMH_EXCHANGES_get_limit() that 137 * determines the refund limit for a given @a exchange_url 138 * 139 * @param cls unused 140 * @param exchange_url base URL of the exchange to get 141 * the refund limit for 142 * @param[in,out] amount lowered to the maximum refund 143 * allowed at the exchange 144 */ 145 static void 146 get_refund_limit (void *cls, 147 const char *exchange_url, 148 struct TALER_Amount *amount) 149 { 150 (void) cls; 151 TMH_EXCHANGES_get_limit (exchange_url, 152 TALER_KYCLOGIC_KYC_TRIGGER_REFUND, 153 amount); 154 } 155 156 157 /** 158 * Handle request for increasing the refund associated with 159 * a contract. 160 * 161 * @param rh context of the handler 162 * @param connection the MHD connection to handle 163 * @param[in,out] hc context with further information about the request 164 * @return MHD result code 165 */ 166 enum MHD_Result 167 TMH_private_post_orders_ID_refund ( 168 const struct TMH_RequestHandler *rh, 169 struct MHD_Connection *connection, 170 struct TMH_HandlerContext *hc) 171 { 172 struct TALER_Amount refund; 173 const char *reason; 174 struct GNUNET_JSON_Specification spec[] = { 175 TALER_JSON_spec_amount_any ("refund", 176 &refund), 177 GNUNET_JSON_spec_string ("reason", 178 &reason), 179 GNUNET_JSON_spec_end () 180 }; 181 enum TALER_MERCHANTDB_RefundStatus rs; 182 struct TALER_PrivateContractHashP h_contract; 183 json_t *contract_terms; 184 struct GNUNET_TIME_Timestamp timestamp; 185 struct TALER_Amount order_total; 186 int16_t choice_index; 187 188 { 189 enum GNUNET_GenericReturnValue res; 190 191 res = TALER_MHD_parse_json_data (connection, 192 hc->request_body, 193 spec); 194 if (GNUNET_OK != res) 195 { 196 return (GNUNET_NO == res) 197 ? MHD_YES 198 : MHD_NO; 199 } 200 } 201 202 { 203 enum GNUNET_DB_QueryStatus qs; 204 uint64_t order_serial; 205 bool paid; 206 bool wired; 207 bool session_matches; 208 struct GNUNET_TIME_Timestamp refund_deadline; 209 struct GNUNET_TIME_Timestamp wire_deadline; 210 211 qs = TALER_MERCHANTDB_get_contract_terms_status (TMH_db, 212 hc->instance->settings.id, 213 hc->infix, 214 NULL, 215 &contract_terms, 216 &order_serial, 217 &paid, 218 &wired, 219 &session_matches, 220 NULL, 221 &choice_index); 222 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) 223 { 224 if (qs < 0) 225 { 226 GNUNET_break (0); 227 return TALER_MHD_reply_with_error ( 228 connection, 229 MHD_HTTP_INTERNAL_SERVER_ERROR, 230 TALER_EC_GENERIC_DB_FETCH_FAILED, 231 "get_contract_terms"); 232 } 233 return TALER_MHD_reply_with_error ( 234 connection, 235 MHD_HTTP_NOT_FOUND, 236 TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, 237 hc->infix); 238 } 239 if (GNUNET_OK != 240 TALER_JSON_contract_hash (contract_terms, 241 &h_contract)) 242 { 243 GNUNET_break (0); 244 json_decref (contract_terms); 245 return TALER_MHD_reply_with_error ( 246 connection, 247 MHD_HTTP_INTERNAL_SERVER_ERROR, 248 TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, 249 "Could not hash contract terms"); 250 } 251 { 252 struct GNUNET_JSON_Specification cspec[] = { 253 GNUNET_JSON_spec_timestamp ("refund_deadline", 254 &refund_deadline), 255 GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", 256 &wire_deadline), 257 GNUNET_JSON_spec_timestamp ("timestamp", 258 ×tamp), 259 GNUNET_JSON_spec_end () 260 }; 261 262 if (GNUNET_YES != 263 GNUNET_JSON_parse (contract_terms, 264 cspec, 265 NULL, NULL)) 266 { 267 GNUNET_break (0); 268 json_decref (contract_terms); 269 return TALER_MHD_reply_with_error ( 270 connection, 271 MHD_HTTP_INTERNAL_SERVER_ERROR, 272 TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, 273 "mandatory fields missing"); 274 } 275 if (GNUNET_TIME_timestamp_cmp (timestamp, 276 ==, 277 refund_deadline)) 278 { 279 /* refund was never allowed, so we should refuse hard */ 280 json_decref (contract_terms); 281 return TALER_MHD_reply_with_error ( 282 connection, 283 MHD_HTTP_FORBIDDEN, 284 TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT, 285 NULL); 286 } 287 if (GNUNET_TIME_absolute_is_past (refund_deadline.abs_time)) 288 { 289 /* it is too late for refunds */ 290 /* NOTE: We MAY still be lucky that the exchange did not yet 291 wire the funds, so we will try to give the refund anyway */ 292 } 293 if (GNUNET_TIME_absolute_is_past (wire_deadline.abs_time)) 294 { 295 /* it is *really* too late for refunds */ 296 return TALER_MHD_reply_with_error ( 297 connection, 298 MHD_HTTP_GONE, 299 TALER_EC_MERCHANT_PRIVATE_POST_REFUND_AFTER_WIRE_DEADLINE, 300 NULL); 301 } 302 } 303 } 304 305 TALER_MERCHANTDB_preflight (TMH_db); 306 for (unsigned int i = 0; i<MAX_RETRIES; i++) 307 { 308 struct TALER_Amount external_total = {0}; 309 bool external_mismatch = false; 310 311 if (GNUNET_OK != 312 TALER_MERCHANTDB_start (TMH_db, 313 "increase refund")) 314 { 315 GNUNET_break (0); 316 json_decref (contract_terms); 317 return TALER_MHD_reply_with_error (connection, 318 MHD_HTTP_INTERNAL_SERVER_ERROR, 319 TALER_EC_GENERIC_DB_START_FAILED, 320 NULL); 321 } 322 { 323 enum GNUNET_DB_QueryStatus eqs; 324 325 eqs = TALER_MERCHANTDB_get_external_refunds_total ( 326 TMH_db, 327 hc->instance->settings.id, 328 hc->infix, 329 &external_total, 330 &external_mismatch); 331 if (0 > eqs) 332 { 333 TALER_MERCHANTDB_rollback (TMH_db); 334 if (GNUNET_DB_STATUS_SOFT_ERROR == eqs) 335 continue; 336 json_decref (contract_terms); 337 return TALER_MHD_reply_with_error (connection, 338 MHD_HTTP_INTERNAL_SERVER_ERROR, 339 TALER_EC_GENERIC_DB_FETCH_FAILED, 340 "select external refunds"); 341 } 342 } 343 if (external_mismatch) 344 { 345 GNUNET_break (0); 346 TALER_MERCHANTDB_rollback (TMH_db); 347 json_decref (contract_terms); 348 return TALER_MHD_reply_with_error ( 349 connection, 350 MHD_HTTP_INTERNAL_SERVER_ERROR, 351 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 352 "external refund currencies do not match"); 353 } 354 if (TALER_amount_is_valid (&external_total)) 355 { 356 struct TALER_Amount remaining; 357 358 /* External refunds only exist for paid orders, so at this point a 359 choice was selected and the order total is well-defined. */ 360 if (GNUNET_OK != 361 TMH_compute_order_total (contract_terms, 362 choice_index, 363 &order_total)) 364 { 365 TALER_MERCHANTDB_rollback (TMH_db); 366 json_decref (contract_terms); 367 return TALER_MHD_reply_with_error ( 368 connection, 369 MHD_HTTP_INTERNAL_SERVER_ERROR, 370 TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, 371 "amount"); 372 } 373 if (0 > 374 TALER_amount_subtract (&remaining, 375 &order_total, 376 &external_total)) 377 { 378 GNUNET_break (0); 379 TALER_MERCHANTDB_rollback (TMH_db); 380 json_decref (contract_terms); 381 return TALER_MHD_reply_with_error ( 382 connection, 383 MHD_HTTP_INTERNAL_SERVER_ERROR, 384 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 385 "external refunds exceed order total"); 386 } 387 if (1 == 388 TALER_amount_cmp (&refund, 389 &remaining)) 390 { 391 TALER_MERCHANTDB_rollback (TMH_db); 392 GNUNET_log ( 393 GNUNET_ERROR_TYPE_WARNING, 394 "Refusing Taler refund of %s that would exceed remaining refundable amount of %s\n", 395 TALER_amount2s (&refund), 396 TALER_amount2s (&remaining)); 397 json_decref (contract_terms); 398 return TALER_MHD_reply_with_error ( 399 connection, 400 MHD_HTTP_CONFLICT, 401 TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT, 402 "Taler and external refunds would exceed the order total"); 403 } 404 } 405 rs = TALER_MERCHANTDB_do_increase_refund (TMH_db, 406 hc->instance->settings.id, 407 hc->infix, 408 &refund, 409 &get_refund_limit, 410 NULL, 411 reason); 412 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 413 "increase refund returned %d\n", 414 rs); 415 if (TALER_MERCHANTDB_RS_SUCCESS != rs) 416 TALER_MERCHANTDB_rollback (TMH_db); 417 if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs) 418 continue; 419 if (TALER_MERCHANTDB_RS_SUCCESS == rs) 420 { 421 enum GNUNET_DB_QueryStatus qs; 422 json_t *rargs; 423 424 rargs = GNUNET_JSON_PACK ( 425 GNUNET_JSON_pack_timestamp ("timestamp", 426 timestamp), 427 GNUNET_JSON_pack_string ("order_id", 428 hc->infix), 429 GNUNET_JSON_pack_object_incref ("contract_terms", 430 contract_terms), 431 TALER_JSON_pack_amount ("refund_amount", 432 &refund), 433 GNUNET_JSON_pack_string ("reason", 434 reason) 435 ); 436 GNUNET_assert (NULL != rargs); 437 qs = TMH_trigger_webhook ( 438 hc->instance->settings.id, 439 "refund", 440 rargs); 441 json_decref (rargs); 442 switch (qs) 443 { 444 case GNUNET_DB_STATUS_HARD_ERROR: 445 GNUNET_break (0); 446 TALER_MERCHANTDB_rollback (TMH_db); 447 rs = TALER_MERCHANTDB_RS_HARD_ERROR; 448 break; 449 case GNUNET_DB_STATUS_SOFT_ERROR: 450 TALER_MERCHANTDB_rollback (TMH_db); 451 continue; 452 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 453 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 454 qs = TALER_MERCHANTDB_commit (TMH_db); 455 break; 456 } 457 if (GNUNET_DB_STATUS_HARD_ERROR == qs) 458 { 459 GNUNET_break (0); 460 rs = TALER_MERCHANTDB_RS_HARD_ERROR; 461 break; 462 } 463 if (GNUNET_DB_STATUS_SOFT_ERROR == qs) 464 continue; 465 trigger_refund_notification (hc, 466 &refund); 467 } 468 break; 469 } /* retries loop */ 470 json_decref (contract_terms); 471 472 switch (rs) 473 { 474 case TALER_MERCHANTDB_RS_LEGAL_FAILURE: 475 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 476 "Refund amount %s exceeded legal limits of the exchanges involved\n", 477 TALER_amount2s (&refund)); 478 return TALER_MHD_reply_with_error ( 479 connection, 480 MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, 481 TALER_EC_MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION, 482 NULL); 483 case TALER_MERCHANTDB_RS_BAD_CURRENCY: 484 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 485 "Refund amount %s is not in the currency of the original payment\n", 486 TALER_amount2s (&refund)); 487 return TALER_MHD_reply_with_error ( 488 connection, 489 MHD_HTTP_CONFLICT, 490 TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, 491 "Order was paid in a different currency"); 492 case TALER_MERCHANTDB_RS_TOO_HIGH: 493 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 494 "Refusing refund amount %s that is larger than original payment\n", 495 TALER_amount2s (&refund)); 496 return TALER_MHD_reply_with_error ( 497 connection, 498 MHD_HTTP_CONFLICT, 499 TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT, 500 "Amount above payment"); 501 case TALER_MERCHANTDB_RS_SOFT_ERROR: 502 case TALER_MERCHANTDB_RS_HARD_ERROR: 503 return TALER_MHD_reply_with_error ( 504 connection, 505 MHD_HTTP_INTERNAL_SERVER_ERROR, 506 TALER_EC_GENERIC_DB_COMMIT_FAILED, 507 NULL); 508 case TALER_MERCHANTDB_RS_NO_SUCH_ORDER: 509 /* We know the order exists from the 510 "get_contract_terms" at the beginning; 511 so if we get 'no such order' here, it 512 must be read as "no PAID order" */ 513 return TALER_MHD_reply_with_error ( 514 connection, 515 MHD_HTTP_CONFLICT, 516 TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID, 517 hc->infix); 518 case TALER_MERCHANTDB_RS_SUCCESS: 519 /* continued below */ 520 break; 521 } /* end switch */ 522 523 { 524 uint64_t order_serial; 525 enum GNUNET_DB_QueryStatus qs; 526 527 qs = TALER_MERCHANTDB_get_order_summary (TMH_db, 528 hc->instance->settings.id, 529 hc->infix, 530 ×tamp, 531 &order_serial); 532 if (0 >= qs) 533 { 534 GNUNET_break (0); 535 return TALER_MHD_reply_with_error ( 536 connection, 537 MHD_HTTP_INTERNAL_SERVER_ERROR, 538 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 539 NULL); 540 } 541 TMH_notify_order_change (hc->instance, 542 TMH_OSF_CLAIMED 543 | TMH_OSF_PAID 544 | TMH_OSF_REFUNDED, 545 timestamp, 546 order_serial); 547 } 548 { 549 enum MHD_Result ret; 550 char *taler_refund_uri; 551 552 taler_refund_uri = make_taler_refund_uri (connection, 553 hc->instance->settings.id, 554 hc->infix); 555 ret = TALER_MHD_REPLY_JSON_PACK ( 556 connection, 557 MHD_HTTP_OK, 558 GNUNET_JSON_pack_string ("taler_refund_uri", 559 taler_refund_uri), 560 GNUNET_JSON_pack_data_auto ("h_contract", 561 &h_contract)); 562 GNUNET_free (taler_refund_uri); 563 return ret; 564 } 565 } 566 567 568 /* end of taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c */