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