taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c (15804B)
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/increase_refund.h" 33 #include "merchant-database/lookup_contract_terms.h" 34 #include "merchant-database/lookup_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 186 { 187 enum GNUNET_GenericReturnValue res; 188 189 res = TALER_MHD_parse_json_data (connection, 190 hc->request_body, 191 spec); 192 if (GNUNET_OK != res) 193 { 194 return (GNUNET_NO == res) 195 ? MHD_YES 196 : MHD_NO; 197 } 198 } 199 200 { 201 enum GNUNET_DB_QueryStatus qs; 202 uint64_t order_serial; 203 struct GNUNET_TIME_Timestamp refund_deadline; 204 struct GNUNET_TIME_Timestamp wire_deadline; 205 206 qs = TALER_MERCHANTDB_lookup_contract_terms (TMH_db, 207 hc->instance->settings.id, 208 hc->infix, 209 &contract_terms, 210 &order_serial, 211 NULL); 212 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) 213 { 214 if (qs < 0) 215 { 216 GNUNET_break (0); 217 return TALER_MHD_reply_with_error ( 218 connection, 219 MHD_HTTP_INTERNAL_SERVER_ERROR, 220 TALER_EC_GENERIC_DB_FETCH_FAILED, 221 "lookup_contract_terms"); 222 } 223 return TALER_MHD_reply_with_error ( 224 connection, 225 MHD_HTTP_NOT_FOUND, 226 TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, 227 hc->infix); 228 } 229 if (GNUNET_OK != 230 TALER_JSON_contract_hash (contract_terms, 231 &h_contract)) 232 { 233 GNUNET_break (0); 234 json_decref (contract_terms); 235 return TALER_MHD_reply_with_error ( 236 connection, 237 MHD_HTTP_INTERNAL_SERVER_ERROR, 238 TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, 239 "Could not hash contract terms"); 240 } 241 { 242 struct GNUNET_JSON_Specification cspec[] = { 243 GNUNET_JSON_spec_timestamp ("refund_deadline", 244 &refund_deadline), 245 GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", 246 &wire_deadline), 247 GNUNET_JSON_spec_timestamp ("timestamp", 248 ×tamp), 249 GNUNET_JSON_spec_end () 250 }; 251 252 if (GNUNET_YES != 253 GNUNET_JSON_parse (contract_terms, 254 cspec, 255 NULL, NULL)) 256 { 257 GNUNET_break (0); 258 json_decref (contract_terms); 259 return TALER_MHD_reply_with_error ( 260 connection, 261 MHD_HTTP_INTERNAL_SERVER_ERROR, 262 TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, 263 "mandatory fields missing"); 264 } 265 if (GNUNET_TIME_timestamp_cmp (timestamp, 266 ==, 267 refund_deadline)) 268 { 269 /* refund was never allowed, so we should refuse hard */ 270 json_decref (contract_terms); 271 return TALER_MHD_reply_with_error ( 272 connection, 273 MHD_HTTP_FORBIDDEN, 274 TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT, 275 NULL); 276 } 277 if (GNUNET_TIME_absolute_is_past (refund_deadline.abs_time)) 278 { 279 /* it is too late for refunds */ 280 /* NOTE: We MAY still be lucky that the exchange did not yet 281 wire the funds, so we will try to give the refund anyway */ 282 } 283 if (GNUNET_TIME_absolute_is_past (wire_deadline.abs_time)) 284 { 285 /* it is *really* too late for refunds */ 286 return TALER_MHD_reply_with_error ( 287 connection, 288 MHD_HTTP_GONE, 289 TALER_EC_MERCHANT_PRIVATE_POST_REFUND_AFTER_WIRE_DEADLINE, 290 NULL); 291 } 292 } 293 } 294 295 TALER_MERCHANTDB_preflight (TMH_db); 296 for (unsigned int i = 0; i<MAX_RETRIES; i++) 297 { 298 if (GNUNET_OK != 299 TALER_MERCHANTDB_start (TMH_db, 300 "increase refund")) 301 { 302 GNUNET_break (0); 303 json_decref (contract_terms); 304 return TALER_MHD_reply_with_error (connection, 305 MHD_HTTP_INTERNAL_SERVER_ERROR, 306 TALER_EC_GENERIC_DB_START_FAILED, 307 NULL); 308 } 309 rs = TALER_MERCHANTDB_increase_refund (TMH_db, 310 hc->instance->settings.id, 311 hc->infix, 312 &refund, 313 &get_refund_limit, 314 NULL, 315 reason); 316 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 317 "increase refund returned %d\n", 318 rs); 319 if (TALER_MERCHANTDB_RS_SUCCESS != rs) 320 TALER_MERCHANTDB_rollback (TMH_db); 321 if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs) 322 continue; 323 if (TALER_MERCHANTDB_RS_SUCCESS == rs) 324 { 325 enum GNUNET_DB_QueryStatus qs; 326 json_t *rargs; 327 328 rargs = GNUNET_JSON_PACK ( 329 GNUNET_JSON_pack_timestamp ("timestamp", 330 timestamp), 331 GNUNET_JSON_pack_string ("order_id", 332 hc->infix), 333 GNUNET_JSON_pack_object_incref ("contract_terms", 334 contract_terms), 335 TALER_JSON_pack_amount ("refund_amount", 336 &refund), 337 GNUNET_JSON_pack_string ("reason", 338 reason) 339 ); 340 GNUNET_assert (NULL != rargs); 341 qs = TMH_trigger_webhook ( 342 hc->instance->settings.id, 343 "refund", 344 rargs); 345 json_decref (rargs); 346 switch (qs) 347 { 348 case GNUNET_DB_STATUS_HARD_ERROR: 349 GNUNET_break (0); 350 TALER_MERCHANTDB_rollback (TMH_db); 351 rs = TALER_MERCHANTDB_RS_HARD_ERROR; 352 break; 353 case GNUNET_DB_STATUS_SOFT_ERROR: 354 TALER_MERCHANTDB_rollback (TMH_db); 355 continue; 356 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 357 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 358 qs = TALER_MERCHANTDB_commit (TMH_db); 359 break; 360 } 361 if (GNUNET_DB_STATUS_HARD_ERROR == qs) 362 { 363 GNUNET_break (0); 364 rs = TALER_MERCHANTDB_RS_HARD_ERROR; 365 break; 366 } 367 if (GNUNET_DB_STATUS_SOFT_ERROR == qs) 368 continue; 369 trigger_refund_notification (hc, 370 &refund); 371 } 372 break; 373 } /* retries loop */ 374 json_decref (contract_terms); 375 376 switch (rs) 377 { 378 case TALER_MERCHANTDB_RS_LEGAL_FAILURE: 379 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 380 "Refund amount %s exceeded legal limits of the exchanges involved\n", 381 TALER_amount2s (&refund)); 382 return TALER_MHD_reply_with_error ( 383 connection, 384 MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, 385 TALER_EC_MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION, 386 NULL); 387 case TALER_MERCHANTDB_RS_BAD_CURRENCY: 388 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 389 "Refund amount %s is not in the currency of the original payment\n", 390 TALER_amount2s (&refund)); 391 return TALER_MHD_reply_with_error ( 392 connection, 393 MHD_HTTP_CONFLICT, 394 TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, 395 "Order was paid in a different currency"); 396 case TALER_MERCHANTDB_RS_TOO_HIGH: 397 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 398 "Refusing refund amount %s that is larger than original payment\n", 399 TALER_amount2s (&refund)); 400 return TALER_MHD_reply_with_error ( 401 connection, 402 MHD_HTTP_CONFLICT, 403 TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, 404 "Amount above payment"); 405 case TALER_MERCHANTDB_RS_SOFT_ERROR: 406 case TALER_MERCHANTDB_RS_HARD_ERROR: 407 return TALER_MHD_reply_with_error ( 408 connection, 409 MHD_HTTP_INTERNAL_SERVER_ERROR, 410 TALER_EC_GENERIC_DB_COMMIT_FAILED, 411 NULL); 412 case TALER_MERCHANTDB_RS_NO_SUCH_ORDER: 413 /* We know the order exists from the 414 "lookup_contract_terms" at the beginning; 415 so if we get 'no such order' here, it 416 must be read as "no PAID order" */ 417 return TALER_MHD_reply_with_error ( 418 connection, 419 MHD_HTTP_CONFLICT, 420 TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID, 421 hc->infix); 422 case TALER_MERCHANTDB_RS_SUCCESS: 423 /* continued below */ 424 break; 425 } /* end switch */ 426 427 { 428 uint64_t order_serial; 429 enum GNUNET_DB_QueryStatus qs; 430 431 qs = TALER_MERCHANTDB_lookup_order_summary (TMH_db, 432 hc->instance->settings.id, 433 hc->infix, 434 ×tamp, 435 &order_serial); 436 if (0 >= qs) 437 { 438 GNUNET_break (0); 439 return TALER_MHD_reply_with_error ( 440 connection, 441 MHD_HTTP_INTERNAL_SERVER_ERROR, 442 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 443 NULL); 444 } 445 TMH_notify_order_change (hc->instance, 446 TMH_OSF_CLAIMED 447 | TMH_OSF_PAID 448 | TMH_OSF_REFUNDED, 449 timestamp, 450 order_serial); 451 } 452 { 453 enum MHD_Result ret; 454 char *taler_refund_uri; 455 456 taler_refund_uri = make_taler_refund_uri (connection, 457 hc->instance->settings.id, 458 hc->infix); 459 ret = TALER_MHD_REPLY_JSON_PACK ( 460 connection, 461 MHD_HTTP_OK, 462 GNUNET_JSON_pack_string ("taler_refund_uri", 463 taler_refund_uri), 464 GNUNET_JSON_pack_data_auto ("h_contract", 465 &h_contract)); 466 GNUNET_free (taler_refund_uri); 467 return ret; 468 } 469 } 470 471 472 /* end of taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c */