Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2020-2026 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify
6 : it under the terms of the GNU Lesser General Public License as
7 : published by the Free Software Foundation; either version 2.1,
8 : or (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful,
11 : but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : GNU Lesser General Public License for more details.
14 :
15 : You should have received a copy of the GNU Lesser General
16 : Public License along with TALER; see the file COPYING.LGPL.
17 : If not, see <http://www.gnu.org/licenses/>
18 : */
19 : /**
20 : * @file merchant_api_post-private-products-new.c
21 : * @brief Implementation of the POST /private/products request
22 : * @author Christian Grothoff
23 : */
24 : #include "taler/platform.h"
25 : #include <curl/curl.h>
26 : #include <jansson.h>
27 : #include <microhttpd.h> /* just for HTTP status codes */
28 : #include <gnunet/gnunet_util_lib.h>
29 : #include <gnunet/gnunet_curl_lib.h>
30 : #include <taler/merchant/post-private-products.h>
31 : #include "merchant_api_curl_defaults.h"
32 : #include "merchant_api_common.h"
33 : #include <taler/taler_json_lib.h>
34 : #include <taler/taler_curl_lib.h>
35 :
36 :
37 : /**
38 : * Handle for a POST /private/products operation.
39 : */
40 : struct TALER_MERCHANT_PostPrivateProductsHandle
41 : {
42 : /**
43 : * Base URL of the merchant backend.
44 : */
45 : char *base_url;
46 :
47 : /**
48 : * The full URL for this request.
49 : */
50 : char *url;
51 :
52 : /**
53 : * Handle for the request.
54 : */
55 : struct GNUNET_CURL_Job *job;
56 :
57 : /**
58 : * Function to call with the result.
59 : */
60 : TALER_MERCHANT_PostPrivateProductsCallback cb;
61 :
62 : /**
63 : * Closure for @a cb.
64 : */
65 : TALER_MERCHANT_POST_PRIVATE_PRODUCTS_RESULT_CLOSURE *cb_cls;
66 :
67 : /**
68 : * Reference to the execution context.
69 : */
70 : struct GNUNET_CURL_Context *ctx;
71 :
72 : /**
73 : * Minor context that holds body and headers.
74 : */
75 : struct TALER_CURL_PostContext post_ctx;
76 :
77 : /**
78 : * Product identifier.
79 : */
80 : char *product_id;
81 :
82 : /**
83 : * Human-readable description.
84 : */
85 : char *description;
86 :
87 : /**
88 : * Unit of measurement.
89 : */
90 : char *unit;
91 :
92 : /**
93 : * Product image (base64-encoded or empty), set via option.
94 : */
95 : char *image;
96 :
97 : /**
98 : * Explicit product name (if different from description).
99 : */
100 : char *product_name;
101 :
102 : /**
103 : * Total stock (-1 for unlimited).
104 : */
105 : int64_t total_stock;
106 :
107 : /**
108 : * Product group ID.
109 : */
110 : uint64_t product_group_id;
111 :
112 : /**
113 : * Whether product_group_id has been set.
114 : */
115 : bool have_product_group_id;
116 :
117 : /**
118 : * Money pot ID.
119 : */
120 : uint64_t money_pot_id;
121 :
122 : /**
123 : * Whether money_pot_id has been set.
124 : */
125 : bool have_money_pot_id;
126 :
127 : /**
128 : * Whether the price is net (before tax).
129 : */
130 : bool price_is_net;
131 :
132 : /**
133 : * Whether price_is_net has been set.
134 : */
135 : bool have_price_is_net;
136 :
137 : /**
138 : * Optional internationalized descriptions (JSON).
139 : */
140 : json_t *description_i18n;
141 :
142 : /**
143 : * Optional tax information (JSON array).
144 : */
145 : json_t *taxes;
146 :
147 : /**
148 : * Optional storage location (JSON).
149 : */
150 : json_t *address;
151 :
152 : /**
153 : * Optional expected restock time.
154 : */
155 : struct GNUNET_TIME_Timestamp next_restock;
156 :
157 : /**
158 : * Whether next_restock has been set.
159 : */
160 : bool have_next_restock;
161 :
162 : /**
163 : * Optional minimum age requirement.
164 : */
165 : uint32_t minimum_age;
166 :
167 : /**
168 : * Whether minimum_age has been set.
169 : */
170 : bool have_minimum_age;
171 :
172 : /**
173 : * Optional category IDs.
174 : */
175 : uint64_t *cats;
176 :
177 : /**
178 : * Number of category IDs.
179 : */
180 : unsigned int num_cats;
181 :
182 : /**
183 : * Array of unit prices.
184 : */
185 : struct TALER_Amount *unit_prices;
186 :
187 : /**
188 : * Length of @e unit_prices array.
189 : */
190 : size_t unit_prices_len;
191 :
192 : /**
193 : * Fractional part of total stock.
194 : */
195 : uint32_t total_stock_frac;
196 :
197 : /**
198 : * Whether fractional quantities are allowed.
199 : */
200 : bool unit_allow_fraction;
201 :
202 : /**
203 : * Whether unit_allow_fraction has been set.
204 : */
205 : bool have_unit_allow_fraction;
206 :
207 : /**
208 : * Precision level for fractions.
209 : */
210 : uint32_t unit_precision_level;
211 :
212 : /**
213 : * Whether unit_precision_level has been set.
214 : */
215 : bool have_unit_precision_level;
216 : };
217 :
218 :
219 : /**
220 : * Function called when we're done processing the
221 : * HTTP POST /private/products request.
222 : *
223 : * @param cls the `struct TALER_MERCHANT_PostPrivateProductsHandle`
224 : * @param response_code HTTP response code, 0 on error
225 : * @param response response body, NULL if not in JSON
226 : */
227 : static void
228 0 : handle_post_products_finished (void *cls,
229 : long response_code,
230 : const void *response)
231 : {
232 0 : struct TALER_MERCHANT_PostPrivateProductsHandle *ppph = cls;
233 0 : const json_t *json = response;
234 0 : struct TALER_MERCHANT_PostPrivateProductsResponse ppr = {
235 0 : .hr.http_status = (unsigned int) response_code,
236 : .hr.reply = json
237 : };
238 :
239 0 : ppph->job = NULL;
240 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
241 : "POST /private/products completed with response code %u\n",
242 : (unsigned int) response_code);
243 0 : switch (response_code)
244 : {
245 0 : case 0:
246 0 : ppr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
247 0 : break;
248 0 : case MHD_HTTP_NO_CONTENT:
249 0 : break;
250 0 : case MHD_HTTP_BAD_REQUEST:
251 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
252 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
253 0 : break;
254 0 : case MHD_HTTP_UNAUTHORIZED:
255 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
256 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
257 0 : break;
258 0 : case MHD_HTTP_FORBIDDEN:
259 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
260 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
261 0 : break;
262 0 : case MHD_HTTP_NOT_FOUND:
263 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
264 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
265 0 : break;
266 0 : case MHD_HTTP_CONFLICT:
267 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
268 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
269 0 : break;
270 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
271 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
272 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
273 0 : break;
274 0 : default:
275 0 : TALER_MERCHANT_parse_error_details_ (json,
276 : response_code,
277 : &ppr.hr);
278 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
279 : "Unexpected response code %u/%d\n",
280 : (unsigned int) response_code,
281 : (int) ppr.hr.ec);
282 0 : GNUNET_break_op (0);
283 0 : break;
284 : }
285 0 : ppph->cb (ppph->cb_cls,
286 : &ppr);
287 0 : TALER_MERCHANT_post_private_products_cancel (ppph);
288 0 : }
289 :
290 :
291 : struct TALER_MERCHANT_PostPrivateProductsHandle *
292 0 : TALER_MERCHANT_post_private_products_create (
293 : struct GNUNET_CURL_Context *ctx,
294 : const char *url,
295 : const char *product_id,
296 : const char *description,
297 : const char *unit,
298 : unsigned int num_prices,
299 : const struct TALER_Amount prices[static num_prices])
300 0 : {
301 : struct TALER_MERCHANT_PostPrivateProductsHandle *ppph;
302 :
303 0 : ppph = GNUNET_new (struct TALER_MERCHANT_PostPrivateProductsHandle);
304 0 : ppph->ctx = ctx;
305 0 : ppph->base_url = GNUNET_strdup (url);
306 0 : ppph->product_id = GNUNET_strdup (product_id);
307 0 : ppph->description = GNUNET_strdup (description);
308 0 : ppph->unit = GNUNET_strdup (unit);
309 0 : ppph->unit_prices_len = num_prices;
310 0 : ppph->unit_prices = GNUNET_new_array (num_prices,
311 : struct TALER_Amount);
312 0 : memcpy (ppph->unit_prices,
313 : prices,
314 : num_prices * sizeof (struct TALER_Amount));
315 0 : return ppph;
316 : }
317 :
318 :
319 : enum GNUNET_GenericReturnValue
320 0 : TALER_MERCHANT_post_private_products_set_options_ (
321 : struct TALER_MERCHANT_PostPrivateProductsHandle *ppph,
322 : unsigned int num_options,
323 : const struct TALER_MERCHANT_PostPrivateProductsOptionValue *options)
324 : {
325 0 : for (unsigned int i = 0; i < num_options; i++)
326 : {
327 0 : switch (options[i].option)
328 : {
329 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_END:
330 0 : return GNUNET_OK;
331 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_DESCRIPTION_I18N:
332 0 : json_decref (ppph->description_i18n);
333 0 : ppph->description_i18n = json_incref ((json_t *) options[i].details.
334 : description_i18n);
335 0 : continue;
336 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_TAXES:
337 0 : json_decref (ppph->taxes);
338 0 : ppph->taxes = json_incref ((json_t *) options[i].details.taxes);
339 0 : continue;
340 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_ADDRESS:
341 0 : json_decref (ppph->address);
342 0 : ppph->address = json_incref ((json_t *) options[i].details.address);
343 0 : continue;
344 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_NEXT_RESTOCK:
345 0 : ppph->next_restock = options[i].details.next_restock;
346 0 : ppph->have_next_restock = true;
347 0 : continue;
348 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_MINIMUM_AGE:
349 0 : ppph->minimum_age = options[i].details.minimum_age;
350 0 : ppph->have_minimum_age = true;
351 0 : continue;
352 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_CATEGORIES:
353 0 : ppph->num_cats = options[i].details.categories.num;
354 0 : GNUNET_free (ppph->cats);
355 0 : ppph->cats = GNUNET_new_array (ppph->num_cats,
356 : uint64_t);
357 0 : memcpy (ppph->cats,
358 0 : options[i].details.categories.cats,
359 0 : ppph->num_cats * sizeof (uint64_t));
360 0 : continue;
361 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_TOTAL_STOCK_FRAC:
362 0 : ppph->total_stock_frac = options[i].details.total_stock.frac;
363 0 : continue;
364 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_TOTAL_STOCK_VAL:
365 0 : ppph->total_stock = options[i].details.total_stock.val;
366 0 : continue;
367 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_TOTAL_STOCK:
368 0 : ppph->total_stock_frac = options[i].details.total_stock.frac;
369 0 : ppph->total_stock = options[i].details.total_stock.val;
370 0 : continue;
371 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_UNIT_ALLOW_FRACTION:
372 0 : ppph->unit_allow_fraction = options[i].details.unit_allow_fraction;
373 0 : ppph->have_unit_allow_fraction = true;
374 0 : continue;
375 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_UNIT_PRECISION_LEVEL:
376 0 : ppph->unit_precision_level = options[i].details.unit_precision_level;
377 0 : ppph->have_unit_precision_level = true;
378 0 : continue;
379 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_PRODUCT_NAME:
380 0 : GNUNET_free (ppph->product_name);
381 0 : ppph->product_name = GNUNET_strdup (options[i].details.product_name);
382 0 : continue;
383 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_IMAGE:
384 0 : GNUNET_free (ppph->image);
385 0 : ppph->image = GNUNET_strdup (options[i].details.image);
386 0 : continue;
387 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_PRODUCT_GROUP_ID:
388 0 : ppph->product_group_id = options[i].details.product_group_id;
389 0 : ppph->have_product_group_id = true;
390 0 : continue;
391 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_MONEY_POT_ID:
392 0 : ppph->money_pot_id = options[i].details.money_pot_id;
393 0 : ppph->have_money_pot_id = true;
394 0 : continue;
395 0 : case TALER_MERCHANT_POST_PRIVATE_PRODUCTS_OPTION_PRICE_IS_NET:
396 0 : ppph->price_is_net = options[i].details.price_is_net;
397 0 : ppph->have_price_is_net = true;
398 0 : continue;
399 : }
400 0 : GNUNET_break (0);
401 0 : return GNUNET_SYSERR;
402 : }
403 0 : return GNUNET_OK;
404 : }
405 :
406 :
407 : enum TALER_ErrorCode
408 0 : TALER_MERCHANT_post_private_products_start (
409 : struct TALER_MERCHANT_PostPrivateProductsHandle *ppph,
410 : TALER_MERCHANT_PostPrivateProductsCallback cb,
411 : TALER_MERCHANT_POST_PRIVATE_PRODUCTS_RESULT_CLOSURE *cb_cls)
412 : {
413 : json_t *req_obj;
414 : json_t *categories;
415 : CURL *eh;
416 : char unit_total_stock_buf[64];
417 :
418 0 : ppph->cb = cb;
419 0 : ppph->cb_cls = cb_cls;
420 0 : ppph->url = TALER_url_join (ppph->base_url,
421 : "private/products",
422 : NULL);
423 0 : if (NULL == ppph->url)
424 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
425 :
426 0 : TALER_MERCHANT_format_stock_string (ppph->total_stock,
427 : ppph->total_stock_frac,
428 : unit_total_stock_buf,
429 : sizeof (unit_total_stock_buf));
430 :
431 0 : if (0 == ppph->num_cats)
432 : {
433 0 : categories = NULL;
434 : }
435 : else
436 : {
437 0 : categories = json_array ();
438 0 : GNUNET_assert (NULL != categories);
439 0 : for (unsigned int i = 0; i < ppph->num_cats; i++)
440 0 : GNUNET_assert (0 ==
441 : json_array_append_new (categories,
442 : json_integer (ppph->cats[i])));
443 : }
444 :
445 0 : req_obj = GNUNET_JSON_PACK (
446 : GNUNET_JSON_pack_string ("product_id",
447 : ppph->product_id),
448 : GNUNET_JSON_pack_string ("product_name",
449 : (NULL != ppph->product_name)
450 : ? ppph->product_name
451 : : ppph->description),
452 : GNUNET_JSON_pack_string ("description",
453 : ppph->description),
454 : GNUNET_JSON_pack_allow_null (
455 : GNUNET_JSON_pack_object_incref ("description_i18n",
456 : ppph->description_i18n)),
457 : GNUNET_JSON_pack_allow_null (
458 : GNUNET_JSON_pack_array_steal ("categories",
459 : categories)),
460 : GNUNET_JSON_pack_string ("unit",
461 : ppph->unit),
462 : TALER_JSON_pack_amount_array ("unit_price",
463 : ppph->unit_prices_len,
464 : ppph->unit_prices),
465 : GNUNET_JSON_pack_allow_null (
466 : GNUNET_JSON_pack_string ("image",
467 : ppph->image)),
468 : GNUNET_JSON_pack_allow_null (
469 : GNUNET_JSON_pack_array_incref ("taxes",
470 : ppph->taxes)),
471 : GNUNET_JSON_pack_string ("unit_total_stock",
472 : unit_total_stock_buf),
473 : GNUNET_JSON_pack_allow_null (
474 : GNUNET_JSON_pack_object_incref ("address",
475 : ppph->address)),
476 : GNUNET_JSON_pack_allow_null (
477 : GNUNET_JSON_pack_timestamp ("next_restock",
478 : ppph->have_next_restock
479 : ? ppph->next_restock
480 : : GNUNET_TIME_UNIT_ZERO_TS)));
481 0 : if (ppph->have_minimum_age)
482 : {
483 0 : GNUNET_assert (0 ==
484 : json_object_set_new (req_obj,
485 : "minimum_age",
486 : json_integer (
487 : ppph->minimum_age)));
488 : }
489 0 : if (ppph->have_product_group_id)
490 : {
491 0 : GNUNET_assert (0 ==
492 : json_object_set_new (req_obj,
493 : "product_group_id",
494 : json_integer (
495 : ppph->product_group_id)));
496 : }
497 0 : if (ppph->have_money_pot_id)
498 : {
499 0 : GNUNET_assert (0 ==
500 : json_object_set_new (req_obj,
501 : "money_pot_id",
502 : json_integer (
503 : ppph->money_pot_id)));
504 : }
505 0 : if (ppph->have_price_is_net)
506 : {
507 0 : GNUNET_assert (0 ==
508 : json_object_set_new (req_obj,
509 : "price_is_net",
510 : json_boolean (
511 : ppph->price_is_net)));
512 : }
513 0 : if (ppph->have_unit_allow_fraction &&
514 0 : ppph->unit_allow_fraction)
515 : {
516 0 : GNUNET_assert (0 ==
517 : json_object_set_new (req_obj,
518 : "unit_allow_fraction",
519 : json_true ()));
520 0 : if (ppph->have_unit_precision_level)
521 : {
522 0 : GNUNET_assert (0 ==
523 : json_object_set_new (req_obj,
524 : "unit_precision_level",
525 : json_integer (
526 : ppph->unit_precision_level)));
527 : }
528 : }
529 0 : eh = TALER_MERCHANT_curl_easy_get_ (ppph->url);
530 0 : if ( (NULL == eh) ||
531 : (GNUNET_OK !=
532 0 : TALER_curl_easy_post (&ppph->post_ctx,
533 : eh,
534 : req_obj)) )
535 : {
536 0 : GNUNET_break (0);
537 0 : json_decref (req_obj);
538 0 : if (NULL != eh)
539 0 : curl_easy_cleanup (eh);
540 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
541 : }
542 0 : json_decref (req_obj);
543 0 : ppph->job = GNUNET_CURL_job_add2 (ppph->ctx,
544 : eh,
545 0 : ppph->post_ctx.headers,
546 : &handle_post_products_finished,
547 : ppph);
548 0 : if (NULL == ppph->job)
549 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
550 0 : return TALER_EC_NONE;
551 : }
552 :
553 :
554 : void
555 0 : TALER_MERCHANT_post_private_products_cancel (
556 : struct TALER_MERCHANT_PostPrivateProductsHandle *ppph)
557 : {
558 0 : if (NULL != ppph->job)
559 : {
560 0 : GNUNET_CURL_job_cancel (ppph->job);
561 0 : ppph->job = NULL;
562 : }
563 0 : TALER_curl_easy_post_finished (&ppph->post_ctx);
564 0 : json_decref (ppph->description_i18n);
565 0 : json_decref (ppph->taxes);
566 0 : json_decref (ppph->address);
567 0 : GNUNET_free (ppph->cats);
568 0 : GNUNET_free (ppph->product_id);
569 0 : GNUNET_free (ppph->description);
570 0 : GNUNET_free (ppph->unit);
571 0 : GNUNET_free (ppph->image);
572 0 : GNUNET_free (ppph->product_name);
573 0 : GNUNET_free (ppph->url);
574 0 : GNUNET_free (ppph->base_url);
575 0 : GNUNET_free (ppph);
576 0 : }
577 :
578 :
579 : /* end of merchant_api_post-private-products-new.c */
|