Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2014, 2015, 2016, 2018, 2020, 2021 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify
6 : it under the terms of the GNU Affero General Public License as
7 : published by the Free Software Foundation; either version 3,
8 : or (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful, but
11 : WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : GNU General Public License for more details.
14 :
15 : You should have received a copy of the GNU General Public
16 : License along with TALER; see the file COPYING. If not,
17 : see <http://www.gnu.org/licenses/>
18 : */
19 :
20 : /**
21 : * @file taler-merchant-httpd_private-post-orders.c
22 : * @brief the POST /orders handler
23 : * @author Christian Grothoff
24 : * @author Marcello Stanisci
25 : */
26 : #include "platform.h"
27 : #include <jansson.h>
28 : #include <taler/taler_signatures.h>
29 : #include <taler/taler_json_lib.h>
30 : #include "taler-merchant-httpd_private-post-orders.h"
31 : #include "taler-merchant-httpd_auditors.h"
32 : #include "taler-merchant-httpd_exchanges.h"
33 : #include "taler-merchant-httpd_helper.h"
34 : #include "taler-merchant-httpd_private-get-orders.h"
35 :
36 :
37 : /**
38 : * How often do we retry the simple INSERT database transaction?
39 : */
40 : #define MAX_RETRIES 3
41 :
42 : /**
43 : * What is the label under which we find/place the merchant's
44 : * jurisdiction in the locations list by default?
45 : */
46 : #define STANDARD_LABEL_MERCHANT_JURISDICTION "_mj"
47 :
48 : /**
49 : * What is the label under which we find/place the merchant's
50 : * address in the locations list by default?
51 : */
52 : #define STANDARD_LABEL_MERCHANT_ADDRESS "_ma"
53 :
54 :
55 : /**
56 : * Check that the given JSON array of products is well-formed.
57 : *
58 : * @param products JSON array to check
59 : * @return #GNUNET_OK if all is fine
60 : */
61 : static enum GNUNET_GenericReturnValue
62 0 : check_products (const json_t *products)
63 : {
64 : size_t index;
65 : json_t *value;
66 :
67 0 : if (! json_is_array (products))
68 : {
69 0 : GNUNET_break (0);
70 0 : return GNUNET_SYSERR;
71 : }
72 0 : json_array_foreach (products, index, value) {
73 : const char *description;
74 : const char *error_name;
75 : unsigned int error_line;
76 : enum GNUNET_GenericReturnValue res;
77 : struct GNUNET_JSON_Specification spec[] = {
78 : // FIXME: parse and format-validate all
79 : // optional fields of a product and check validity
80 0 : GNUNET_JSON_spec_string ("description",
81 : &description),
82 0 : GNUNET_JSON_spec_end ()
83 : };
84 :
85 : /* extract fields we need to sign separately */
86 0 : res = GNUNET_JSON_parse (value,
87 : spec,
88 : &error_name,
89 : &error_line);
90 0 : if (GNUNET_OK != res)
91 : {
92 0 : GNUNET_break (0);
93 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
94 : "Product parsing failed at #%u: %s:%u\n",
95 : (unsigned int) index,
96 : error_name,
97 : error_line);
98 0 : return GNUNET_SYSERR;
99 : }
100 0 : GNUNET_JSON_parse_free (spec);
101 : }
102 0 : return GNUNET_OK;
103 : }
104 :
105 :
106 : /**
107 : * Generate the base URL for the given merchant instance.
108 : *
109 : * @param connection the MHD connection
110 : * @param instance_id the merchant instance ID
111 : * @returns the merchant instance's base URL
112 : */
113 : static char *
114 0 : make_merchant_base_url (struct MHD_Connection *connection,
115 : const char *instance_id)
116 : {
117 : const char *host;
118 : const char *forwarded_host;
119 : const char *uri_path;
120 0 : struct GNUNET_Buffer buf = { 0 };
121 :
122 0 : if (GNUNET_YES == TALER_mhd_is_https (connection))
123 0 : GNUNET_buffer_write_str (&buf, "https://");
124 : else
125 0 : GNUNET_buffer_write_str (&buf, "http://");
126 0 : host = MHD_lookup_connection_value (connection,
127 : MHD_HEADER_KIND,
128 : MHD_HTTP_HEADER_HOST);
129 0 : forwarded_host = MHD_lookup_connection_value (connection,
130 : MHD_HEADER_KIND,
131 : "X-Forwarded-Host");
132 0 : if (NULL != forwarded_host)
133 : {
134 0 : GNUNET_buffer_write_str (&buf,
135 : forwarded_host);
136 : }
137 : else
138 : {
139 0 : GNUNET_assert (NULL != host);
140 0 : GNUNET_buffer_write_str (&buf,
141 : host);
142 : }
143 0 : uri_path = MHD_lookup_connection_value (connection,
144 : MHD_HEADER_KIND,
145 : "X-Forwarded-Prefix");
146 0 : if (NULL != uri_path)
147 0 : GNUNET_buffer_write_path (&buf, uri_path);
148 :
149 0 : if (0 != strcmp (instance_id,
150 : "default"))
151 : {
152 0 : GNUNET_buffer_write_path (&buf,
153 : "/instances/");
154 0 : GNUNET_buffer_write_str (&buf,
155 : instance_id);
156 : }
157 0 : GNUNET_buffer_write_path (&buf,
158 : "");
159 0 : return GNUNET_buffer_reap_str (&buf);
160 : }
161 :
162 :
163 : /**
164 : * Information about a product we are supposed to add to the order
165 : * based on what we know it from our inventory.
166 : */
167 : struct InventoryProduct
168 : {
169 : /**
170 : * Identifier of the product in the inventory.
171 : */
172 : const char *product_id;
173 :
174 : /**
175 : * Number of units of the product to add to the order.
176 : */
177 : uint32_t quantity;
178 : };
179 :
180 :
181 : /**
182 : * Execute the database transaction to setup the order.
183 : *
184 : * @param hc handler context for the request
185 : * @param order_id unique ID for the order
186 : * @param h_post_data hash of the client's POST request, for idempotency checks
187 : * @param pay_deadline until when does the order have to be paid
188 : * @param[in] order order to process (not modified)
189 : * @param claim_token token to use for access control
190 : * @param inventory_products_length length of the @a inventory_products array
191 : * @param inventory_products array of products to add to @a order from our inventory
192 : * @param uuids_length length of the @a uuids array
193 : * @param uuids array of UUIDs used to reserve products from @a inventory_products
194 : * @param[out] out_of_stock_index which product (by offset) is out of stock, UINT_MAX if all were in-stock
195 : * @return transaction status, 0 if @a uuids were insufficient to reserve required inventory
196 : */
197 : static enum GNUNET_DB_QueryStatus
198 0 : execute_transaction (struct TMH_HandlerContext *hc,
199 : const char *order_id,
200 : const struct TALER_MerchantPostDataHashP *h_post_data,
201 : struct GNUNET_TIME_Timestamp pay_deadline,
202 : const json_t *order,
203 : const struct TALER_ClaimTokenP *claim_token,
204 : unsigned int inventory_products_length,
205 : const struct InventoryProduct inventory_products[],
206 : unsigned int uuids_length,
207 : const struct GNUNET_Uuid uuids[],
208 : unsigned int *out_of_stock_index)
209 : {
210 : enum GNUNET_DB_QueryStatus qs;
211 : struct GNUNET_TIME_Timestamp timestamp;
212 : uint64_t order_serial;
213 :
214 0 : if (GNUNET_OK !=
215 0 : TMH_db->start (TMH_db->cls,
216 : "insert_order"))
217 : {
218 0 : GNUNET_break (0);
219 0 : return GNUNET_DB_STATUS_HARD_ERROR;
220 : }
221 : /* Setup order */
222 0 : qs = TMH_db->insert_order (TMH_db->cls,
223 0 : hc->instance->settings.id,
224 : order_id,
225 : h_post_data,
226 : pay_deadline,
227 : claim_token,
228 : order); // called 'contract terms' at database.
229 0 : if (qs <= 0)
230 : {
231 : /* qs == 0: probably instance does not exist (anymore) */
232 0 : TMH_db->rollback (TMH_db->cls);
233 0 : return qs;
234 : }
235 : /* Migrate locks from UUIDs to new order: first release old locks */
236 0 : for (unsigned int i = 0; i<uuids_length; i++)
237 : {
238 0 : qs = TMH_db->unlock_inventory (TMH_db->cls,
239 0 : &uuids[i]);
240 0 : if (qs < 0)
241 : {
242 0 : TMH_db->rollback (TMH_db->cls);
243 0 : return qs;
244 : }
245 : /* qs == 0 is OK here, that just means we did not HAVE any lock under this
246 : UUID */
247 : }
248 : /* Migrate locks from UUIDs to new order: acquire new locks
249 : (note: this can basically ONLY fail on serializability OR
250 : because the UUID locks were insufficient for the desired
251 : quantities). */
252 0 : for (unsigned int i = 0; i<inventory_products_length; i++)
253 : {
254 0 : qs = TMH_db->insert_order_lock (TMH_db->cls,
255 0 : hc->instance->settings.id,
256 : order_id,
257 0 : inventory_products[i].product_id,
258 0 : inventory_products[i].quantity);
259 0 : if (qs < 0)
260 : {
261 0 : TMH_db->rollback (TMH_db->cls);
262 0 : return qs;
263 : }
264 0 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
265 : {
266 : /* qs == 0: lock acquisition failed due to insufficient stocks */
267 0 : TMH_db->rollback (TMH_db->cls);
268 0 : *out_of_stock_index = i; /* indicate which product is causing the issue */
269 0 : return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
270 : }
271 : }
272 0 : *out_of_stock_index = UINT_MAX;
273 :
274 : /* Get the order serial and timestamp for the order we just created to
275 : update long-poll clients. */
276 0 : qs = TMH_db->lookup_order_summary (TMH_db->cls,
277 0 : hc->instance->settings.id,
278 : order_id,
279 : ×tamp,
280 : &order_serial);
281 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
282 : {
283 0 : TMH_db->rollback (TMH_db->cls);
284 0 : return qs;
285 : }
286 0 : TMH_notify_order_change (hc->instance,
287 : TMH_OSF_NONE,
288 : timestamp,
289 : order_serial);
290 : /* finally, commit transaction (note: if it fails, we ALSO re-acquire
291 : the UUID locks, which is exactly what we want) */
292 0 : qs = TMH_db->commit (TMH_db->cls);
293 0 : if (0 > qs)
294 0 : return qs;
295 0 : return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* 1 == success! */
296 : }
297 :
298 :
299 : /**
300 : * Transform an order into a proposal and store it in the
301 : * database. Write the resulting proposal or an error message
302 : * of a MHD connection.
303 : *
304 : * @param connection connection to write the result or error to
305 : * @param hc handler context for the request
306 : * @param h_post_data hash of the client's POST request, for idempotency checks
307 : * @param[in,out] order order to process (can be modified)
308 : * @param claim_token token to use for access control
309 : * @param inventory_products_length length of the @a inventory_products array
310 : * @param inventory_products array of products to add to @a order from our inventory
311 : * @param uuids_length length of the @a uuids array
312 : * @param uuids array of UUIDs used to reserve products from @a inventory_products
313 : * @return MHD result code
314 : */
315 : static MHD_RESULT
316 0 : execute_order (struct MHD_Connection *connection,
317 : struct TMH_HandlerContext *hc,
318 : const struct TALER_MerchantPostDataHashP *h_post_data,
319 : json_t *order,
320 : const struct TALER_ClaimTokenP *claim_token,
321 : unsigned int inventory_products_length,
322 : const struct InventoryProduct inventory_products[],
323 : unsigned int uuids_length,
324 : const struct GNUNET_Uuid uuids[])
325 : {
326 0 : const struct TALER_MERCHANTDB_InstanceSettings *settings =
327 0 : &hc->instance->settings;
328 : struct TALER_Amount total;
329 : const char *order_id;
330 : const char *summary;
331 0 : const char *fulfillment_msg = NULL;
332 : json_t *products;
333 : json_t *merchant;
334 0 : json_t *summary_i18n = NULL;
335 0 : json_t *fulfillment_i18n = NULL;
336 : struct GNUNET_TIME_Timestamp timestamp;
337 0 : struct GNUNET_TIME_Timestamp refund_deadline = { 0 };
338 : struct GNUNET_TIME_Timestamp wire_transfer_deadline;
339 : struct GNUNET_TIME_Timestamp pay_deadline;
340 : struct GNUNET_JSON_Specification spec[] = {
341 0 : TALER_JSON_spec_amount ("amount",
342 : TMH_currency,
343 : &total),
344 0 : GNUNET_JSON_spec_string ("order_id",
345 : &order_id),
346 0 : GNUNET_JSON_spec_string ("summary",
347 : &summary),
348 : /**
349 : * The following entries we don't actually need,
350 : * except to check that the order is well-formed */
351 0 : GNUNET_JSON_spec_json ("products",
352 : &products),
353 0 : GNUNET_JSON_spec_json ("merchant",
354 : &merchant),
355 0 : GNUNET_JSON_spec_mark_optional (
356 : GNUNET_JSON_spec_json ("summary_i18n",
357 : &summary_i18n),
358 : NULL),
359 0 : GNUNET_JSON_spec_mark_optional (
360 : GNUNET_JSON_spec_string ("fulfillment_message",
361 : &fulfillment_msg),
362 : NULL),
363 0 : GNUNET_JSON_spec_mark_optional (
364 : GNUNET_JSON_spec_json ("fulfillment_message_i18n",
365 : &fulfillment_i18n),
366 : NULL),
367 0 : GNUNET_JSON_spec_timestamp ("timestamp",
368 : ×tamp),
369 0 : GNUNET_JSON_spec_mark_optional (
370 : GNUNET_JSON_spec_timestamp ("refund_deadline",
371 : &refund_deadline),
372 : NULL),
373 0 : GNUNET_JSON_spec_timestamp ("pay_deadline",
374 : &pay_deadline),
375 0 : GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
376 : &wire_transfer_deadline),
377 0 : GNUNET_JSON_spec_end ()
378 : };
379 : enum GNUNET_DB_QueryStatus qs;
380 : unsigned int out_of_stock_index;
381 :
382 : /* extract fields we need to sign separately */
383 : {
384 : enum GNUNET_GenericReturnValue res;
385 :
386 0 : res = TALER_MHD_parse_json_data (connection,
387 : order,
388 : spec);
389 0 : if (GNUNET_OK != res)
390 : {
391 0 : GNUNET_break_op (0);
392 : return (GNUNET_NO == res)
393 : ? MHD_YES
394 0 : : MHD_NO;
395 : }
396 : }
397 :
398 : /* check product list in contract is well-formed */
399 0 : if (GNUNET_OK != check_products (products))
400 : {
401 0 : GNUNET_JSON_parse_free (spec);
402 0 : return TALER_MHD_reply_with_error (connection,
403 : MHD_HTTP_BAD_REQUEST,
404 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
405 : "order:products");
406 : }
407 :
408 0 : if ( (NULL != fulfillment_i18n) &&
409 0 : (! TALER_JSON_check_i18n (fulfillment_i18n)) )
410 : {
411 0 : GNUNET_JSON_parse_free (spec);
412 0 : return TALER_MHD_reply_with_error (connection,
413 : MHD_HTTP_BAD_REQUEST,
414 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
415 : "order:fulfillment_message_i18n");
416 : }
417 0 : if ( (NULL != summary_i18n) &&
418 0 : (! TALER_JSON_check_i18n (summary_i18n)) )
419 : {
420 0 : GNUNET_JSON_parse_free (spec);
421 0 : return TALER_MHD_reply_with_error (connection,
422 : MHD_HTTP_BAD_REQUEST,
423 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
424 : "order:summary_i18n");
425 : }
426 :
427 : /* Test if we already have an order with this id */
428 : {
429 : struct TALER_ClaimTokenP token;
430 : json_t *contract_terms;
431 : struct TALER_MerchantPostDataHashP orig_post;
432 :
433 0 : TMH_db->preflight (TMH_db->cls);
434 0 : qs = TMH_db->lookup_order (TMH_db->cls,
435 0 : hc->instance->settings.id,
436 : order_id,
437 : &token,
438 : &orig_post,
439 : &contract_terms);
440 : /* If yes, check for idempotency */
441 0 : if (0 > qs)
442 : {
443 0 : GNUNET_break (0);
444 0 : TMH_db->rollback (TMH_db->cls);
445 0 : GNUNET_JSON_parse_free (spec);
446 0 : return TALER_MHD_reply_with_error (connection,
447 : MHD_HTTP_INTERNAL_SERVER_ERROR,
448 : TALER_EC_GENERIC_DB_FETCH_FAILED,
449 : "lookup_order");
450 : }
451 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
452 : {
453 : MHD_RESULT ret;
454 :
455 0 : json_decref (contract_terms);
456 : /* Comparing the contract terms is sufficient because all the other
457 : params get added to it at some point. */
458 0 : if (0 == GNUNET_memcmp (&orig_post,
459 : h_post_data))
460 : {
461 0 : ret = TALER_MHD_REPLY_JSON_PACK (
462 : connection,
463 : MHD_HTTP_OK,
464 : GNUNET_JSON_pack_string ("order_id",
465 : order_id),
466 : GNUNET_JSON_pack_allow_null (
467 : GNUNET_JSON_pack_data_varsize (
468 : "token",
469 : GNUNET_is_zero (&token)
470 : ? NULL
471 : : &token,
472 : sizeof (token))));
473 : }
474 : else
475 : {
476 : /* This request is not idempotent */
477 0 : ret = TALER_MHD_reply_with_error (
478 : connection,
479 : MHD_HTTP_CONFLICT,
480 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS,
481 : order_id);
482 : }
483 0 : GNUNET_JSON_parse_free (spec);
484 0 : return ret;
485 : }
486 : }
487 0 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
488 : "Executing database transaction to create order '%s' for instance '%s'\n",
489 : order_id,
490 : settings->id);
491 0 : for (unsigned int i = 0; i<MAX_RETRIES; i++)
492 : {
493 0 : TMH_db->preflight (TMH_db->cls);
494 0 : qs = execute_transaction (hc,
495 : order_id,
496 : h_post_data,
497 : pay_deadline,
498 : order,
499 : claim_token,
500 : inventory_products_length,
501 : inventory_products,
502 : uuids_length,
503 : uuids,
504 : &out_of_stock_index);
505 0 : if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
506 0 : break;
507 : }
508 0 : if (0 >= qs)
509 : {
510 0 : GNUNET_JSON_parse_free (spec);
511 : /* Special report if retries insufficient */
512 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
513 : {
514 0 : GNUNET_break (0);
515 0 : return TALER_MHD_reply_with_error (connection,
516 : MHD_HTTP_INTERNAL_SERVER_ERROR,
517 : TALER_EC_GENERIC_DB_SOFT_FAILURE,
518 : NULL);
519 : }
520 0 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
521 : {
522 : /* should be: contract (!) with same order ID
523 : already exists */
524 0 : return TALER_MHD_reply_with_error (
525 : connection,
526 : MHD_HTTP_CONFLICT,
527 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS,
528 : order_id);
529 : }
530 : /* Other hard transaction error (disk full, etc.) */
531 0 : GNUNET_break (0);
532 0 : return TALER_MHD_reply_with_error (
533 : connection,
534 : MHD_HTTP_INTERNAL_SERVER_ERROR,
535 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
536 : NULL);
537 : }
538 :
539 : /* DB transaction succeeded, check for out-of-stock */
540 0 : if (out_of_stock_index < UINT_MAX)
541 : {
542 : /* We had a product that has insufficient quantities,
543 : generate the details for the response. */
544 : struct TALER_MERCHANTDB_ProductDetails pd;
545 : MHD_RESULT ret;
546 :
547 0 : memset (&pd, 0, sizeof (pd));
548 0 : qs = TMH_db->lookup_product (
549 0 : TMH_db->cls,
550 0 : hc->instance->settings.id,
551 0 : inventory_products[out_of_stock_index].product_id,
552 : &pd);
553 0 : GNUNET_JSON_parse_free (spec);
554 0 : switch (qs)
555 : {
556 0 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
557 0 : ret = TALER_MHD_REPLY_JSON_PACK (
558 : connection,
559 : MHD_HTTP_GONE,
560 : GNUNET_JSON_pack_string (
561 : "product_id",
562 : inventory_products[out_of_stock_index].product_id),
563 : GNUNET_JSON_pack_uint64 (
564 : "requested_quantity",
565 : inventory_products[out_of_stock_index].quantity),
566 : GNUNET_JSON_pack_uint64 (
567 : "available_quantity",
568 : pd.total_stock - pd.total_sold - pd.total_lost),
569 : GNUNET_JSON_pack_allow_null (
570 : GNUNET_JSON_pack_timestamp (
571 : "restock_expected",
572 : pd.next_restock)));
573 0 : TALER_MERCHANTDB_product_details_free (&pd);
574 0 : return ret;
575 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
576 0 : return TALER_MHD_REPLY_JSON_PACK (
577 : connection,
578 : MHD_HTTP_GONE,
579 : GNUNET_JSON_pack_string (
580 : "product_id",
581 : inventory_products[out_of_stock_index].product_id),
582 : GNUNET_JSON_pack_uint64 (
583 : "requested_quantity",
584 : inventory_products[out_of_stock_index].quantity),
585 : GNUNET_JSON_pack_uint64 (
586 : "available_quantity",
587 : 0));
588 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
589 0 : GNUNET_break (0);
590 0 : return TALER_MHD_reply_with_error (
591 : connection,
592 : MHD_HTTP_INTERNAL_SERVER_ERROR,
593 : TALER_EC_GENERIC_DB_SOFT_FAILURE,
594 : NULL);
595 0 : case GNUNET_DB_STATUS_HARD_ERROR:
596 0 : return TALER_MHD_reply_with_error (
597 : connection,
598 : MHD_HTTP_INTERNAL_SERVER_ERROR,
599 : TALER_EC_GENERIC_DB_FETCH_FAILED,
600 : NULL);
601 : }
602 0 : GNUNET_break (0);
603 0 : return MHD_NO;
604 : }
605 :
606 : /* Everything in-stock, generate positive response */
607 : {
608 : MHD_RESULT ret;
609 :
610 0 : ret = TALER_MHD_REPLY_JSON_PACK (
611 : connection,
612 : MHD_HTTP_OK,
613 : GNUNET_JSON_pack_string ("order_id",
614 : order_id),
615 : GNUNET_JSON_pack_allow_null (
616 : GNUNET_JSON_pack_data_varsize (
617 : "token",
618 : GNUNET_is_zero (claim_token)
619 : ? NULL
620 : : claim_token,
621 : sizeof (*claim_token))));
622 0 : GNUNET_JSON_parse_free (spec);
623 0 : return ret;
624 : }
625 : }
626 :
627 :
628 : /**
629 : * Add missing fields to the order. Upon success, continue
630 : * processing with execute_order().
631 : *
632 : * @param connection connection to write the result or error to
633 : * @param hc handler context for the request
634 : * @param h_post_data hash of the client's POST request, for idempotency checks
635 : * @param[in,out] order order to process (can be modified)
636 : * @param claim_token token to use for access control
637 : * @param refund_delay refund delay
638 : * @param inventory_products_length length of the @a inventory_products array
639 : * @param inventory_products array of products to add to @a order from our inventory
640 : * @param uuids_length length of the @a uuids array
641 : * @param uuids array of UUIDs used to reserve products from @a inventory_products
642 : * @return MHD result code
643 : */
644 : static MHD_RESULT
645 0 : patch_order (struct MHD_Connection *connection,
646 : struct TMH_HandlerContext *hc,
647 : const struct TALER_MerchantPostDataHashP *h_post_data,
648 : json_t *order,
649 : const struct TALER_ClaimTokenP *claim_token,
650 : struct GNUNET_TIME_Relative refund_delay,
651 : unsigned int inventory_products_length,
652 : const struct InventoryProduct inventory_products[],
653 : unsigned int uuids_length,
654 : const struct GNUNET_Uuid uuids[])
655 : {
656 0 : const struct TALER_MERCHANTDB_InstanceSettings *settings =
657 0 : &hc->instance->settings;
658 0 : const char *order_id = NULL;
659 0 : const char *fulfillment_url = NULL;
660 0 : const char *merchant_base_url = NULL;
661 0 : json_t *jmerchant = NULL;
662 0 : json_t *delivery_location = NULL;
663 0 : struct TALER_Amount max_wire_fee = { 0 };
664 0 : struct TALER_Amount max_fee = { 0 };
665 0 : uint32_t wire_fee_amortization = 0;
666 0 : struct GNUNET_TIME_Timestamp timestamp
667 : = GNUNET_TIME_UNIT_ZERO_TS;
668 0 : struct GNUNET_TIME_Timestamp delivery_date
669 : = GNUNET_TIME_UNIT_ZERO_TS;
670 0 : struct GNUNET_TIME_Timestamp refund_deadline
671 : = GNUNET_TIME_UNIT_FOREVER_TS;
672 0 : struct GNUNET_TIME_Timestamp pay_deadline
673 : = GNUNET_TIME_UNIT_ZERO_TS;
674 0 : struct GNUNET_TIME_Timestamp wire_deadline
675 : = GNUNET_TIME_UNIT_FOREVER_TS;
676 : /* auto_refund only needs to be type-checked,
677 : * mostly because in GNUnet relative times can't
678 : * be negative. */
679 : struct GNUNET_TIME_Relative auto_refund;
680 : struct GNUNET_JSON_Specification spec[] = {
681 0 : GNUNET_JSON_spec_mark_optional (
682 : GNUNET_JSON_spec_string ("merchant_base_url",
683 : &merchant_base_url),
684 : NULL),
685 0 : GNUNET_JSON_spec_mark_optional (
686 : GNUNET_JSON_spec_json ("merchant",
687 : &jmerchant),
688 : NULL),
689 0 : GNUNET_JSON_spec_mark_optional (
690 : GNUNET_JSON_spec_string ("order_id",
691 : &order_id),
692 : NULL),
693 0 : GNUNET_JSON_spec_mark_optional (
694 : GNUNET_JSON_spec_string ("fulfillment_url",
695 : &fulfillment_url),
696 : NULL),
697 0 : GNUNET_JSON_spec_mark_optional (
698 : GNUNET_JSON_spec_timestamp ("timestamp",
699 : ×tamp),
700 : NULL),
701 0 : GNUNET_JSON_spec_mark_optional (
702 : GNUNET_JSON_spec_timestamp ("refund_deadline",
703 : &refund_deadline),
704 : NULL),
705 0 : GNUNET_JSON_spec_mark_optional (
706 : GNUNET_JSON_spec_timestamp ("pay_deadline",
707 : &pay_deadline),
708 : NULL),
709 0 : GNUNET_JSON_spec_mark_optional (
710 : GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
711 : &wire_deadline),
712 : NULL),
713 0 : GNUNET_JSON_spec_mark_optional (
714 : TALER_JSON_spec_amount ("max_fee",
715 : TMH_currency,
716 : &max_fee),
717 : NULL),
718 0 : GNUNET_JSON_spec_mark_optional (
719 : TALER_JSON_spec_amount ("max_wire_fee",
720 : TMH_currency,
721 : &max_wire_fee),
722 : NULL),
723 0 : GNUNET_JSON_spec_mark_optional (
724 : GNUNET_JSON_spec_uint32 ("wire_fee_amortization",
725 : &wire_fee_amortization),
726 : NULL),
727 0 : GNUNET_JSON_spec_mark_optional (
728 : GNUNET_JSON_spec_timestamp ("delivery_date",
729 : &delivery_date),
730 : NULL),
731 0 : GNUNET_JSON_spec_mark_optional (
732 : GNUNET_JSON_spec_relative_time ("auto_refund",
733 : &auto_refund),
734 : NULL),
735 0 : GNUNET_JSON_spec_mark_optional (
736 : GNUNET_JSON_spec_json ("delivery_location",
737 : &delivery_location),
738 : NULL),
739 0 : GNUNET_JSON_spec_end ()
740 : };
741 : enum GNUNET_GenericReturnValue ret;
742 :
743 0 : ret = TALER_MHD_parse_json_data (connection,
744 : order,
745 : spec);
746 0 : if (GNUNET_OK != ret)
747 : {
748 0 : GNUNET_break_op (0);
749 : return (GNUNET_NO == ret)
750 : ? MHD_YES
751 0 : : MHD_NO;
752 : }
753 :
754 : /* Add order_id if it doesn't exist. */
755 0 : if (NULL == order_id)
756 : {
757 : char buf[256];
758 : time_t timer;
759 : struct tm *tm_info;
760 : size_t off;
761 : uint64_t rand;
762 : char *last;
763 : json_t *jbuf;
764 :
765 0 : time (&timer);
766 0 : tm_info = localtime (&timer);
767 0 : if (NULL == tm_info)
768 : {
769 0 : return TALER_MHD_reply_with_error (
770 : connection,
771 : MHD_HTTP_INTERNAL_SERVER_ERROR,
772 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME,
773 : NULL);
774 : }
775 0 : off = strftime (buf,
776 : sizeof (buf) - 1,
777 : "%Y.%j",
778 : tm_info);
779 : /* Check for error state of strftime */
780 0 : GNUNET_assert (0 != off);
781 0 : buf[off++] = '-';
782 0 : rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK,
783 : UINT64_MAX);
784 0 : last = GNUNET_STRINGS_data_to_string (&rand,
785 : sizeof (uint64_t),
786 : &buf[off],
787 : sizeof (buf) - off);
788 0 : GNUNET_assert (NULL != last);
789 0 : *last = '\0';
790 0 : jbuf = json_string (buf);
791 0 : GNUNET_assert (NULL != jbuf);
792 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
793 : "Assigning order ID `%s' server-side\n",
794 : buf);
795 0 : GNUNET_break (0 ==
796 : json_object_set_new (order,
797 : "order_id",
798 : jbuf));
799 0 : order_id = json_string_value (jbuf);
800 0 : GNUNET_assert (NULL != order_id);
801 : }
802 :
803 : /* Patch fulfillment URL with order_id (implements #6467). */
804 0 : if (NULL != fulfillment_url)
805 : {
806 : const char *pos;
807 :
808 0 : pos = strstr (fulfillment_url,
809 : "${ORDER_ID}");
810 0 : if (NULL != pos)
811 : {
812 : /* replace ${ORDER_ID} with the real order_id */
813 : char *nurl;
814 :
815 : /* We only allow one placeholder */
816 0 : if (strstr (pos + strlen ("${ORDER_ID}"),
817 : "${ORDER_ID}"))
818 : {
819 : /* FIXME: free anything? */
820 0 : GNUNET_break_op (0);
821 0 : return TALER_MHD_reply_with_error (connection,
822 : MHD_HTTP_BAD_REQUEST,
823 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
824 : "fulfillment_url");
825 : }
826 :
827 0 : GNUNET_asprintf (&nurl,
828 : "%.*s%s%s",
829 : /* first output URL until ${ORDER_ID} */
830 0 : (int) (pos - fulfillment_url),
831 : fulfillment_url,
832 : /* replace ${ORDER_ID} with the right order_id */
833 : order_id,
834 : /* append rest of original URL */
835 : pos + strlen ("${ORDER_ID}"));
836 : /* replace in JSON of the order */
837 0 : GNUNET_break (0 ==
838 : json_object_set_new (order,
839 : "fulfillment_url",
840 : json_string (nurl)));
841 0 : GNUNET_free (nurl);
842 : }
843 : }
844 :
845 : /* Check soundness of refund deadline, and that a timestamp
846 : * is actually present. */
847 : {
848 0 : struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
849 :
850 : /* Add timestamp if it doesn't exist (or is zero) */
851 0 : if (GNUNET_TIME_absolute_is_zero (timestamp.abs_time))
852 : {
853 0 : GNUNET_assert (0 ==
854 : json_object_set_new (order,
855 : "timestamp",
856 : GNUNET_JSON_from_timestamp (now)));
857 : }
858 :
859 : /* If no refund_deadline given, set one based on refund_delay. */
860 0 : if (GNUNET_TIME_absolute_is_never (refund_deadline.abs_time))
861 : {
862 0 : if (GNUNET_TIME_relative_is_zero (refund_delay))
863 : {
864 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
865 : "Refund delay is zero, no refunds are possible for this order\n");
866 0 : refund_deadline = now; /* if delay was 0, ensure that refund_deadline == timestamp */
867 : }
868 : else
869 : {
870 0 : refund_deadline = GNUNET_TIME_relative_to_timestamp (refund_delay);
871 : }
872 :
873 0 : GNUNET_assert (0 ==
874 : json_object_set_new (order,
875 : "refund_deadline",
876 : GNUNET_JSON_from_timestamp (
877 : refund_deadline)));
878 : }
879 0 : if ( (! GNUNET_TIME_absolute_is_zero (delivery_date.abs_time)) &&
880 0 : (GNUNET_TIME_timestamp_cmp (delivery_date,
881 : <,
882 : now)) )
883 : {
884 0 : GNUNET_break_op (0);
885 0 : return TALER_MHD_reply_with_error (
886 : connection,
887 : MHD_HTTP_BAD_REQUEST,
888 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST,
889 : NULL);
890 : }
891 : }
892 :
893 0 : if (GNUNET_TIME_absolute_is_zero (pay_deadline.abs_time))
894 : {
895 : struct GNUNET_TIME_Timestamp t;
896 :
897 0 : t = GNUNET_TIME_relative_to_timestamp (settings->default_pay_delay);
898 0 : GNUNET_assert (0 ==
899 : json_object_set_new (order,
900 : "pay_deadline",
901 : GNUNET_JSON_from_timestamp (t)));
902 : }
903 :
904 0 : if (GNUNET_TIME_absolute_is_never (wire_deadline.abs_time))
905 : {
906 : struct GNUNET_TIME_Timestamp t;
907 :
908 0 : t = GNUNET_TIME_relative_to_timestamp (
909 : GNUNET_TIME_relative_max (settings->default_wire_transfer_delay,
910 : refund_delay));
911 0 : wire_deadline = GNUNET_TIME_timestamp_max (refund_deadline,
912 : t);
913 0 : if (GNUNET_TIME_absolute_is_never (wire_deadline.abs_time))
914 : {
915 0 : GNUNET_break_op (0);
916 0 : return TALER_MHD_reply_with_error (
917 : connection,
918 : MHD_HTTP_BAD_REQUEST,
919 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER,
920 : "order:wire_transfer_deadline");
921 :
922 : }
923 0 : GNUNET_assert (0 ==
924 : json_object_set_new (order,
925 : "wire_transfer_deadline",
926 : GNUNET_JSON_from_timestamp (
927 : wire_deadline)));
928 : }
929 0 : if (GNUNET_TIME_timestamp_cmp (wire_deadline,
930 : <,
931 : refund_deadline))
932 : {
933 0 : GNUNET_break_op (0);
934 0 : return TALER_MHD_reply_with_error (
935 : connection,
936 : MHD_HTTP_BAD_REQUEST,
937 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE,
938 : "order:wire_transfer_deadline;order:refund_deadline");
939 : }
940 :
941 : /* Note: total amount currency match checked
942 : later in execute_order() */
943 0 : if (GNUNET_OK !=
944 0 : TALER_amount_is_valid (&max_wire_fee))
945 : {
946 0 : GNUNET_assert (0 ==
947 : json_object_set_new (
948 : order,
949 : "max_wire_fee",
950 : TALER_JSON_from_amount (&settings->default_max_wire_fee)));
951 : }
952 :
953 0 : if (GNUNET_OK !=
954 0 : TALER_amount_is_valid (&max_fee))
955 : {
956 0 : GNUNET_assert (0 ==
957 : json_object_set_new (
958 : order,
959 : "max_fee",
960 : TALER_JSON_from_amount (
961 : &settings->default_max_deposit_fee)));
962 : }
963 0 : if (0 == wire_fee_amortization)
964 : {
965 0 : GNUNET_assert (0 ==
966 : json_object_set_new (
967 : order,
968 : "wire_fee_amortization",
969 : json_integer (
970 : (json_int_t) settings->default_wire_fee_amortization)));
971 : }
972 0 : if (NULL == merchant_base_url)
973 : {
974 : char *url;
975 :
976 0 : url = make_merchant_base_url (connection,
977 0 : settings->id);
978 0 : GNUNET_assert (0 ==
979 : json_object_set_new (order,
980 : "merchant_base_url",
981 : json_string (url)));
982 0 : GNUNET_free (url);
983 : }
984 0 : else if (('\0' == *merchant_base_url) ||
985 0 : ('/' != merchant_base_url[strlen (merchant_base_url) - 1]))
986 : {
987 0 : GNUNET_break_op (0);
988 0 : return TALER_MHD_reply_with_error (
989 : connection,
990 : MHD_HTTP_BAD_REQUEST,
991 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR,
992 : "merchant_base_url is not valid");
993 : }
994 :
995 : /* Fill in merchant information if necessary */
996 0 : if (NULL != jmerchant)
997 : {
998 0 : GNUNET_break_op (0);
999 0 : return TALER_MHD_reply_with_error (
1000 : connection,
1001 : MHD_HTTP_BAD_REQUEST,
1002 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR,
1003 : "'merchant' field already set, but must be provided by backend");
1004 : }
1005 0 : jmerchant = GNUNET_JSON_PACK (
1006 : GNUNET_JSON_pack_string ("name",
1007 : settings->name),
1008 : GNUNET_JSON_pack_allow_null (
1009 : GNUNET_JSON_pack_string ("website",
1010 : settings->website)),
1011 : GNUNET_JSON_pack_allow_null (
1012 : GNUNET_JSON_pack_string ("email",
1013 : settings->email)),
1014 : GNUNET_JSON_pack_allow_null (
1015 : GNUNET_JSON_pack_string ("logo",
1016 : settings->logo)));
1017 0 : GNUNET_assert (NULL != jmerchant);
1018 : {
1019 : json_t *loca;
1020 :
1021 : /* Handle merchant address */
1022 0 : loca = settings->address;
1023 0 : if (NULL != loca)
1024 : {
1025 0 : loca = json_deep_copy (loca);
1026 0 : GNUNET_assert (0 ==
1027 : json_object_set_new (jmerchant,
1028 : "address",
1029 : loca));
1030 : }
1031 : }
1032 : {
1033 : json_t *locj;
1034 :
1035 : /* Handle merchant jurisdiction */
1036 0 : locj = settings->jurisdiction;
1037 0 : if (NULL != locj)
1038 : {
1039 0 : locj = json_deep_copy (locj);
1040 0 : GNUNET_assert (0 ==
1041 : json_object_set_new (jmerchant,
1042 : "jurisdiction",
1043 : locj));
1044 : }
1045 : }
1046 0 : GNUNET_assert (0 ==
1047 : json_object_set_new (order,
1048 : "merchant",
1049 : jmerchant));
1050 :
1051 : /* add fields to the contract that the backend should provide */
1052 0 : GNUNET_assert (0 ==
1053 : json_object_set (order,
1054 : "exchanges",
1055 : TMH_trusted_exchanges));
1056 0 : GNUNET_assert (0 ==
1057 : json_object_set (order,
1058 : "auditors",
1059 : j_auditors));
1060 0 : GNUNET_assert (0 ==
1061 : json_object_set_new (order,
1062 : "merchant_pub",
1063 : GNUNET_JSON_from_data_auto (
1064 : &hc->instance->merchant_pub)));
1065 0 : if (GNUNET_OK !=
1066 0 : TALER_JSON_contract_seed_forgettable (order))
1067 : {
1068 0 : return TALER_MHD_reply_with_error (
1069 : connection,
1070 : MHD_HTTP_BAD_REQUEST,
1071 : TALER_EC_GENERIC_JSON_INVALID,
1072 : "could not compute hash of order due to bogus forgettable fields");
1073 : }
1074 :
1075 0 : if ( (NULL != delivery_location) &&
1076 0 : (! TMH_location_object_valid (delivery_location)) )
1077 : {
1078 0 : GNUNET_break_op (0);
1079 0 : GNUNET_JSON_parse_free (spec);
1080 0 : return TALER_MHD_reply_with_error (connection,
1081 : MHD_HTTP_BAD_REQUEST,
1082 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1083 : "delivery_location");
1084 : }
1085 :
1086 : /* sanity check result */
1087 : {
1088 : struct TALER_PrivateContractHashP h_control;
1089 :
1090 0 : switch (TALER_JSON_contract_hash (order,
1091 : &h_control))
1092 : {
1093 0 : case GNUNET_SYSERR:
1094 0 : GNUNET_break (0);
1095 0 : return TALER_MHD_reply_with_error (
1096 : connection,
1097 : MHD_HTTP_INTERNAL_SERVER_ERROR,
1098 : TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
1099 : "could not compute hash of patched order");
1100 0 : case GNUNET_NO:
1101 0 : GNUNET_break_op (0);
1102 0 : return TALER_MHD_reply_with_error (
1103 : connection,
1104 : MHD_HTTP_BAD_REQUEST,
1105 : TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
1106 : "order contained unallowed values");
1107 0 : case GNUNET_OK:
1108 0 : break;
1109 : }
1110 0 : }
1111 0 : return execute_order (connection,
1112 : hc,
1113 : h_post_data,
1114 : order,
1115 : claim_token,
1116 : inventory_products_length,
1117 : inventory_products,
1118 : uuids_length,
1119 : uuids);
1120 : }
1121 :
1122 :
1123 : /**
1124 : * Process the @a payment_target and add the details of how the
1125 : * order could be paid to @a order. On success, continue
1126 : * processing with patch_order().
1127 : *
1128 : * @param connection connection to write the result or error to
1129 : * @param hc handler context for the request
1130 : * @param h_post_data hash of the client's POST request, for idempotency checks
1131 : * @param[in,out] order order to process (can be modified)
1132 : * @param claim_token token to use for access control
1133 : * @param refund_delay refund delay
1134 : * @param payment_target desired wire method, NULL for no preference
1135 : * @param inventory_products_length length of the @a inventory_products array
1136 : * @param inventory_products array of products to add to @a order from our inventory
1137 : * @param uuids_length length of the @a uuids array
1138 : * @param uuids array of UUIDs used to reserve products from @a inventory_products
1139 : * @return MHD result code
1140 : */
1141 : static MHD_RESULT
1142 0 : add_payment_details (struct MHD_Connection *connection,
1143 : struct TMH_HandlerContext *hc,
1144 : const struct TALER_MerchantPostDataHashP *h_post_data,
1145 : json_t *order,
1146 : const struct TALER_ClaimTokenP *claim_token,
1147 : struct GNUNET_TIME_Relative refund_delay,
1148 : const char *payment_target,
1149 : unsigned int inventory_products_length,
1150 : const struct InventoryProduct inventory_products[],
1151 : unsigned int uuids_length,
1152 : const struct GNUNET_Uuid uuids[])
1153 : {
1154 : struct TMH_WireMethod *wm;
1155 :
1156 0 : wm = hc->instance->wm_head;
1157 : /* Locate wire method that has a matching payment target */
1158 0 : while ( (NULL != wm) &&
1159 0 : ( (! wm->active) ||
1160 0 : ( (NULL != payment_target) &&
1161 0 : (0 != strcasecmp (payment_target,
1162 0 : wm->wire_method) ) ) ) )
1163 0 : wm = wm->next;
1164 0 : if (NULL == wm)
1165 : {
1166 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
1167 : "No wire method available for instance '%s'\n",
1168 : hc->instance->settings.id);
1169 0 : return TALER_MHD_reply_with_error (connection,
1170 : MHD_HTTP_NOT_FOUND,
1171 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE,
1172 : payment_target);
1173 : }
1174 0 : GNUNET_assert (0 ==
1175 : json_object_set_new (order,
1176 : "h_wire",
1177 : GNUNET_JSON_from_data_auto (
1178 : &wm->h_wire)));
1179 0 : GNUNET_assert (0 ==
1180 : json_object_set_new (order,
1181 : "wire_method",
1182 : json_string (wm->wire_method)));
1183 0 : return patch_order (connection,
1184 : hc,
1185 : h_post_data,
1186 : order,
1187 : claim_token,
1188 : refund_delay,
1189 : inventory_products_length,
1190 : inventory_products,
1191 : uuids_length,
1192 : uuids);
1193 : }
1194 :
1195 :
1196 : /**
1197 : * Merge the inventory products into @a order, querying the
1198 : * database about the details of those products. Upon success,
1199 : * continue processing by calling add_payment_details().
1200 : *
1201 : * @param connection connection to write the result or error to
1202 : * @param hc handler context for the request
1203 : * @param h_post_data hash of the client's POST request, for idempotency checks
1204 : * @param[in,out] order order to process (can be modified)
1205 : * @param claim_token token to use for access control
1206 : * @param refund_delay time window where it is possible to ask a refund
1207 : * @param payment_target RFC8905 payment target type to find a matching merchant account
1208 : * @param inventory_products_length length of the @a inventory_products array
1209 : * @param inventory_products array of products to add to @a order from our inventory
1210 : * @param uuids_length length of the @a uuids array
1211 : * @param uuids array of UUIDs used to reserve products from @a inventory_products
1212 : * @return MHD result code
1213 : */
1214 : static MHD_RESULT
1215 0 : merge_inventory (struct MHD_Connection *connection,
1216 : struct TMH_HandlerContext *hc,
1217 : const struct TALER_MerchantPostDataHashP *h_post_data,
1218 : json_t *order,
1219 : const struct TALER_ClaimTokenP *claim_token,
1220 : struct GNUNET_TIME_Relative refund_delay,
1221 : const char *payment_target,
1222 : unsigned int inventory_products_length,
1223 : const struct InventoryProduct inventory_products[],
1224 : unsigned int uuids_length,
1225 : const struct GNUNET_Uuid uuids[])
1226 : {
1227 : /**
1228 : * inventory_products => instructions to add products to contract terms
1229 : * order.products => contains products that are not from the backend-managed inventory.
1230 : */
1231 0 : GNUNET_assert (NULL != order);
1232 : {
1233 0 : json_t *jprod = json_object_get (order,
1234 : "products");
1235 0 : if (NULL == jprod)
1236 : {
1237 0 : GNUNET_assert (0 ==
1238 : json_object_set_new (order,
1239 : "products",
1240 : json_array ()));
1241 : }
1242 0 : else if (! TMH_products_array_valid (jprod))
1243 : {
1244 0 : return TALER_MHD_reply_with_error (connection,
1245 : MHD_HTTP_BAD_REQUEST,
1246 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1247 : "order.products");
1248 : }
1249 : }
1250 :
1251 : /* Populate products from inventory product array and database */
1252 : {
1253 0 : json_t *np = json_array ();
1254 :
1255 0 : for (unsigned int i = 0; i<inventory_products_length; i++)
1256 : {
1257 : struct TALER_MERCHANTDB_ProductDetails pd;
1258 : enum GNUNET_DB_QueryStatus qs;
1259 :
1260 0 : qs = TMH_db->lookup_product (TMH_db->cls,
1261 0 : hc->instance->settings.id,
1262 0 : inventory_products[i].product_id,
1263 : &pd);
1264 0 : if (qs <= 0)
1265 : {
1266 0 : enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
1267 0 : unsigned int http_status = 0;
1268 :
1269 0 : switch (qs)
1270 : {
1271 0 : case GNUNET_DB_STATUS_HARD_ERROR:
1272 0 : GNUNET_break (0);
1273 0 : http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
1274 0 : ec = TALER_EC_GENERIC_DB_FETCH_FAILED;
1275 0 : break;
1276 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
1277 0 : GNUNET_break (0);
1278 0 : http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
1279 0 : ec = TALER_EC_GENERIC_DB_SOFT_FAILURE;
1280 0 : break;
1281 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
1282 0 : http_status = MHD_HTTP_NOT_FOUND;
1283 0 : ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN;
1284 0 : break;
1285 0 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
1286 : /* case listed to make compilers happy */
1287 0 : GNUNET_assert (0);
1288 : }
1289 0 : json_decref (np);
1290 0 : return TALER_MHD_reply_with_error (connection,
1291 : http_status,
1292 : ec,
1293 0 : inventory_products[i].product_id);
1294 : }
1295 : {
1296 : json_t *p;
1297 :
1298 0 : p = GNUNET_JSON_PACK (
1299 : GNUNET_JSON_pack_string ("description",
1300 : pd.description),
1301 : GNUNET_JSON_pack_object_steal ("description_i18n",
1302 : pd.description_i18n),
1303 : GNUNET_JSON_pack_string ("unit",
1304 : pd.unit),
1305 : TALER_JSON_pack_amount ("price",
1306 : &pd.price),
1307 : GNUNET_JSON_pack_array_steal ("taxes",
1308 : pd.taxes),
1309 : GNUNET_JSON_pack_string ("image",
1310 : pd.image),
1311 : GNUNET_JSON_pack_uint64 ("quantity",
1312 : inventory_products[i].
1313 : quantity));
1314 0 : GNUNET_assert (NULL != p);
1315 0 : GNUNET_assert (0 ==
1316 : json_array_append_new (np,
1317 : p));
1318 : }
1319 0 : GNUNET_free (pd.description);
1320 0 : GNUNET_free (pd.unit);
1321 0 : GNUNET_free (pd.image);
1322 0 : json_decref (pd.address);
1323 : }
1324 : /* merge into existing products list */
1325 : {
1326 : json_t *xp;
1327 :
1328 0 : xp = json_object_get (order,
1329 : "products");
1330 0 : GNUNET_assert (NULL != xp);
1331 0 : json_array_extend (xp, np);
1332 0 : json_decref (np);
1333 : }
1334 : }
1335 0 : return add_payment_details (connection,
1336 : hc,
1337 : h_post_data,
1338 : order,
1339 : claim_token,
1340 : refund_delay,
1341 : payment_target,
1342 : inventory_products_length,
1343 : inventory_products,
1344 : uuids_length,
1345 : uuids);
1346 : }
1347 :
1348 :
1349 : /**
1350 : * Generate an order. We add the fields 'exchanges', 'merchant_pub', and
1351 : * 'H_wire' to the order gotten from the frontend, as well as possibly other
1352 : * fields if the frontend did not provide them. Returns the order_id.
1353 : *
1354 : * @param rh context of the handler
1355 : * @param connection the MHD connection to handle
1356 : * @param[in,out] hc context with further information about the request
1357 : * @return MHD result code
1358 : */
1359 : MHD_RESULT
1360 0 : TMH_private_post_orders (const struct TMH_RequestHandler *rh,
1361 : struct MHD_Connection *connection,
1362 : struct TMH_HandlerContext *hc)
1363 : {
1364 : json_t *order;
1365 0 : struct GNUNET_TIME_Relative refund_delay = GNUNET_TIME_UNIT_ZERO;
1366 0 : const char *payment_target = NULL;
1367 0 : json_t *ip = NULL;
1368 0 : unsigned int ips_len = 0;
1369 0 : struct InventoryProduct *ips = NULL;
1370 0 : unsigned int uuids_len = 0;
1371 : json_t *uuid;
1372 0 : struct GNUNET_Uuid *uuids = NULL;
1373 : struct TALER_ClaimTokenP claim_token;
1374 0 : bool create_token = true; /* default */
1375 : struct GNUNET_JSON_Specification spec[] = {
1376 0 : GNUNET_JSON_spec_json ("order",
1377 : &order),
1378 0 : GNUNET_JSON_spec_mark_optional (
1379 : GNUNET_JSON_spec_relative_time ("refund_delay",
1380 : &refund_delay),
1381 : NULL),
1382 0 : GNUNET_JSON_spec_mark_optional (
1383 : GNUNET_JSON_spec_string ("payment_target",
1384 : &payment_target),
1385 : NULL),
1386 0 : GNUNET_JSON_spec_mark_optional (
1387 : GNUNET_JSON_spec_json ("inventory_products",
1388 : &ip),
1389 : NULL),
1390 0 : GNUNET_JSON_spec_mark_optional (
1391 : GNUNET_JSON_spec_json ("lock_uuids",
1392 : &uuid),
1393 : NULL),
1394 0 : GNUNET_JSON_spec_mark_optional (
1395 : GNUNET_JSON_spec_bool ("create_token",
1396 : &create_token),
1397 : NULL),
1398 0 : GNUNET_JSON_spec_end ()
1399 : };
1400 : enum GNUNET_GenericReturnValue ret;
1401 : struct TALER_MerchantPostDataHashP h_post_data;
1402 :
1403 : (void) rh;
1404 0 : ret = TALER_MHD_parse_json_data (connection,
1405 0 : hc->request_body,
1406 : spec);
1407 0 : if (GNUNET_OK != ret)
1408 : return (GNUNET_NO == ret)
1409 : ? MHD_YES
1410 0 : : MHD_NO;
1411 :
1412 0 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
1413 : "Refund delay is %s\n",
1414 : GNUNET_TIME_relative2s (refund_delay,
1415 : false));
1416 :
1417 0 : TMH_db->expire_locks (TMH_db->cls);
1418 0 : if (create_token)
1419 : {
1420 0 : GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
1421 : &claim_token,
1422 : sizeof (claim_token));
1423 : }
1424 : else
1425 : {
1426 : /* we use all-zeros for 'no token' */
1427 0 : memset (&claim_token,
1428 : 0,
1429 : sizeof (claim_token));
1430 : }
1431 :
1432 : /* Compute h_post_data (for idempotency check) */
1433 : {
1434 : char *req_body_enc;
1435 :
1436 : /* Dump normalized JSON to string. */
1437 0 : if (NULL == (req_body_enc = json_dumps (hc->request_body,
1438 : JSON_ENCODE_ANY
1439 : | JSON_COMPACT
1440 : | JSON_SORT_KEYS)))
1441 : {
1442 0 : GNUNET_break (0);
1443 0 : GNUNET_JSON_parse_free (spec);
1444 0 : return TALER_MHD_reply_with_error (connection,
1445 : MHD_HTTP_INTERNAL_SERVER_ERROR,
1446 : TALER_EC_GENERIC_ALLOCATION_FAILURE,
1447 : "request body normalization for hashing");
1448 : }
1449 0 : GNUNET_CRYPTO_hash (req_body_enc,
1450 : strlen (req_body_enc),
1451 : &h_post_data.hash);
1452 0 : GNUNET_free (req_body_enc);
1453 : }
1454 :
1455 : /* parse the inventory_products (optionally given) */
1456 0 : if (NULL != ip)
1457 : {
1458 0 : if (! json_is_array (ip))
1459 : {
1460 0 : GNUNET_JSON_parse_free (spec);
1461 0 : return TALER_MHD_reply_with_error (connection,
1462 : MHD_HTTP_BAD_REQUEST,
1463 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1464 : "inventory_products");
1465 : }
1466 0 : GNUNET_array_grow (ips,
1467 : ips_len,
1468 : json_array_size (ip));
1469 0 : for (unsigned int i = 0; i<ips_len; i++)
1470 : {
1471 : const char *error_name;
1472 : unsigned int error_line;
1473 : struct GNUNET_JSON_Specification ispec[] = {
1474 0 : GNUNET_JSON_spec_string ("product_id",
1475 0 : &ips[i].product_id),
1476 0 : GNUNET_JSON_spec_uint32 ("quantity",
1477 0 : &ips[i].quantity),
1478 0 : GNUNET_JSON_spec_end ()
1479 : };
1480 :
1481 0 : ret = GNUNET_JSON_parse (json_array_get (ip,
1482 : i),
1483 : ispec,
1484 : &error_name,
1485 : &error_line);
1486 0 : if (GNUNET_OK != ret)
1487 : {
1488 0 : GNUNET_break_op (0);
1489 0 : GNUNET_array_grow (ips,
1490 : ips_len,
1491 : 0);
1492 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
1493 : "Product parsing failed at #%u: %s:%u\n",
1494 : i,
1495 : error_name,
1496 : error_line);
1497 0 : GNUNET_JSON_parse_free (spec);
1498 0 : return TALER_MHD_reply_with_error (connection,
1499 : MHD_HTTP_BAD_REQUEST,
1500 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1501 : "inventory_products");
1502 : }
1503 : }
1504 : }
1505 :
1506 : /* parse the lock_uuids (optionally given) */
1507 0 : if (NULL != uuid)
1508 : {
1509 0 : if (! json_is_array (uuid))
1510 : {
1511 0 : GNUNET_array_grow (ips,
1512 : ips_len,
1513 : 0);
1514 0 : GNUNET_JSON_parse_free (spec);
1515 0 : return TALER_MHD_reply_with_error (connection,
1516 : MHD_HTTP_BAD_REQUEST,
1517 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1518 : "lock_uuids");
1519 : }
1520 0 : GNUNET_array_grow (uuids,
1521 : uuids_len,
1522 : json_array_size (uuid));
1523 0 : for (unsigned int i = 0; i<uuids_len; i++)
1524 : {
1525 0 : json_t *ui = json_array_get (uuid,
1526 : i);
1527 :
1528 0 : if (! json_is_string (ui))
1529 : {
1530 0 : GNUNET_break_op (0);
1531 0 : GNUNET_array_grow (ips,
1532 : ips_len,
1533 : 0);
1534 0 : GNUNET_array_grow (uuids,
1535 : uuids_len,
1536 : 0);
1537 0 : GNUNET_JSON_parse_free (spec);
1538 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
1539 : "UUID parsing failed at #%u\n",
1540 : i);
1541 0 : return TALER_MHD_reply_with_error (connection,
1542 : MHD_HTTP_BAD_REQUEST,
1543 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1544 : "lock_uuids");
1545 : }
1546 0 : TMH_uuid_from_string (json_string_value (ui),
1547 0 : &uuids[i]);
1548 : }
1549 : }
1550 :
1551 : /* Finally, start by completing the order */
1552 : {
1553 : MHD_RESULT res;
1554 :
1555 0 : res = merge_inventory (connection,
1556 : hc,
1557 : &h_post_data,
1558 : order,
1559 : &claim_token,
1560 : refund_delay,
1561 : payment_target,
1562 : ips_len,
1563 : ips,
1564 : uuids_len,
1565 : uuids);
1566 0 : GNUNET_array_grow (ips,
1567 : ips_len,
1568 : 0);
1569 0 : GNUNET_array_grow (uuids,
1570 : uuids_len,
1571 : 0);
1572 0 : GNUNET_JSON_parse_free (spec);
1573 0 : return res;
1574 : }
1575 : }
1576 :
1577 :
1578 : /* end of taler-merchant-httpd_private-post-orders.c */
|