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