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