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