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