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