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