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 src/lib/merchant_api_patch-private-products-PRODUCT_ID.c
21 : * @brief Implementation of the PATCH /private/products/$PRODUCT_ID request
22 : * @author Christian Grothoff
23 : */
24 : #include "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/patch-private-products-PRODUCT_ID.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 PATCH /private/products/$PRODUCT_ID operation.
39 : */
40 : struct TALER_MERCHANT_PatchPrivateProductHandle
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_PatchPrivateProductCallback cb;
61 :
62 : /**
63 : * Closure for @a cb.
64 : */
65 : TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_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 : * Identifier of the product to update.
79 : */
80 : char *product_id;
81 :
82 : /**
83 : * New human-readable description.
84 : */
85 : char *description;
86 :
87 : /**
88 : * Internationalized descriptions (JSON).
89 : */
90 : json_t *description_i18n;
91 :
92 : /**
93 : * Unit of measurement.
94 : */
95 : char *unit;
96 :
97 : /**
98 : * New base64-encoded image.
99 : */
100 : char *image;
101 :
102 : /**
103 : * Explicit product name (if different from description).
104 : */
105 : char *product_name;
106 :
107 : /**
108 : * Optional category IDs.
109 : */
110 : uint64_t *cats;
111 :
112 : /**
113 : * Product group ID.
114 : */
115 : uint64_t 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 : * Whether product_group_id has been set.
139 : */
140 : bool have_product_group_id;
141 :
142 : /**
143 : * Number of category IDs.
144 : */
145 : unsigned int num_cats;
146 :
147 : /**
148 : * New tax information (JSON array).
149 : */
150 : json_t *taxes;
151 :
152 : /**
153 : * New total stock (-1 for unlimited).
154 : */
155 : int64_t total_stock;
156 :
157 : /**
158 : * Total units lost/expired.
159 : */
160 : uint64_t total_lost;
161 :
162 : /**
163 : * Storage location (JSON).
164 : */
165 : json_t *address;
166 :
167 : /**
168 : * Expected restock time.
169 : */
170 : struct GNUNET_TIME_Timestamp next_restock;
171 :
172 : /**
173 : * Array of unit prices.
174 : */
175 : struct TALER_Amount *unit_prices;
176 :
177 : /**
178 : * Optional minimum age requirement.
179 : */
180 : uint32_t minimum_age;
181 :
182 : /**
183 : * Whether minimum_age has been set.
184 : */
185 : bool have_minimum_age;
186 :
187 : /**
188 : * Number of prices in @e unit_prices.
189 : */
190 : size_t unit_price_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 @e next_restock was explicitly set.
204 : */
205 : bool have_next_restock;
206 :
207 : /**
208 : * Whether @e unit_allow_fraction was explicitly set.
209 : */
210 : bool have_unit_allow_fraction;
211 :
212 : /**
213 : * Precision level for fractions.
214 : */
215 : uint32_t unit_precision_level;
216 :
217 : /**
218 : * Whether @e unit_precision_level was explicitly set.
219 : */
220 : bool have_unit_precision_level;
221 : };
222 :
223 :
224 : /**
225 : * Function called when we're done processing the
226 : * HTTP PATCH /private/products/$PRODUCT_ID request.
227 : *
228 : * @param cls the `struct TALER_MERCHANT_PatchPrivateProductHandle`
229 : * @param response_code HTTP response code, 0 on error
230 : * @param response response body, NULL if not in JSON
231 : */
232 : static void
233 24 : handle_patch_product_finished (void *cls,
234 : long response_code,
235 : const void *response)
236 : {
237 24 : struct TALER_MERCHANT_PatchPrivateProductHandle *pph = cls;
238 24 : const json_t *json = response;
239 24 : struct TALER_MERCHANT_PatchPrivateProductResponse ppr = {
240 24 : .hr.http_status = (unsigned int) response_code,
241 : .hr.reply = json
242 : };
243 :
244 24 : pph->job = NULL;
245 24 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
246 : "PATCH /private/products/$PRODUCT_ID completed with response code %u\n",
247 : (unsigned int) response_code);
248 24 : switch (response_code)
249 : {
250 0 : case 0:
251 0 : ppr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
252 0 : break;
253 16 : case MHD_HTTP_NO_CONTENT:
254 16 : break;
255 4 : case MHD_HTTP_BAD_REQUEST:
256 4 : ppr.hr.ec = TALER_JSON_get_error_code (json);
257 4 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
258 4 : GNUNET_break_op (0);
259 : /* This should never happen, either us
260 : * or the merchant is buggy (or API version conflict);
261 : * just pass JSON reply to the application */
262 4 : break;
263 0 : case MHD_HTTP_UNAUTHORIZED:
264 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
265 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
266 : /* Nothing really to verify, merchant says we need to authenticate. */
267 0 : break;
268 0 : case MHD_HTTP_FORBIDDEN:
269 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
270 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
271 0 : break;
272 4 : case MHD_HTTP_NOT_FOUND:
273 4 : ppr.hr.ec = TALER_JSON_get_error_code (json);
274 4 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
275 4 : break;
276 0 : case MHD_HTTP_CONFLICT:
277 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
278 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
279 0 : break;
280 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
281 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
282 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
283 : /* Server had an internal issue; we should retry,
284 : but this API leaves this to the application */
285 0 : break;
286 0 : default:
287 0 : TALER_MERCHANT_parse_error_details_ (json,
288 : response_code,
289 : &ppr.hr);
290 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
291 : "Unexpected response code %u/%d\n",
292 : (unsigned int) response_code,
293 : (int) ppr.hr.ec);
294 0 : GNUNET_break_op (0);
295 0 : break;
296 : }
297 24 : pph->cb (pph->cb_cls,
298 : &ppr);
299 24 : TALER_MERCHANT_patch_private_product_cancel (pph);
300 24 : }
301 :
302 :
303 : struct TALER_MERCHANT_PatchPrivateProductHandle *
304 24 : TALER_MERCHANT_patch_private_product_create (
305 : struct GNUNET_CURL_Context *ctx,
306 : const char *url,
307 : const char *product_id,
308 : const char *description,
309 : const char *unit,
310 : unsigned int num_prices,
311 : const struct TALER_Amount prices[static num_prices])
312 24 : {
313 : struct TALER_MERCHANT_PatchPrivateProductHandle *pph;
314 :
315 24 : pph = GNUNET_new (struct TALER_MERCHANT_PatchPrivateProductHandle);
316 24 : pph->ctx = ctx;
317 24 : pph->base_url = GNUNET_strdup (url);
318 24 : pph->product_id = GNUNET_strdup (product_id);
319 24 : pph->description = GNUNET_strdup (description);
320 24 : pph->unit = GNUNET_strdup (unit);
321 24 : pph->unit_price_len = num_prices;
322 24 : pph->unit_prices = GNUNET_new_array (num_prices,
323 : struct TALER_Amount);
324 24 : memcpy (pph->unit_prices,
325 : prices,
326 : num_prices * sizeof (struct TALER_Amount));
327 24 : return pph;
328 : }
329 :
330 :
331 : enum GNUNET_GenericReturnValue
332 32 : TALER_MERCHANT_patch_private_product_set_options_ (
333 : struct TALER_MERCHANT_PatchPrivateProductHandle *pph,
334 : unsigned int num_options,
335 : const struct TALER_MERCHANT_PatchPrivateProductOptionValue *options)
336 : {
337 248 : for (unsigned int i = 0; i < num_options; i++)
338 : {
339 248 : switch (options[i].option)
340 : {
341 32 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_END:
342 32 : return GNUNET_OK;
343 8 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_TOTAL_STOCK_FRAC:
344 8 : pph->total_stock_frac = options[i].details.total_stock.frac;
345 8 : continue;
346 24 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_TOTAL_STOCK_VAL:
347 24 : pph->total_stock = options[i].details.total_stock.val;
348 24 : continue;
349 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_TOTAL_STOCK:
350 0 : pph->total_stock = options[i].details.total_stock.val;
351 0 : pph->total_stock_frac = options[i].details.total_stock.frac;
352 0 : continue;
353 24 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_TOTAL_LOST:
354 24 : pph->total_lost = options[i].details.total_lost;
355 24 : continue;
356 8 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_UNIT_ALLOW_FRACTION:
357 8 : pph->unit_allow_fraction = options[i].details.unit_allow_fraction;
358 8 : pph->have_unit_allow_fraction = true;
359 8 : continue;
360 8 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_UNIT_PRECISION_LEVEL:
361 8 : pph->unit_precision_level = options[i].details.unit_precision_level;
362 8 : pph->have_unit_precision_level = true;
363 8 : continue;
364 24 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_DESCRIPTION_I18N:
365 24 : json_decref (pph->description_i18n);
366 48 : pph->description_i18n = json_incref (
367 24 : (json_t *) options[i].details.description_i18n);
368 24 : continue;
369 24 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_TAXES:
370 24 : json_decref (pph->taxes);
371 24 : pph->taxes = json_incref ((json_t *) options[i].details.taxes);
372 24 : continue;
373 24 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_ADDRESS:
374 24 : json_decref (pph->address);
375 24 : pph->address = json_incref ((json_t *) options[i].details.address);
376 24 : continue;
377 24 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_NEXT_RESTOCK:
378 24 : pph->next_restock = options[i].details.next_restock;
379 24 : pph->have_next_restock = true;
380 24 : continue;
381 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_MINIMUM_AGE:
382 0 : pph->minimum_age = options[i].details.minimum_age;
383 0 : pph->have_minimum_age = true;
384 0 : continue;
385 24 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_CATEGORIES:
386 24 : pph->num_cats = options[i].details.categories.num;
387 24 : GNUNET_free (pph->cats);
388 24 : pph->cats = GNUNET_new_array (pph->num_cats,
389 : uint64_t);
390 24 : memcpy (pph->cats,
391 24 : options[i].details.categories.cats,
392 24 : pph->num_cats * sizeof (uint64_t));
393 24 : continue;
394 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_PRODUCT_NAME:
395 0 : GNUNET_free (pph->product_name);
396 0 : pph->product_name = GNUNET_strdup (options[i].details.product_name);
397 0 : continue;
398 24 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_IMAGE:
399 24 : GNUNET_free (pph->image);
400 24 : pph->image = GNUNET_strdup (options[i].details.image);
401 24 : continue;
402 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_PRODUCT_GROUP_ID:
403 0 : pph->product_group_id = options[i].details.product_group_id;
404 0 : pph->have_product_group_id = true;
405 0 : continue;
406 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_MONEY_POT_ID:
407 0 : pph->money_pot_id = options[i].details.money_pot_id;
408 0 : pph->have_money_pot_id = true;
409 0 : continue;
410 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_PRICE_IS_NET:
411 0 : pph->price_is_net = options[i].details.price_is_net;
412 0 : pph->have_price_is_net = true;
413 0 : continue;
414 : }
415 0 : GNUNET_break (0);
416 0 : return GNUNET_SYSERR;
417 : }
418 0 : return GNUNET_OK;
419 : }
420 :
421 :
422 : enum TALER_ErrorCode
423 24 : TALER_MERCHANT_patch_private_product_start (
424 : struct TALER_MERCHANT_PatchPrivateProductHandle *pph,
425 : TALER_MERCHANT_PatchPrivateProductCallback cb,
426 : TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_RESULT_CLOSURE *cb_cls)
427 : {
428 : json_t *req_obj;
429 : json_t *categories;
430 : CURL *eh;
431 : char unit_total_stock_buf[64];
432 :
433 24 : pph->cb = cb;
434 24 : pph->cb_cls = cb_cls;
435 : {
436 : char *path;
437 :
438 24 : GNUNET_asprintf (&path,
439 : "private/products/%s",
440 : pph->product_id);
441 24 : pph->url = TALER_url_join (pph->base_url,
442 : path,
443 : NULL);
444 24 : GNUNET_free (path);
445 : }
446 24 : if (NULL == pph->url)
447 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
448 24 : TALER_MERCHANT_format_stock_string (pph->total_stock,
449 : pph->total_stock_frac,
450 : unit_total_stock_buf,
451 : sizeof (unit_total_stock_buf));
452 :
453 24 : if (0 == pph->num_cats)
454 : {
455 16 : categories = NULL;
456 : }
457 : else
458 : {
459 8 : categories = json_array ();
460 8 : GNUNET_assert (NULL != categories);
461 18 : for (unsigned int i = 0; i < pph->num_cats; i++)
462 10 : GNUNET_assert (0 ==
463 : json_array_append_new (categories,
464 : json_integer (pph->cats[i])));
465 : }
466 :
467 24 : req_obj = GNUNET_JSON_PACK (
468 : GNUNET_JSON_pack_string ("product_name",
469 : (NULL == pph->product_name)
470 : ? pph->description
471 : : pph->product_name),
472 : GNUNET_JSON_pack_string ("description",
473 : pph->description),
474 : GNUNET_JSON_pack_allow_null (
475 : GNUNET_JSON_pack_object_incref ("description_i18n",
476 : pph->description_i18n)),
477 : GNUNET_JSON_pack_allow_null (
478 : GNUNET_JSON_pack_array_steal ("categories",
479 : categories)),
480 : GNUNET_JSON_pack_string ("unit",
481 : pph->unit),
482 : TALER_JSON_pack_amount_array ("unit_price",
483 : pph->unit_price_len,
484 : pph->unit_prices),
485 : GNUNET_JSON_pack_allow_null (
486 : GNUNET_JSON_pack_string ("image",
487 : pph->image)),
488 : GNUNET_JSON_pack_allow_null (
489 : GNUNET_JSON_pack_array_incref ("taxes",
490 : pph->taxes)),
491 : GNUNET_JSON_pack_string ("unit_total_stock",
492 : unit_total_stock_buf),
493 : GNUNET_JSON_pack_uint64 ("total_lost",
494 : pph->total_lost),
495 : GNUNET_JSON_pack_allow_null (
496 : GNUNET_JSON_pack_object_incref ("address",
497 : pph->address)),
498 : GNUNET_JSON_pack_conditional (
499 : pph->have_minimum_age,
500 : GNUNET_JSON_pack_uint64 ("minimum_age",
501 : pph->minimum_age)),
502 : GNUNET_JSON_pack_conditional (
503 : pph->have_product_group_id,
504 : GNUNET_JSON_pack_uint64 ("product_group_id",
505 : pph->product_group_id)),
506 : GNUNET_JSON_pack_conditional (
507 : pph->have_money_pot_id,
508 : GNUNET_JSON_pack_uint64 ("money_pot_id",
509 : pph->money_pot_id)),
510 : GNUNET_JSON_pack_conditional (
511 : pph->have_unit_allow_fraction &&
512 : pph->unit_allow_fraction,
513 : GNUNET_JSON_pack_bool ("unit_allow_fraction",
514 : pph->unit_allow_fraction)),
515 : GNUNET_JSON_pack_conditional (
516 : pph->have_unit_allow_fraction &&
517 : pph->unit_allow_fraction &&
518 : pph->have_unit_precision_level,
519 : GNUNET_JSON_pack_uint64 ("unit_precision_level",
520 : pph->unit_precision_level)),
521 : GNUNET_JSON_pack_conditional (
522 : pph->have_price_is_net,
523 : GNUNET_JSON_pack_bool ("price_is_net",
524 : pph->price_is_net)),
525 : GNUNET_JSON_pack_allow_null (
526 : GNUNET_JSON_pack_timestamp ("next_restock",
527 : pph->next_restock)));
528 24 : eh = TALER_MERCHANT_curl_easy_get_ (pph->url);
529 48 : if ( (NULL == eh) ||
530 : (GNUNET_OK !=
531 24 : TALER_curl_easy_post (&pph->post_ctx,
532 : eh,
533 : req_obj)) )
534 : {
535 0 : GNUNET_break (0);
536 0 : json_decref (req_obj);
537 0 : if (NULL != eh)
538 0 : curl_easy_cleanup (eh);
539 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
540 : }
541 24 : json_decref (req_obj);
542 24 : GNUNET_assert (CURLE_OK ==
543 : curl_easy_setopt (eh,
544 : CURLOPT_CUSTOMREQUEST,
545 : MHD_HTTP_METHOD_PATCH));
546 48 : pph->job = GNUNET_CURL_job_add2 (pph->ctx,
547 : eh,
548 24 : pph->post_ctx.headers,
549 : &handle_patch_product_finished,
550 : pph);
551 24 : if (NULL == pph->job)
552 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
553 24 : return TALER_EC_NONE;
554 : }
555 :
556 :
557 : void
558 24 : TALER_MERCHANT_patch_private_product_cancel (
559 : struct TALER_MERCHANT_PatchPrivateProductHandle *pph)
560 : {
561 24 : if (NULL != pph->job)
562 : {
563 0 : GNUNET_CURL_job_cancel (pph->job);
564 0 : pph->job = NULL;
565 : }
566 24 : TALER_curl_easy_post_finished (&pph->post_ctx);
567 24 : json_decref (pph->description_i18n);
568 24 : json_decref (pph->taxes);
569 24 : json_decref (pph->address);
570 24 : GNUNET_free (pph->cats);
571 24 : GNUNET_free (pph->unit_prices);
572 24 : GNUNET_free (pph->url);
573 24 : GNUNET_free (pph->base_url);
574 24 : GNUNET_free (pph->product_id);
575 24 : GNUNET_free (pph->description);
576 24 : GNUNET_free (pph->unit);
577 24 : GNUNET_free (pph->image);
578 24 : GNUNET_free (pph->product_name);
579 24 : GNUNET_free (pph);
580 24 : }
581 :
582 :
583 : /* end of merchant_api_patch-private-products-PRODUCT_ID.c */
|