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_patch-private-products-PRODUCT_ID.c
21 : * @brief Implementation of the PATCH /private/products/$PRODUCT_ID 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/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 0 : handle_patch_product_finished (void *cls,
234 : long response_code,
235 : const void *response)
236 : {
237 0 : struct TALER_MERCHANT_PatchPrivateProductHandle *pph = cls;
238 0 : const json_t *json = response;
239 0 : struct TALER_MERCHANT_PatchPrivateProductResponse ppr = {
240 0 : .hr.http_status = (unsigned int) response_code,
241 : .hr.reply = json
242 : };
243 :
244 0 : pph->job = NULL;
245 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
246 : "PATCH /private/products/$PRODUCT_ID completed with response code %u\n",
247 : (unsigned int) response_code);
248 0 : switch (response_code)
249 : {
250 0 : case 0:
251 0 : ppr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
252 0 : break;
253 0 : case MHD_HTTP_NO_CONTENT:
254 0 : break;
255 0 : case MHD_HTTP_BAD_REQUEST:
256 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
257 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
258 0 : 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 0 : 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 0 : case MHD_HTTP_NOT_FOUND:
273 0 : ppr.hr.ec = TALER_JSON_get_error_code (json);
274 0 : ppr.hr.hint = TALER_JSON_get_error_hint (json);
275 0 : 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 0 : pph->cb (pph->cb_cls,
298 : &ppr);
299 0 : TALER_MERCHANT_patch_private_product_cancel (pph);
300 0 : }
301 :
302 :
303 : struct TALER_MERCHANT_PatchPrivateProductHandle *
304 0 : 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 0 : {
313 : struct TALER_MERCHANT_PatchPrivateProductHandle *pph;
314 :
315 0 : pph = GNUNET_new (struct TALER_MERCHANT_PatchPrivateProductHandle);
316 0 : pph->ctx = ctx;
317 0 : pph->base_url = GNUNET_strdup (url);
318 0 : pph->product_id = GNUNET_strdup (product_id);
319 0 : pph->description = GNUNET_strdup (description);
320 0 : pph->unit = GNUNET_strdup (unit);
321 0 : pph->unit_price_len = num_prices;
322 0 : pph->unit_prices = GNUNET_new_array (num_prices,
323 : struct TALER_Amount);
324 0 : memcpy (pph->unit_prices,
325 : prices,
326 : num_prices * sizeof (struct TALER_Amount));
327 0 : return pph;
328 : }
329 :
330 :
331 : enum GNUNET_GenericReturnValue
332 0 : 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 0 : for (unsigned int i = 0; i < num_options; i++)
338 : {
339 0 : switch (options[i].option)
340 : {
341 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_END:
342 0 : return GNUNET_OK;
343 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_TOTAL_STOCK_FRAC:
344 0 : pph->total_stock_frac = options[i].details.total_stock.frac;
345 0 : continue;
346 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_TOTAL_STOCK_VAL:
347 0 : pph->total_stock = options[i].details.total_stock.val;
348 0 : 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 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_TOTAL_LOST:
354 0 : pph->total_lost = options[i].details.total_lost;
355 0 : continue;
356 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_UNIT_ALLOW_FRACTION:
357 0 : pph->unit_allow_fraction = options[i].details.unit_allow_fraction;
358 0 : pph->have_unit_allow_fraction = true;
359 0 : continue;
360 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_UNIT_PRECISION_LEVEL:
361 0 : pph->unit_precision_level = options[i].details.unit_precision_level;
362 0 : pph->have_unit_precision_level = true;
363 0 : continue;
364 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_DESCRIPTION_I18N:
365 0 : json_decref (pph->description_i18n);
366 0 : pph->description_i18n = json_incref (
367 0 : (json_t *) options[i].details.description_i18n);
368 0 : continue;
369 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_TAXES:
370 0 : json_decref (pph->taxes);
371 0 : pph->taxes = json_incref ((json_t *) options[i].details.taxes);
372 0 : continue;
373 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_ADDRESS:
374 0 : json_decref (pph->address);
375 0 : pph->address = json_incref ((json_t *) options[i].details.address);
376 0 : continue;
377 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_NEXT_RESTOCK:
378 0 : pph->next_restock = options[i].details.next_restock;
379 0 : pph->have_next_restock = true;
380 0 : 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 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_CATEGORIES:
386 0 : pph->num_cats = options[i].details.categories.num;
387 0 : GNUNET_free (pph->cats);
388 0 : pph->cats = GNUNET_new_array (pph->num_cats,
389 : uint64_t);
390 0 : memcpy (pph->cats,
391 0 : options[i].details.categories.cats,
392 0 : pph->num_cats * sizeof (uint64_t));
393 0 : 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 0 : case TALER_MERCHANT_PATCH_PRIVATE_PRODUCT_OPTION_IMAGE:
399 0 : GNUNET_free (pph->image);
400 0 : pph->image = GNUNET_strdup (options[i].details.image);
401 0 : 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 0 : 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 : CURL *eh;
430 : char unit_total_stock_buf[64];
431 :
432 0 : pph->cb = cb;
433 0 : pph->cb_cls = cb_cls;
434 : {
435 : char *path;
436 :
437 0 : GNUNET_asprintf (&path,
438 : "private/products/%s",
439 : pph->product_id);
440 0 : pph->url = TALER_url_join (pph->base_url,
441 : path,
442 : NULL);
443 0 : GNUNET_free (path);
444 : }
445 0 : if (NULL == pph->url)
446 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
447 0 : TALER_MERCHANT_format_stock_string (pph->total_stock,
448 : pph->total_stock_frac,
449 : unit_total_stock_buf,
450 : sizeof (unit_total_stock_buf));
451 :
452 0 : req_obj = GNUNET_JSON_PACK (
453 : GNUNET_JSON_pack_string ("product_name",
454 : pph->description),
455 : GNUNET_JSON_pack_string ("description",
456 : pph->description),
457 : GNUNET_JSON_pack_object_incref ("description_i18n",
458 : pph->description_i18n),
459 : GNUNET_JSON_pack_string ("unit",
460 : pph->unit),
461 : TALER_JSON_pack_amount_array ("unit_price",
462 : pph->unit_price_len,
463 : pph->unit_prices),
464 : GNUNET_JSON_pack_string ("image",
465 : pph->image),
466 : GNUNET_JSON_pack_array_incref ("taxes",
467 : pph->taxes),
468 : GNUNET_JSON_pack_string ("unit_total_stock",
469 : unit_total_stock_buf),
470 : GNUNET_JSON_pack_uint64 ("total_lost",
471 : pph->total_lost),
472 : GNUNET_JSON_pack_object_incref ("address",
473 : pph->address),
474 : GNUNET_JSON_pack_timestamp ("next_restock",
475 : pph->next_restock));
476 0 : if (pph->have_unit_allow_fraction &&
477 0 : pph->unit_allow_fraction)
478 : {
479 0 : GNUNET_assert (0 ==
480 : json_object_set_new (req_obj,
481 : "unit_allow_fraction",
482 : json_boolean (
483 : pph->unit_allow_fraction)));
484 0 : if (pph->have_unit_precision_level)
485 : {
486 0 : GNUNET_assert (0 ==
487 : json_object_set_new (req_obj,
488 : "unit_precision_level",
489 : json_integer (
490 : pph->unit_precision_level)));
491 : }
492 : }
493 0 : eh = TALER_MERCHANT_curl_easy_get_ (pph->url);
494 0 : if ( (NULL == eh) ||
495 : (GNUNET_OK !=
496 0 : TALER_curl_easy_post (&pph->post_ctx,
497 : eh,
498 : req_obj)) )
499 : {
500 0 : GNUNET_break (0);
501 0 : json_decref (req_obj);
502 0 : if (NULL != eh)
503 0 : curl_easy_cleanup (eh);
504 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
505 : }
506 0 : json_decref (req_obj);
507 0 : GNUNET_assert (CURLE_OK ==
508 : curl_easy_setopt (eh,
509 : CURLOPT_CUSTOMREQUEST,
510 : MHD_HTTP_METHOD_PATCH));
511 0 : pph->job = GNUNET_CURL_job_add2 (pph->ctx,
512 : eh,
513 0 : pph->post_ctx.headers,
514 : &handle_patch_product_finished,
515 : pph);
516 0 : if (NULL == pph->job)
517 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
518 0 : return TALER_EC_NONE;
519 : }
520 :
521 :
522 : void
523 0 : TALER_MERCHANT_patch_private_product_cancel (
524 : struct TALER_MERCHANT_PatchPrivateProductHandle *pph)
525 : {
526 0 : if (NULL != pph->job)
527 : {
528 0 : GNUNET_CURL_job_cancel (pph->job);
529 0 : pph->job = NULL;
530 : }
531 0 : TALER_curl_easy_post_finished (&pph->post_ctx);
532 0 : json_decref (pph->description_i18n);
533 0 : json_decref (pph->taxes);
534 0 : json_decref (pph->address);
535 0 : GNUNET_free (pph->cats);
536 0 : GNUNET_free (pph->unit_prices);
537 0 : GNUNET_free (pph->url);
538 0 : GNUNET_free (pph->base_url);
539 0 : GNUNET_free (pph->product_id);
540 0 : GNUNET_free (pph->description);
541 0 : GNUNET_free (pph->unit);
542 0 : GNUNET_free (pph->image);
543 0 : GNUNET_free (pph->product_name);
544 0 : GNUNET_free (pph);
545 0 : }
546 :
547 :
548 : /* end of merchant_api_patch-private-products-PRODUCT_ID.c */
|