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 :
139 14 : pd.total_sold = 0; /* will be ignored anyway */
140 14 : GNUNET_assert (NULL != mi);
141 14 : GNUNET_assert (NULL != product_id);
142 : {
143 : enum GNUNET_GenericReturnValue res;
144 :
145 14 : res = TALER_MHD_parse_json_data (connection,
146 14 : hc->request_body,
147 : spec);
148 14 : if (GNUNET_OK != res)
149 : return (GNUNET_NO == res)
150 : ? MHD_YES
151 0 : : MHD_NO;
152 : /* For pre-v20 clients, we use the description given as the
153 : product name; remove once we make product_name mandatory. */
154 14 : if (NULL == pd.product_name)
155 0 : pd.product_name = pd.description;
156 : }
157 14 : if (! unit_price_missing)
158 : {
159 14 : if (! price_missing)
160 : {
161 0 : if (0 != TALER_amount_cmp (&pd.price,
162 0 : &pd.price_array[0]))
163 : {
164 0 : ret = TALER_MHD_reply_with_error (connection,
165 : MHD_HTTP_BAD_REQUEST,
166 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
167 : "price,unit_price mismatch");
168 0 : goto cleanup;
169 : }
170 : }
171 : else
172 : {
173 14 : pd.price = pd.price_array[0];
174 14 : price_missing = false;
175 : }
176 : }
177 : else
178 : {
179 0 : if (price_missing)
180 : {
181 0 : ret = TALER_MHD_reply_with_error (connection,
182 : MHD_HTTP_BAD_REQUEST,
183 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
184 : "price missing");
185 0 : goto cleanup;
186 : }
187 0 : pd.price_array = GNUNET_new_array (1,
188 : struct TALER_Amount);
189 0 : pd.price_array[0] = pd.price;
190 0 : pd.price_array_length = 1;
191 : }
192 14 : if (! unit_precision_missing)
193 : {
194 4 : if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL)
195 : {
196 0 : ret = TALER_MHD_reply_with_error (connection,
197 : MHD_HTTP_BAD_REQUEST,
198 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
199 : "unit_precision_level");
200 0 : goto cleanup;
201 : }
202 : }
203 : {
204 : bool default_allow_fractional;
205 : uint32_t default_precision_level;
206 :
207 14 : if (GNUNET_OK !=
208 14 : TMH_unit_defaults_for_instance (mi,
209 14 : pd.unit,
210 : &default_allow_fractional,
211 : &default_precision_level))
212 : {
213 0 : GNUNET_break (0);
214 0 : ret = TALER_MHD_reply_with_error (connection,
215 : MHD_HTTP_INTERNAL_SERVER_ERROR,
216 : TALER_EC_GENERIC_DB_FETCH_FAILED,
217 : "unit defaults");
218 0 : goto cleanup;
219 : }
220 14 : if (unit_allow_fraction_missing)
221 10 : unit_allow_fraction = default_allow_fractional;
222 14 : if (unit_precision_missing)
223 10 : unit_precision_level = default_precision_level;
224 :
225 14 : if (! unit_allow_fraction)
226 10 : unit_precision_level = 0;
227 14 : pd.fractional_precision_level = unit_precision_level;
228 : }
229 : {
230 : const char *eparam;
231 14 : if (GNUNET_OK !=
232 14 : TMH_process_quantity_inputs (TMH_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 (! TMH_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 (! TMH_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 14 : switch (qs)
348 : {
349 0 : case GNUNET_DB_STATUS_HARD_ERROR:
350 0 : GNUNET_break (0);
351 0 : ret = TALER_MHD_reply_with_error (connection,
352 : MHD_HTTP_INTERNAL_SERVER_ERROR,
353 : TALER_EC_GENERIC_DB_STORE_FAILED,
354 : NULL);
355 0 : goto cleanup;
356 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
357 0 : GNUNET_break (0);
358 0 : ret = TALER_MHD_reply_with_error (connection,
359 : MHD_HTTP_INTERNAL_SERVER_ERROR,
360 : TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
361 : "unexpected serialization problem");
362 0 : goto cleanup;
363 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
364 0 : GNUNET_break (0);
365 0 : ret = TALER_MHD_reply_with_error (connection,
366 : MHD_HTTP_INTERNAL_SERVER_ERROR,
367 : TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
368 : "unexpected problem in stored procedure");
369 0 : goto cleanup;
370 14 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
371 14 : break;
372 : }
373 :
374 14 : if (no_instance)
375 : {
376 0 : ret = TALER_MHD_reply_with_error (connection,
377 : MHD_HTTP_NOT_FOUND,
378 : TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
379 0 : mi->settings.id);
380 0 : goto cleanup;
381 : }
382 14 : if (-1 != no_cat)
383 : {
384 : char cat_str[24];
385 :
386 0 : GNUNET_snprintf (cat_str,
387 : sizeof (cat_str),
388 : "%llu",
389 : (unsigned long long) no_cat);
390 0 : ret = TALER_MHD_reply_with_error (connection,
391 : MHD_HTTP_NOT_FOUND,
392 : TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN,
393 : cat_str);
394 0 : goto cleanup;
395 : }
396 14 : if (no_product)
397 : {
398 2 : ret = TALER_MHD_reply_with_error (connection,
399 : MHD_HTTP_NOT_FOUND,
400 : TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
401 : product_id);
402 2 : goto cleanup;
403 : }
404 12 : if (lost_reduced)
405 : {
406 0 : ret = TALER_MHD_reply_with_error (
407 : connection,
408 : MHD_HTTP_CONFLICT,
409 : TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED,
410 : NULL);
411 0 : goto cleanup;
412 : }
413 12 : if (sold_reduced)
414 : {
415 0 : ret = TALER_MHD_reply_with_error (
416 : connection,
417 : MHD_HTTP_CONFLICT,
418 : TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED,
419 : NULL);
420 0 : goto cleanup;
421 : }
422 12 : if (stock_reduced)
423 : {
424 0 : ret = TALER_MHD_reply_with_error (
425 : connection,
426 : MHD_HTTP_CONFLICT,
427 : TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED,
428 : NULL);
429 0 : goto cleanup;
430 : }
431 : /* success! */
432 12 : ret = TALER_MHD_reply_static (connection,
433 : MHD_HTTP_NO_CONTENT,
434 : NULL,
435 : NULL,
436 : 0);
437 14 : cleanup:
438 14 : GNUNET_free (cats);
439 14 : GNUNET_free (pd.price_array);
440 14 : GNUNET_JSON_parse_free (spec);
441 14 : return ret;
442 : }
443 :
444 :
445 : /* end of taler-merchant-httpd_private-patch-products-ID.c */
|