taler-merchant-httpd_patch-private-products-PRODUCT_ID.c (16810B)
1 /* 2 This file is part of TALER 3 (C) 2020--2026 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_patch-private-products-PRODUCT_ID.c 22 * @brief implementing PATCH /products/$ID request handling 23 * @author Christian Grothoff 24 */ 25 #include "platform.h" 26 #include "taler-merchant-httpd_patch-private-products-PRODUCT_ID.h" 27 #include "taler-merchant-httpd_helper.h" 28 #include <taler/taler_json_lib.h> 29 #include "merchant-database/update_product.h" 30 31 32 /** 33 * PATCH configuration of an existing instance, given its configuration. 34 * 35 * @param rh context of the handler 36 * @param connection the MHD connection to handle 37 * @param[in,out] hc context with further information about the request 38 * @return MHD result code 39 */ 40 enum MHD_Result 41 TMH_private_patch_products_ID ( 42 const struct TMH_RequestHandler *rh, 43 struct MHD_Connection *connection, 44 struct TMH_HandlerContext *hc) 45 { 46 struct TMH_MerchantInstance *mi = hc->instance; 47 const char *product_id = hc->infix; 48 struct TALER_MERCHANTDB_ProductDetails pd = {0}; 49 const json_t *categories = NULL; 50 int64_t total_stock; 51 const char *unit_total_stock = NULL; 52 bool unit_total_stock_missing; 53 bool total_stock_missing; 54 struct TALER_Amount price; 55 bool price_missing; 56 bool unit_price_missing; 57 bool unit_allow_fraction; 58 bool unit_allow_fraction_missing; 59 uint32_t unit_precision_level; 60 bool unit_precision_missing; 61 enum GNUNET_DB_QueryStatus qs; 62 struct GNUNET_JSON_Specification spec[] = { 63 /* new in protocol v20, thus optional for backwards-compatibility */ 64 GNUNET_JSON_spec_mark_optional ( 65 GNUNET_JSON_spec_string ("product_name", 66 (const char **) &pd.product_name), 67 NULL), 68 GNUNET_JSON_spec_string ("description", 69 (const char **) &pd.description), 70 GNUNET_JSON_spec_mark_optional ( 71 GNUNET_JSON_spec_json ("description_i18n", 72 &pd.description_i18n), 73 NULL), 74 TALER_JSON_spec_slug ("unit", 75 (const char **) &pd.unit), 76 // FIXME: deprecated API 77 GNUNET_JSON_spec_mark_optional ( 78 TALER_JSON_spec_amount_any ("price", 79 &price), 80 &price_missing), 81 GNUNET_JSON_spec_mark_optional ( 82 TALER_JSON_spec_amount_any_array ("unit_price", 83 &pd.price_array_length, 84 &pd.price_array), 85 &unit_price_missing), 86 GNUNET_JSON_spec_mark_optional ( 87 GNUNET_JSON_spec_string ("image", 88 (const char **) &pd.image), 89 NULL), 90 GNUNET_JSON_spec_mark_optional ( 91 GNUNET_JSON_spec_json ("taxes", 92 &pd.taxes), 93 NULL), 94 GNUNET_JSON_spec_mark_optional ( 95 GNUNET_JSON_spec_array_const ("categories", 96 &categories), 97 NULL), 98 GNUNET_JSON_spec_mark_optional ( 99 GNUNET_JSON_spec_string ("unit_total_stock", 100 &unit_total_stock), 101 &unit_total_stock_missing), 102 GNUNET_JSON_spec_mark_optional ( 103 GNUNET_JSON_spec_int64 ("total_stock", 104 &total_stock), 105 &total_stock_missing), 106 GNUNET_JSON_spec_mark_optional ( 107 GNUNET_JSON_spec_bool ("unit_allow_fraction", 108 &unit_allow_fraction), 109 &unit_allow_fraction_missing), 110 GNUNET_JSON_spec_mark_optional ( 111 GNUNET_JSON_spec_uint32 ("unit_precision_level", 112 &unit_precision_level), 113 &unit_precision_missing), 114 GNUNET_JSON_spec_mark_optional ( 115 GNUNET_JSON_spec_uint64 ("total_lost", 116 &pd.total_lost), 117 NULL), 118 GNUNET_JSON_spec_mark_optional ( 119 GNUNET_JSON_spec_uint64 ("product_group_id", 120 &pd.product_group_id), 121 NULL), 122 GNUNET_JSON_spec_mark_optional ( 123 GNUNET_JSON_spec_uint64 ("money_pot_id", 124 &pd.money_pot_id), 125 NULL), 126 GNUNET_JSON_spec_mark_optional ( 127 GNUNET_JSON_spec_json ("address", 128 &pd.address), 129 NULL), 130 GNUNET_JSON_spec_mark_optional ( 131 GNUNET_JSON_spec_timestamp ("next_restock", 132 &pd.next_restock), 133 NULL), 134 GNUNET_JSON_spec_mark_optional ( 135 GNUNET_JSON_spec_uint32 ("minimum_age", 136 &pd.minimum_age), 137 NULL), 138 GNUNET_JSON_spec_mark_optional ( 139 GNUNET_JSON_spec_bool ("price_is_net", 140 &pd.price_is_net), 141 NULL), 142 GNUNET_JSON_spec_end () 143 }; 144 enum MHD_Result ret; 145 size_t num_cats = 0; 146 uint64_t *cats = NULL; 147 ssize_t no_cat; 148 bool no_product; 149 bool lost_reduced; 150 bool sold_reduced; 151 bool stock_reduced; 152 bool no_group; 153 bool no_pot; 154 bool no_unit; 155 156 GNUNET_assert (NULL != mi); 157 GNUNET_assert (NULL != product_id); 158 { 159 enum GNUNET_GenericReturnValue res; 160 161 res = TALER_MHD_parse_json_data (connection, 162 hc->request_body, 163 spec); 164 if (GNUNET_OK != res) 165 return (GNUNET_NO == res) 166 ? MHD_YES 167 : MHD_NO; 168 /* For pre-v20 clients, we use the description given as the 169 product name; remove once we make product_name mandatory. */ 170 if (NULL == pd.product_name) 171 pd.product_name = pd.description; 172 } 173 if ( (! unit_price_missing) && 174 (0 == pd.price_array_length) ) 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 "unit_price"); 181 goto cleanup; 182 } 183 if (! unit_price_missing) 184 { 185 if (! price_missing) 186 { 187 if (0 != TALER_amount_cmp (&price, 188 &pd.price_array[0])) 189 { 190 ret = TALER_MHD_reply_with_error (connection, 191 MHD_HTTP_BAD_REQUEST, 192 TALER_EC_GENERIC_PARAMETER_MALFORMED, 193 "price,unit_price mismatch"); 194 goto cleanup; 195 } 196 } 197 if (GNUNET_OK != 198 TMH_validate_unit_price_array (pd.price_array, 199 pd.price_array_length)) 200 { 201 ret = TALER_MHD_reply_with_error (connection, 202 MHD_HTTP_BAD_REQUEST, 203 TALER_EC_GENERIC_PARAMETER_MALFORMED, 204 "unit_price"); 205 goto cleanup; 206 } 207 } 208 else 209 { 210 if (price_missing) 211 { 212 ret = TALER_MHD_reply_with_error (connection, 213 MHD_HTTP_BAD_REQUEST, 214 TALER_EC_GENERIC_PARAMETER_MALFORMED, 215 "price missing"); 216 goto cleanup; 217 } 218 pd.price_array = GNUNET_new_array (1, 219 struct TALER_Amount); 220 pd.price_array[0] = price; 221 pd.price_array_length = 1; 222 } 223 if (! unit_precision_missing) 224 { 225 if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) 226 { 227 ret = TALER_MHD_reply_with_error (connection, 228 MHD_HTTP_BAD_REQUEST, 229 TALER_EC_GENERIC_PARAMETER_MALFORMED, 230 "unit_precision_level"); 231 goto cleanup; 232 } 233 } 234 { 235 bool default_allow_fractional; 236 uint32_t default_precision_level; 237 238 if (GNUNET_OK != 239 TMH_unit_defaults_for_instance (mi, 240 pd.unit, 241 &default_allow_fractional, 242 &default_precision_level)) 243 { 244 GNUNET_break (0); 245 ret = TALER_MHD_reply_with_error (connection, 246 MHD_HTTP_INTERNAL_SERVER_ERROR, 247 TALER_EC_GENERIC_DB_FETCH_FAILED, 248 "unit defaults"); 249 goto cleanup; 250 } 251 if (unit_allow_fraction_missing) 252 unit_allow_fraction = default_allow_fractional; 253 if (unit_precision_missing) 254 unit_precision_level = default_precision_level; 255 256 if (! unit_allow_fraction) 257 unit_precision_level = 0; 258 pd.fractional_precision_level = unit_precision_level; 259 } 260 { 261 const char *eparam; 262 if (GNUNET_OK != 263 TALER_MERCHANT_vk_process_quantity_inputs ( 264 TALER_MERCHANT_VK_STOCK, 265 unit_allow_fraction, 266 total_stock_missing, 267 total_stock, 268 unit_total_stock_missing, 269 unit_total_stock, 270 &pd.total_stock, 271 &pd.total_stock_frac, 272 &eparam)) 273 { 274 ret = TALER_MHD_reply_with_error ( 275 connection, 276 MHD_HTTP_BAD_REQUEST, 277 TALER_EC_GENERIC_PARAMETER_MALFORMED, 278 eparam); 279 goto cleanup; 280 } 281 pd.allow_fractional_quantity = unit_allow_fraction; 282 } 283 if (NULL == pd.address) 284 pd.address = json_object (); 285 286 if (! TMH_location_object_valid (pd.address)) 287 { 288 GNUNET_break_op (0); 289 ret = TALER_MHD_reply_with_error (connection, 290 MHD_HTTP_BAD_REQUEST, 291 TALER_EC_GENERIC_PARAMETER_MALFORMED, 292 "address"); 293 goto cleanup; 294 } 295 num_cats = json_array_size (categories); 296 cats = GNUNET_new_array (num_cats, 297 uint64_t); 298 { 299 size_t idx; 300 json_t *val; 301 302 json_array_foreach (categories, idx, val) 303 { 304 if ( (! json_is_integer (val)) || 305 (0 >= json_integer_value (val)) ) 306 { 307 GNUNET_break_op (0); 308 ret = TALER_MHD_reply_with_error (connection, 309 MHD_HTTP_BAD_REQUEST, 310 TALER_EC_GENERIC_PARAMETER_MALFORMED, 311 "categories"); 312 goto cleanup; 313 } 314 cats[idx] = json_integer_value (val); 315 } 316 } 317 318 if (NULL == pd.description_i18n) 319 pd.description_i18n = json_object (); 320 321 if (! TALER_JSON_check_i18n (pd.description_i18n)) 322 { 323 GNUNET_break_op (0); 324 ret = TALER_MHD_reply_with_error (connection, 325 MHD_HTTP_BAD_REQUEST, 326 TALER_EC_GENERIC_PARAMETER_MALFORMED, 327 "description_i18n"); 328 goto cleanup; 329 } 330 331 if (NULL == pd.taxes) 332 pd.taxes = json_array (); 333 /* check taxes is well-formed */ 334 if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) 335 { 336 GNUNET_break_op (0); 337 ret = TALER_MHD_reply_with_error (connection, 338 MHD_HTTP_BAD_REQUEST, 339 TALER_EC_GENERIC_PARAMETER_MALFORMED, 340 "taxes"); 341 goto cleanup; 342 } 343 344 if (NULL == pd.image) 345 pd.image = (char *) ""; 346 if (! TALER_MERCHANT_image_data_url_valid (pd.image)) 347 { 348 GNUNET_break_op (0); 349 ret = TALER_MHD_reply_with_error (connection, 350 MHD_HTTP_BAD_REQUEST, 351 TALER_EC_GENERIC_PARAMETER_MALFORMED, 352 "image"); 353 goto cleanup; 354 } 355 356 if ( (pd.total_stock < pd.total_sold + pd.total_lost) || 357 (pd.total_sold + pd.total_lost < pd.total_sold) /* integer overflow */) 358 { 359 GNUNET_break_op (0); 360 ret = TALER_MHD_reply_with_error ( 361 connection, 362 MHD_HTTP_BAD_REQUEST, 363 TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS, 364 NULL); 365 goto cleanup; 366 } 367 368 qs = TALER_MERCHANTDB_update_product (TMH_db, 369 mi->settings.id, 370 product_id, 371 &pd, 372 num_cats, 373 cats, 374 &no_cat, 375 &no_product, 376 &lost_reduced, 377 &sold_reduced, 378 &stock_reduced, 379 &no_group, 380 &no_pot, 381 &no_unit); 382 switch (qs) 383 { 384 case GNUNET_DB_STATUS_HARD_ERROR: 385 GNUNET_break (0); 386 ret = TALER_MHD_reply_with_error (connection, 387 MHD_HTTP_INTERNAL_SERVER_ERROR, 388 TALER_EC_GENERIC_DB_STORE_FAILED, 389 NULL); 390 goto cleanup; 391 case GNUNET_DB_STATUS_SOFT_ERROR: 392 GNUNET_break (0); 393 ret = TALER_MHD_reply_with_error (connection, 394 MHD_HTTP_INTERNAL_SERVER_ERROR, 395 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 396 "unexpected serialization problem"); 397 goto cleanup; 398 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 399 GNUNET_break (0); 400 ret = TALER_MHD_reply_with_error (connection, 401 MHD_HTTP_INTERNAL_SERVER_ERROR, 402 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 403 "unexpected problem in stored procedure"); 404 goto cleanup; 405 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 406 break; 407 } 408 409 if (no_unit) 410 { 411 ret = TALER_MHD_reply_with_error ( 412 connection, 413 MHD_HTTP_NOT_FOUND, 414 TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN, 415 pd.unit); 416 goto cleanup; 417 } 418 if (-1 != no_cat) 419 { 420 char cat_str[24]; 421 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 (cat_str, 427 sizeof (cat_str), 428 "%llu", 429 (unsigned long long) cats[no_cat - 1]); 430 ret = TALER_MHD_reply_with_error (connection, 431 MHD_HTTP_NOT_FOUND, 432 TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, 433 cat_str); 434 goto cleanup; 435 } 436 if (no_product) 437 { 438 ret = TALER_MHD_reply_with_error (connection, 439 MHD_HTTP_NOT_FOUND, 440 TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, 441 product_id); 442 goto cleanup; 443 } 444 if (no_group) 445 { 446 ret = TALER_MHD_reply_with_error ( 447 connection, 448 MHD_HTTP_NOT_FOUND, 449 TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, 450 NULL); 451 goto cleanup; 452 } 453 if (no_pot) 454 { 455 ret = TALER_MHD_reply_with_error ( 456 connection, 457 MHD_HTTP_NOT_FOUND, 458 TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, 459 NULL); 460 goto cleanup; 461 } 462 if (lost_reduced) 463 { 464 ret = TALER_MHD_reply_with_error ( 465 connection, 466 MHD_HTTP_CONFLICT, 467 TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED, 468 NULL); 469 goto cleanup; 470 } 471 if (sold_reduced) 472 { 473 ret = TALER_MHD_reply_with_error ( 474 connection, 475 MHD_HTTP_CONFLICT, 476 TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED, 477 NULL); 478 goto cleanup; 479 } 480 if (stock_reduced) 481 { 482 ret = TALER_MHD_reply_with_error ( 483 connection, 484 MHD_HTTP_CONFLICT, 485 TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED, 486 NULL); 487 goto cleanup; 488 } 489 /* success! */ 490 ret = TALER_MHD_reply_static (connection, 491 MHD_HTTP_NO_CONTENT, 492 NULL, 493 NULL, 494 0); 495 cleanup: 496 GNUNET_free (cats); 497 GNUNET_free (pd.price_array); 498 GNUNET_JSON_parse_free (spec); 499 return ret; 500 } 501 502 503 /* end of taler-merchant-httpd_patch-private-products-PRODUCT_ID.c */