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 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 6 : trigger_refund_notification (
49 : struct TMH_HandlerContext *hc,
50 : const struct TALER_Amount *amount)
51 : {
52 : const char *as;
53 6 : struct TMH_OrderRefundEventP refund_eh = {
54 6 : .header.size = htons (sizeof (refund_eh)),
55 6 : .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND),
56 6 : .merchant_pub = hc->instance->merchant_pub
57 : };
58 :
59 : /* Resume clients that may wait for this refund */
60 6 : as = TALER_amount2s (amount);
61 6 : 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 6 : GNUNET_CRYPTO_hash (hc->infix,
66 6 : strlen (hc->infix),
67 : &refund_eh.h_order_id);
68 6 : TMH_db->event_notify (TMH_db->cls,
69 : &refund_eh.header,
70 : as,
71 : strlen (as));
72 6 : }
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 6 : 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 6 : GNUNET_assert (NULL != instance_id);
91 6 : GNUNET_assert (NULL != order_id);
92 6 : if (GNUNET_OK !=
93 6 : TMH_taler_uri_by_connection (connection,
94 : "refund",
95 : instance_id,
96 : &buf))
97 : {
98 0 : GNUNET_break (0);
99 0 : return NULL;
100 : }
101 6 : GNUNET_buffer_write_path (&buf,
102 : order_id);
103 6 : GNUNET_buffer_write_path (&buf,
104 : ""); /* Trailing slash */
105 6 : 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 6 : get_refund_limit (void *cls,
121 : const char *exchange_url,
122 : struct TALER_Amount *amount)
123 : {
124 : (void) cls;
125 6 : TMH_EXCHANGES_get_limit (exchange_url,
126 : TALER_KYCLOGIC_KYC_TRIGGER_REFUND,
127 : amount);
128 6 : }
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 10 : 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 10 : TALER_JSON_spec_amount_any ("refund",
150 : &refund),
151 10 : GNUNET_JSON_spec_string ("reason",
152 : &reason),
153 10 : 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 10 : res = TALER_MHD_parse_json_data (connection,
164 10 : hc->request_body,
165 : spec);
166 10 : if (GNUNET_OK != res)
167 : {
168 : return (GNUNET_NO == res)
169 : ? MHD_YES
170 0 : : 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 10 : qs = TMH_db->lookup_contract_terms (TMH_db->cls,
181 10 : hc->instance->settings.id,
182 10 : hc->infix,
183 : &contract_terms,
184 : &order_serial,
185 : NULL);
186 10 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
187 : {
188 2 : if (qs < 0)
189 : {
190 0 : GNUNET_break (0);
191 2 : 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 2 : return TALER_MHD_reply_with_error (
198 : connection,
199 : MHD_HTTP_NOT_FOUND,
200 : TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
201 2 : hc->infix);
202 : }
203 8 : if (GNUNET_OK !=
204 8 : TALER_JSON_contract_hash (contract_terms,
205 : &h_contract))
206 : {
207 0 : GNUNET_break (0);
208 0 : json_decref (contract_terms);
209 0 : 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 8 : GNUNET_JSON_spec_timestamp ("refund_deadline",
218 : &refund_deadline),
219 8 : GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
220 : &wire_deadline),
221 8 : GNUNET_JSON_spec_timestamp ("timestamp",
222 : ×tamp),
223 8 : GNUNET_JSON_spec_end ()
224 : };
225 :
226 8 : if (GNUNET_YES !=
227 8 : GNUNET_JSON_parse (contract_terms,
228 : cspec,
229 : NULL, NULL))
230 : {
231 0 : GNUNET_break (0);
232 0 : json_decref (contract_terms);
233 0 : 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 8 : if (GNUNET_TIME_timestamp_cmp (timestamp,
240 : ==,
241 : refund_deadline))
242 : {
243 : /* refund was never allowed, so we should refuse hard */
244 0 : json_decref (contract_terms);
245 0 : 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 8 : 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 8 : if (GNUNET_TIME_absolute_is_past (wire_deadline.abs_time))
258 : {
259 : /* it is *really* too late for refunds */
260 0 : 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 8 : TMH_db->preflight (TMH_db->cls);
270 8 : for (unsigned int i = 0; i<MAX_RETRIES; i++)
271 : {
272 8 : if (GNUNET_OK !=
273 8 : TMH_db->start (TMH_db->cls,
274 : "increase refund"))
275 : {
276 0 : GNUNET_break (0);
277 0 : json_decref (contract_terms);
278 0 : return TALER_MHD_reply_with_error (connection,
279 : MHD_HTTP_INTERNAL_SERVER_ERROR,
280 : TALER_EC_GENERIC_DB_START_FAILED,
281 : NULL);
282 : }
283 8 : rs = TMH_db->increase_refund (TMH_db->cls,
284 8 : hc->instance->settings.id,
285 8 : hc->infix,
286 : &refund,
287 : &get_refund_limit,
288 : NULL,
289 : reason);
290 8 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
291 : "increase refund returned %d\n",
292 : rs);
293 8 : if (TALER_MERCHANTDB_RS_SUCCESS != rs)
294 2 : TMH_db->rollback (TMH_db->cls);
295 8 : if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs)
296 0 : continue;
297 8 : if (TALER_MERCHANTDB_RS_SUCCESS == rs)
298 : {
299 : enum GNUNET_DB_QueryStatus qs;
300 : json_t *rargs;
301 :
302 6 : 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 6 : GNUNET_assert (NULL != rargs);
315 6 : qs = TMH_trigger_webhook (
316 6 : hc->instance->settings.id,
317 : "refund",
318 : rargs);
319 6 : json_decref (rargs);
320 6 : switch (qs)
321 : {
322 0 : case GNUNET_DB_STATUS_HARD_ERROR:
323 0 : GNUNET_break (0);
324 0 : TMH_db->rollback (TMH_db->cls);
325 0 : rs = TALER_MERCHANTDB_RS_HARD_ERROR;
326 0 : break;
327 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
328 0 : TMH_db->rollback (TMH_db->cls);
329 0 : continue;
330 6 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
331 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
332 6 : qs = TMH_db->commit (TMH_db->cls);
333 6 : break;
334 : }
335 6 : if (GNUNET_DB_STATUS_HARD_ERROR == qs)
336 : {
337 0 : GNUNET_break (0);
338 0 : rs = TALER_MERCHANTDB_RS_HARD_ERROR;
339 0 : break;
340 : }
341 6 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
342 0 : continue;
343 6 : trigger_refund_notification (hc,
344 : &refund);
345 : }
346 8 : break;
347 : } /* retries loop */
348 8 : json_decref (contract_terms);
349 :
350 8 : switch (rs)
351 : {
352 0 : case TALER_MERCHANTDB_RS_LEGAL_FAILURE:
353 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
354 : "Refund amount %s exceeded legal limits of the exchanges involved\n",
355 : TALER_amount2s (&refund));
356 0 : 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 0 : case TALER_MERCHANTDB_RS_BAD_CURRENCY:
362 0 : 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 0 : 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 0 : case TALER_MERCHANTDB_RS_TOO_HIGH:
371 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
372 : "Refusing refund amount %s that is larger than original payment\n",
373 : TALER_amount2s (&refund));
374 0 : return TALER_MHD_reply_with_error (
375 : connection,
376 : MHD_HTTP_CONFLICT,
377 : TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT,
378 : "Amount above payment");
379 0 : case TALER_MERCHANTDB_RS_SOFT_ERROR:
380 : case TALER_MERCHANTDB_RS_HARD_ERROR:
381 0 : return TALER_MHD_reply_with_error (
382 : connection,
383 : MHD_HTTP_INTERNAL_SERVER_ERROR,
384 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
385 : NULL);
386 2 : 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 2 : return TALER_MHD_reply_with_error (
392 : connection,
393 : MHD_HTTP_CONFLICT,
394 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID,
395 2 : hc->infix);
396 6 : case TALER_MERCHANTDB_RS_SUCCESS:
397 : /* continued below */
398 6 : break;
399 : } /* end switch */
400 :
401 : {
402 : uint64_t order_serial;
403 : enum GNUNET_DB_QueryStatus qs;
404 :
405 6 : qs = TMH_db->lookup_order_summary (TMH_db->cls,
406 6 : hc->instance->settings.id,
407 6 : hc->infix,
408 : ×tamp,
409 : &order_serial);
410 6 : if (0 >= qs)
411 : {
412 0 : GNUNET_break (0);
413 0 : 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 6 : 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 6 : taler_refund_uri = make_taler_refund_uri (connection,
431 6 : hc->instance->settings.id,
432 6 : hc->infix);
433 6 : 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 6 : GNUNET_free (taler_refund_uri);
441 6 : return ret;
442 : }
443 : }
444 :
445 :
446 : /* end of taler-merchant-httpd_private-post-orders-ID-refund.c */
|