Line data Source code
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 10 : trigger_refund_notification (
56 : struct TMH_HandlerContext *hc,
57 : const struct TALER_Amount *amount)
58 : {
59 : {
60 : const char *as;
61 10 : struct TMH_OrderRefundEventP refund_eh = {
62 10 : .header.size = htons (sizeof (refund_eh)),
63 10 : .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND),
64 10 : .merchant_pub = hc->instance->merchant_pub
65 : };
66 :
67 : /* Resume clients that may wait for this refund */
68 10 : as = TALER_amount2s (amount);
69 10 : 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 10 : GNUNET_CRYPTO_hash (hc->infix,
74 10 : strlen (hc->infix),
75 : &refund_eh.h_order_id);
76 10 : TALER_MERCHANTDB_event_notify (TMH_db,
77 : &refund_eh.header,
78 : as,
79 : strlen (as));
80 : }
81 : {
82 10 : struct TMH_OrderPayEventP pay_eh = {
83 10 : .header.size = htons (sizeof (pay_eh)),
84 10 : .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED),
85 10 : .merchant_pub = hc->instance->merchant_pub
86 : };
87 :
88 10 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
89 : "Notifying clients about status change of order %s\n",
90 : hc->infix);
91 10 : GNUNET_CRYPTO_hash (hc->infix,
92 10 : strlen (hc->infix),
93 : &pay_eh.h_order_id);
94 10 : TALER_MERCHANTDB_event_notify (TMH_db,
95 : &pay_eh.header,
96 : NULL,
97 : 0);
98 : }
99 10 : }
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 10 : 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 10 : GNUNET_assert (NULL != instance_id);
118 10 : GNUNET_assert (NULL != order_id);
119 10 : if (GNUNET_OK !=
120 10 : TMH_taler_uri_by_connection (connection,
121 : "refund",
122 : instance_id,
123 : &buf))
124 : {
125 0 : GNUNET_break (0);
126 0 : return NULL;
127 : }
128 10 : GNUNET_buffer_write_path (&buf,
129 : order_id);
130 10 : GNUNET_buffer_write_path (&buf,
131 : ""); /* Trailing slash */
132 10 : 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 10 : get_refund_limit (void *cls,
148 : const char *exchange_url,
149 : struct TALER_Amount *amount)
150 : {
151 : (void) cls;
152 10 : TMH_EXCHANGES_get_limit (exchange_url,
153 : TALER_KYCLOGIC_KYC_TRIGGER_REFUND,
154 : amount);
155 10 : }
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 16 : 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 16 : TALER_JSON_spec_amount_any ("refund",
177 : &refund),
178 16 : GNUNET_JSON_spec_string ("reason",
179 : &reason),
180 16 : 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 16 : res = TALER_MHD_parse_json_data (connection,
193 16 : hc->request_body,
194 : spec);
195 16 : if (GNUNET_OK != res)
196 : {
197 : return (GNUNET_NO == res)
198 : ? MHD_YES
199 0 : : 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 16 : qs = TALER_MERCHANTDB_get_contract_terms_status (TMH_db,
213 16 : hc->instance->settings.id,
214 16 : hc->infix,
215 : NULL,
216 : &contract_terms,
217 : &order_serial,
218 : &paid,
219 : &wired,
220 : &session_matches,
221 : NULL,
222 : &choice_index);
223 16 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
224 : {
225 2 : if (qs < 0)
226 : {
227 0 : GNUNET_break (0);
228 2 : 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 2 : return TALER_MHD_reply_with_error (
235 : connection,
236 : MHD_HTTP_NOT_FOUND,
237 : TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
238 2 : hc->infix);
239 : }
240 14 : if (GNUNET_OK !=
241 14 : TALER_JSON_contract_hash (contract_terms,
242 : &h_contract))
243 : {
244 0 : GNUNET_break (0);
245 0 : json_decref (contract_terms);
246 0 : 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 14 : GNUNET_JSON_spec_timestamp ("refund_deadline",
255 : &refund_deadline),
256 14 : GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
257 : &wire_deadline),
258 14 : GNUNET_JSON_spec_timestamp ("timestamp",
259 : ×tamp),
260 14 : GNUNET_JSON_spec_end ()
261 : };
262 :
263 14 : if (GNUNET_YES !=
264 14 : GNUNET_JSON_parse (contract_terms,
265 : cspec,
266 : NULL, NULL))
267 : {
268 0 : GNUNET_break (0);
269 0 : json_decref (contract_terms);
270 0 : 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 14 : if (GNUNET_TIME_timestamp_cmp (timestamp,
277 : ==,
278 : refund_deadline))
279 : {
280 : /* refund was never allowed, so we should refuse hard */
281 0 : json_decref (contract_terms);
282 0 : 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 14 : 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 14 : if (GNUNET_TIME_absolute_is_past (wire_deadline.abs_time))
295 : {
296 : /* it is *really* too late for refunds */
297 0 : 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 14 : TALER_MERCHANTDB_preflight (TMH_db);
307 14 : for (unsigned int i = 0; i<MAX_RETRIES; i++)
308 : {
309 14 : struct TALER_Amount external_total = {0};
310 14 : bool external_mismatch = false;
311 :
312 14 : if (GNUNET_OK !=
313 14 : TALER_MERCHANTDB_start (TMH_db,
314 : "increase refund"))
315 : {
316 0 : GNUNET_break (0);
317 0 : json_decref (contract_terms);
318 2 : 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 14 : eqs = TALER_MERCHANTDB_get_external_refunds_total (
327 : TMH_db,
328 14 : hc->instance->settings.id,
329 14 : hc->infix,
330 : &external_total,
331 : &external_mismatch);
332 14 : if (0 > eqs)
333 : {
334 0 : TALER_MERCHANTDB_rollback (TMH_db);
335 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == eqs)
336 0 : continue;
337 0 : json_decref (contract_terms);
338 0 : 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 14 : if (external_mismatch)
345 : {
346 0 : GNUNET_break (0);
347 0 : TALER_MERCHANTDB_rollback (TMH_db);
348 0 : json_decref (contract_terms);
349 0 : 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 14 : 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 2 : if (GNUNET_OK !=
362 2 : TMH_compute_order_total (contract_terms,
363 : choice_index,
364 : &order_total))
365 : {
366 0 : TALER_MERCHANTDB_rollback (TMH_db);
367 0 : json_decref (contract_terms);
368 2 : 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 2 : if (0 >
375 2 : TALER_amount_subtract (&remaining,
376 : &order_total,
377 : &external_total))
378 : {
379 0 : GNUNET_break (0);
380 0 : TALER_MERCHANTDB_rollback (TMH_db);
381 0 : json_decref (contract_terms);
382 0 : 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 2 : if (1 ==
389 2 : TALER_amount_cmp (&refund,
390 : &remaining))
391 : {
392 2 : TALER_MERCHANTDB_rollback (TMH_db);
393 2 : 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 2 : json_decref (contract_terms);
399 2 : 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 12 : rs = TALER_MERCHANTDB_do_increase_refund (TMH_db,
407 12 : hc->instance->settings.id,
408 12 : hc->infix,
409 : &refund,
410 : &get_refund_limit,
411 : NULL,
412 : reason);
413 12 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
414 : "increase refund returned %d\n",
415 : rs);
416 12 : if (TALER_MERCHANTDB_RS_SUCCESS != rs)
417 2 : TALER_MERCHANTDB_rollback (TMH_db);
418 12 : if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs)
419 0 : continue;
420 12 : if (TALER_MERCHANTDB_RS_SUCCESS == rs)
421 : {
422 : enum GNUNET_DB_QueryStatus qs;
423 : json_t *rargs;
424 :
425 10 : 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 10 : GNUNET_assert (NULL != rargs);
438 10 : qs = TMH_trigger_webhook (
439 10 : hc->instance->settings.id,
440 : "refund",
441 : rargs);
442 10 : json_decref (rargs);
443 10 : switch (qs)
444 : {
445 0 : case GNUNET_DB_STATUS_HARD_ERROR:
446 0 : GNUNET_break (0);
447 0 : TALER_MERCHANTDB_rollback (TMH_db);
448 0 : rs = TALER_MERCHANTDB_RS_HARD_ERROR;
449 0 : break;
450 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
451 0 : TALER_MERCHANTDB_rollback (TMH_db);
452 0 : continue;
453 10 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
454 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
455 10 : qs = TALER_MERCHANTDB_commit (TMH_db);
456 10 : break;
457 : }
458 10 : if (GNUNET_DB_STATUS_HARD_ERROR == qs)
459 : {
460 0 : GNUNET_break (0);
461 0 : rs = TALER_MERCHANTDB_RS_HARD_ERROR;
462 0 : break;
463 : }
464 10 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
465 0 : continue;
466 10 : trigger_refund_notification (hc,
467 : &refund);
468 : }
469 12 : break;
470 : } /* retries loop */
471 12 : json_decref (contract_terms);
472 :
473 12 : switch (rs)
474 : {
475 0 : case TALER_MERCHANTDB_RS_LEGAL_FAILURE:
476 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
477 : "Refund amount %s exceeded legal limits of the exchanges involved\n",
478 : TALER_amount2s (&refund));
479 0 : 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 0 : case TALER_MERCHANTDB_RS_BAD_CURRENCY:
485 0 : 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 0 : 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 0 : case TALER_MERCHANTDB_RS_TOO_HIGH:
494 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
495 : "Refusing refund amount %s that is larger than original payment\n",
496 : TALER_amount2s (&refund));
497 0 : 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 0 : case TALER_MERCHANTDB_RS_SOFT_ERROR:
503 : case TALER_MERCHANTDB_RS_HARD_ERROR:
504 0 : return TALER_MHD_reply_with_error (
505 : connection,
506 : MHD_HTTP_INTERNAL_SERVER_ERROR,
507 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
508 : NULL);
509 2 : 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 2 : return TALER_MHD_reply_with_error (
515 : connection,
516 : MHD_HTTP_CONFLICT,
517 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID,
518 2 : hc->infix);
519 10 : case TALER_MERCHANTDB_RS_SUCCESS:
520 : /* continued below */
521 10 : break;
522 : } /* end switch */
523 :
524 : {
525 : uint64_t order_serial;
526 : enum GNUNET_DB_QueryStatus qs;
527 :
528 10 : qs = TALER_MERCHANTDB_get_order_summary (TMH_db,
529 10 : hc->instance->settings.id,
530 10 : hc->infix,
531 : ×tamp,
532 : &order_serial);
533 10 : if (0 >= qs)
534 : {
535 0 : GNUNET_break (0);
536 0 : 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 10 : 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 10 : taler_refund_uri = make_taler_refund_uri (connection,
554 10 : hc->instance->settings.id,
555 10 : hc->infix);
556 10 : 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 10 : GNUNET_free (taler_refund_uri);
564 10 : return ret;
565 : }
566 : }
567 :
568 :
569 : /* end of taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c */
|