Line data Source code
1 : /*
2 : This file is part of TALER
3 : (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 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 src/backend/taler-merchant-httpd_patch-private-products-PRODUCT_ID.c
22 : * @brief implementing PATCH /products/$ID request handling
23 : * @author Christian Grothoff
24 : */
25 : #include "platform.h"
26 : #include "taler-merchant-httpd_patch-private-products-PRODUCT_ID.h"
27 : #include "taler-merchant-httpd_helper.h"
28 : #include <taler/taler_json_lib.h>
29 : #include "merchant-database/update_product.h"
30 :
31 :
32 : /**
33 : * PATCH configuration of an existing instance, given its configuration.
34 : *
35 : * @param rh context of the handler
36 : * @param connection the MHD connection to handle
37 : * @param[in,out] hc context with further information about the request
38 : * @return MHD result code
39 : */
40 : enum MHD_Result
41 26 : TMH_private_patch_products_ID (
42 : const struct TMH_RequestHandler *rh,
43 : struct MHD_Connection *connection,
44 : struct TMH_HandlerContext *hc)
45 : {
46 26 : struct TMH_MerchantInstance *mi = hc->instance;
47 26 : const char *product_id = hc->infix;
48 26 : struct TALER_MERCHANTDB_ProductDetails pd = {0};
49 26 : const json_t *categories = NULL;
50 : int64_t total_stock;
51 26 : const char *unit_total_stock = NULL;
52 : bool unit_total_stock_missing;
53 : bool total_stock_missing;
54 : struct TALER_Amount price;
55 : bool price_missing;
56 : bool unit_price_missing;
57 : bool unit_allow_fraction;
58 : bool unit_allow_fraction_missing;
59 : uint32_t unit_precision_level;
60 : bool unit_precision_missing;
61 : enum GNUNET_DB_QueryStatus qs;
62 : struct GNUNET_JSON_Specification spec[] = {
63 : /* new in protocol v20, thus optional for backwards-compatibility */
64 26 : GNUNET_JSON_spec_mark_optional (
65 : GNUNET_JSON_spec_string ("product_name",
66 : (const char **) &pd.product_name),
67 : NULL),
68 26 : GNUNET_JSON_spec_string ("description",
69 : (const char **) &pd.description),
70 26 : GNUNET_JSON_spec_mark_optional (
71 : GNUNET_JSON_spec_json ("description_i18n",
72 : &pd.description_i18n),
73 : NULL),
74 26 : TALER_JSON_spec_slug ("unit",
75 : (const char **) &pd.unit),
76 : // FIXME: deprecated API
77 26 : GNUNET_JSON_spec_mark_optional (
78 : TALER_JSON_spec_amount_any ("price",
79 : &price),
80 : &price_missing),
81 26 : GNUNET_JSON_spec_mark_optional (
82 : TALER_JSON_spec_amount_any_array ("unit_price",
83 : &pd.price_array_length,
84 : &pd.price_array),
85 : &unit_price_missing),
86 26 : GNUNET_JSON_spec_mark_optional (
87 : GNUNET_JSON_spec_string ("image",
88 : (const char **) &pd.image),
89 : NULL),
90 26 : GNUNET_JSON_spec_mark_optional (
91 : GNUNET_JSON_spec_json ("taxes",
92 : &pd.taxes),
93 : NULL),
94 26 : GNUNET_JSON_spec_mark_optional (
95 : GNUNET_JSON_spec_array_const ("categories",
96 : &categories),
97 : NULL),
98 26 : GNUNET_JSON_spec_mark_optional (
99 : GNUNET_JSON_spec_string ("unit_total_stock",
100 : &unit_total_stock),
101 : &unit_total_stock_missing),
102 26 : GNUNET_JSON_spec_mark_optional (
103 : GNUNET_JSON_spec_int64 ("total_stock",
104 : &total_stock),
105 : &total_stock_missing),
106 26 : GNUNET_JSON_spec_mark_optional (
107 : GNUNET_JSON_spec_bool ("unit_allow_fraction",
108 : &unit_allow_fraction),
109 : &unit_allow_fraction_missing),
110 26 : GNUNET_JSON_spec_mark_optional (
111 : GNUNET_JSON_spec_uint32 ("unit_precision_level",
112 : &unit_precision_level),
113 : &unit_precision_missing),
114 26 : GNUNET_JSON_spec_mark_optional (
115 : GNUNET_JSON_spec_uint64 ("total_lost",
116 : &pd.total_lost),
117 : NULL),
118 26 : GNUNET_JSON_spec_mark_optional (
119 : GNUNET_JSON_spec_uint64 ("product_group_id",
120 : &pd.product_group_id),
121 : NULL),
122 26 : GNUNET_JSON_spec_mark_optional (
123 : GNUNET_JSON_spec_uint64 ("money_pot_id",
124 : &pd.money_pot_id),
125 : NULL),
126 26 : GNUNET_JSON_spec_mark_optional (
127 : GNUNET_JSON_spec_json ("address",
128 : &pd.address),
129 : NULL),
130 26 : GNUNET_JSON_spec_mark_optional (
131 : GNUNET_JSON_spec_timestamp ("next_restock",
132 : &pd.next_restock),
133 : NULL),
134 26 : GNUNET_JSON_spec_mark_optional (
135 : GNUNET_JSON_spec_uint32 ("minimum_age",
136 : &pd.minimum_age),
137 : NULL),
138 26 : GNUNET_JSON_spec_mark_optional (
139 : GNUNET_JSON_spec_bool ("price_is_net",
140 : &pd.price_is_net),
141 : NULL),
142 26 : GNUNET_JSON_spec_end ()
143 : };
144 : enum MHD_Result ret;
145 26 : size_t num_cats = 0;
146 26 : uint64_t *cats = NULL;
147 : ssize_t no_cat;
148 : bool no_product;
149 : bool lost_reduced;
150 : bool sold_reduced;
151 : bool stock_reduced;
152 : bool no_group;
153 : bool no_pot;
154 :
155 26 : GNUNET_assert (NULL != mi);
156 26 : GNUNET_assert (NULL != product_id);
157 : {
158 : enum GNUNET_GenericReturnValue res;
159 :
160 26 : res = TALER_MHD_parse_json_data (connection,
161 26 : hc->request_body,
162 : spec);
163 26 : if (GNUNET_OK != res)
164 : return (GNUNET_NO == res)
165 : ? MHD_YES
166 0 : : MHD_NO;
167 : /* For pre-v20 clients, we use the description given as the
168 : product name; remove once we make product_name mandatory. */
169 26 : if (NULL == pd.product_name)
170 0 : pd.product_name = pd.description;
171 : }
172 26 : if ( (! unit_price_missing) &&
173 26 : (0 == pd.price_array_length) )
174 : {
175 0 : GNUNET_break_op (0);
176 0 : ret = TALER_MHD_reply_with_error (connection,
177 : MHD_HTTP_BAD_REQUEST,
178 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
179 : "unit_price");
180 0 : goto cleanup;
181 : }
182 26 : if (! unit_price_missing)
183 : {
184 26 : if (! price_missing)
185 : {
186 0 : if (0 != TALER_amount_cmp (&price,
187 0 : &pd.price_array[0]))
188 : {
189 0 : ret = TALER_MHD_reply_with_error (connection,
190 : MHD_HTTP_BAD_REQUEST,
191 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
192 : "price,unit_price mismatch");
193 0 : goto cleanup;
194 : }
195 : }
196 26 : if (GNUNET_OK !=
197 26 : TMH_validate_unit_price_array (pd.price_array,
198 : pd.price_array_length))
199 : {
200 2 : ret = TALER_MHD_reply_with_error (connection,
201 : MHD_HTTP_BAD_REQUEST,
202 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
203 : "unit_price");
204 2 : goto cleanup;
205 : }
206 : }
207 : else
208 : {
209 0 : if (price_missing)
210 : {
211 0 : ret = TALER_MHD_reply_with_error (connection,
212 : MHD_HTTP_BAD_REQUEST,
213 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
214 : "price missing");
215 0 : goto cleanup;
216 : }
217 0 : pd.price_array = GNUNET_new_array (1,
218 : struct TALER_Amount);
219 0 : pd.price_array[0] = price;
220 0 : pd.price_array_length = 1;
221 : }
222 24 : if (! unit_precision_missing)
223 : {
224 4 : if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL)
225 : {
226 0 : ret = TALER_MHD_reply_with_error (connection,
227 : MHD_HTTP_BAD_REQUEST,
228 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
229 : "unit_precision_level");
230 0 : goto cleanup;
231 : }
232 : }
233 : {
234 : bool default_allow_fractional;
235 : uint32_t default_precision_level;
236 :
237 24 : if (GNUNET_OK !=
238 24 : TMH_unit_defaults_for_instance (mi,
239 24 : pd.unit,
240 : &default_allow_fractional,
241 : &default_precision_level))
242 : {
243 0 : GNUNET_break (0);
244 0 : ret = TALER_MHD_reply_with_error (connection,
245 : MHD_HTTP_INTERNAL_SERVER_ERROR,
246 : TALER_EC_GENERIC_DB_FETCH_FAILED,
247 : "unit defaults");
248 0 : goto cleanup;
249 : }
250 24 : if (unit_allow_fraction_missing)
251 20 : unit_allow_fraction = default_allow_fractional;
252 24 : if (unit_precision_missing)
253 20 : unit_precision_level = default_precision_level;
254 :
255 24 : if (! unit_allow_fraction)
256 20 : unit_precision_level = 0;
257 24 : pd.fractional_precision_level = unit_precision_level;
258 : }
259 : {
260 : const char *eparam;
261 24 : if (GNUNET_OK !=
262 24 : TALER_MERCHANT_vk_process_quantity_inputs (
263 : TALER_MERCHANT_VK_STOCK,
264 : unit_allow_fraction,
265 : total_stock_missing,
266 : total_stock,
267 : unit_total_stock_missing,
268 : unit_total_stock,
269 : &pd.total_stock,
270 : &pd.total_stock_frac,
271 : &eparam))
272 : {
273 0 : ret = TALER_MHD_reply_with_error (
274 : connection,
275 : MHD_HTTP_BAD_REQUEST,
276 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
277 : eparam);
278 0 : goto cleanup;
279 : }
280 24 : pd.allow_fractional_quantity = unit_allow_fraction;
281 : }
282 24 : if (NULL == pd.address)
283 2 : pd.address = json_object ();
284 :
285 24 : if (! TMH_location_object_valid (pd.address))
286 : {
287 0 : GNUNET_break_op (0);
288 0 : ret = TALER_MHD_reply_with_error (connection,
289 : MHD_HTTP_BAD_REQUEST,
290 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
291 : "address");
292 0 : goto cleanup;
293 : }
294 24 : num_cats = json_array_size (categories);
295 24 : cats = GNUNET_new_array (num_cats,
296 : uint64_t);
297 : {
298 : size_t idx;
299 : json_t *val;
300 :
301 32 : json_array_foreach (categories, idx, val)
302 : {
303 20 : if ( (! json_is_integer (val)) ||
304 10 : (0 >= json_integer_value (val)) )
305 : {
306 2 : GNUNET_break_op (0);
307 2 : ret = TALER_MHD_reply_with_error (connection,
308 : MHD_HTTP_BAD_REQUEST,
309 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
310 : "categories");
311 2 : goto cleanup;
312 : }
313 8 : cats[idx] = json_integer_value (val);
314 : }
315 : }
316 :
317 22 : if (NULL == pd.description_i18n)
318 2 : pd.description_i18n = json_object ();
319 :
320 22 : if (! TALER_JSON_check_i18n (pd.description_i18n))
321 : {
322 0 : GNUNET_break_op (0);
323 0 : ret = TALER_MHD_reply_with_error (connection,
324 : MHD_HTTP_BAD_REQUEST,
325 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
326 : "description_i18n");
327 0 : goto cleanup;
328 : }
329 :
330 22 : if (NULL == pd.taxes)
331 0 : pd.taxes = json_array ();
332 : /* check taxes is well-formed */
333 22 : if (! TALER_MERCHANT_taxes_array_valid (pd.taxes))
334 : {
335 0 : GNUNET_break_op (0);
336 0 : ret = TALER_MHD_reply_with_error (connection,
337 : MHD_HTTP_BAD_REQUEST,
338 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
339 : "taxes");
340 0 : goto cleanup;
341 : }
342 :
343 22 : if (NULL == pd.image)
344 0 : pd.image = (char *) "";
345 22 : if (! TALER_MERCHANT_image_data_url_valid (pd.image))
346 : {
347 0 : GNUNET_break_op (0);
348 0 : ret = TALER_MHD_reply_with_error (connection,
349 : MHD_HTTP_BAD_REQUEST,
350 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
351 : "image");
352 0 : goto cleanup;
353 : }
354 :
355 22 : if ( (pd.total_stock < pd.total_sold + pd.total_lost) ||
356 22 : (pd.total_sold + pd.total_lost < pd.total_sold) /* integer overflow */)
357 : {
358 0 : GNUNET_break_op (0);
359 0 : ret = TALER_MHD_reply_with_error (
360 : connection,
361 : MHD_HTTP_BAD_REQUEST,
362 : TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS,
363 : NULL);
364 0 : goto cleanup;
365 : }
366 :
367 22 : qs = TALER_MERCHANTDB_update_product (TMH_db,
368 22 : mi->settings.id,
369 : product_id,
370 : &pd,
371 : num_cats,
372 : cats,
373 : &no_cat,
374 : &no_product,
375 : &lost_reduced,
376 : &sold_reduced,
377 : &stock_reduced,
378 : &no_group,
379 : &no_pot);
380 22 : switch (qs)
381 : {
382 0 : case GNUNET_DB_STATUS_HARD_ERROR:
383 0 : GNUNET_break (0);
384 0 : ret = TALER_MHD_reply_with_error (connection,
385 : MHD_HTTP_INTERNAL_SERVER_ERROR,
386 : TALER_EC_GENERIC_DB_STORE_FAILED,
387 : NULL);
388 0 : goto cleanup;
389 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
390 0 : GNUNET_break (0);
391 0 : ret = TALER_MHD_reply_with_error (connection,
392 : MHD_HTTP_INTERNAL_SERVER_ERROR,
393 : TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
394 : "unexpected serialization problem");
395 0 : goto cleanup;
396 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
397 0 : GNUNET_break (0);
398 0 : ret = TALER_MHD_reply_with_error (connection,
399 : MHD_HTTP_INTERNAL_SERVER_ERROR,
400 : TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
401 : "unexpected problem in stored procedure");
402 0 : goto cleanup;
403 22 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
404 22 : break;
405 : }
406 :
407 22 : if (-1 != no_cat)
408 : {
409 : char cat_str[24];
410 :
411 : /* 'no_cat' is the 1-based index into 'cats' of the unknown
412 : category; report the category itself to the client. */
413 2 : GNUNET_assert (no_cat > 0);
414 2 : GNUNET_assert (no_cat <= (ssize_t) num_cats);
415 2 : GNUNET_snprintf (cat_str,
416 : sizeof (cat_str),
417 : "%llu",
418 2 : (unsigned long long) cats[no_cat - 1]);
419 2 : ret = TALER_MHD_reply_with_error (connection,
420 : MHD_HTTP_NOT_FOUND,
421 : TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN,
422 : cat_str);
423 2 : goto cleanup;
424 : }
425 20 : if (no_product)
426 : {
427 2 : ret = TALER_MHD_reply_with_error (connection,
428 : MHD_HTTP_NOT_FOUND,
429 : TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
430 : product_id);
431 2 : goto cleanup;
432 : }
433 18 : if (no_group)
434 : {
435 0 : ret = TALER_MHD_reply_with_error (
436 : connection,
437 : MHD_HTTP_NOT_FOUND,
438 : TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN,
439 : NULL);
440 0 : goto cleanup;
441 : }
442 18 : if (no_pot)
443 : {
444 0 : ret = TALER_MHD_reply_with_error (
445 : connection,
446 : MHD_HTTP_NOT_FOUND,
447 : TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN,
448 : NULL);
449 0 : goto cleanup;
450 : }
451 18 : if (lost_reduced)
452 : {
453 0 : ret = TALER_MHD_reply_with_error (
454 : connection,
455 : MHD_HTTP_CONFLICT,
456 : TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED,
457 : NULL);
458 0 : goto cleanup;
459 : }
460 18 : if (sold_reduced)
461 : {
462 0 : ret = TALER_MHD_reply_with_error (
463 : connection,
464 : MHD_HTTP_CONFLICT,
465 : TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED,
466 : NULL);
467 0 : goto cleanup;
468 : }
469 18 : if (stock_reduced)
470 : {
471 0 : ret = TALER_MHD_reply_with_error (
472 : connection,
473 : MHD_HTTP_CONFLICT,
474 : TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED,
475 : NULL);
476 0 : goto cleanup;
477 : }
478 : /* success! */
479 18 : ret = TALER_MHD_reply_static (connection,
480 : MHD_HTTP_NO_CONTENT,
481 : NULL,
482 : NULL,
483 : 0);
484 26 : cleanup:
485 26 : GNUNET_free (cats);
486 26 : GNUNET_free (pd.price_array);
487 26 : GNUNET_JSON_parse_free (spec);
488 26 : return ret;
489 : }
490 :
491 :
492 : /* end of taler-merchant-httpd_patch-private-products-PRODUCT_ID.c */
|