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