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