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