taler-merchant-httpd_patch-private-products-PRODUCT_ID.c (16153B)
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 GNUNET_JSON_spec_string ("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_end () 139 }; 140 enum MHD_Result ret; 141 size_t num_cats = 0; 142 uint64_t *cats = NULL; 143 ssize_t no_cat; 144 bool no_product; 145 bool lost_reduced; 146 bool sold_reduced; 147 bool stock_reduced; 148 bool no_group; 149 bool no_pot; 150 151 GNUNET_assert (NULL != mi); 152 GNUNET_assert (NULL != product_id); 153 { 154 enum GNUNET_GenericReturnValue res; 155 156 res = TALER_MHD_parse_json_data (connection, 157 hc->request_body, 158 spec); 159 if (GNUNET_OK != res) 160 return (GNUNET_NO == res) 161 ? MHD_YES 162 : MHD_NO; 163 /* For pre-v20 clients, we use the description given as the 164 product name; remove once we make product_name mandatory. */ 165 if (NULL == pd.product_name) 166 pd.product_name = pd.description; 167 } 168 if ( (! unit_price_missing) && 169 (0 == pd.price_array_length) ) 170 { 171 GNUNET_break_op (0); 172 ret = TALER_MHD_reply_with_error (connection, 173 MHD_HTTP_BAD_REQUEST, 174 TALER_EC_GENERIC_PARAMETER_MALFORMED, 175 "unit_price"); 176 goto cleanup; 177 } 178 if (! unit_price_missing) 179 { 180 if (! price_missing) 181 { 182 if (0 != TALER_amount_cmp (&price, 183 &pd.price_array[0])) 184 { 185 ret = TALER_MHD_reply_with_error (connection, 186 MHD_HTTP_BAD_REQUEST, 187 TALER_EC_GENERIC_PARAMETER_MALFORMED, 188 "price,unit_price mismatch"); 189 goto cleanup; 190 } 191 } 192 if (GNUNET_OK != 193 TMH_validate_unit_price_array (pd.price_array, 194 pd.price_array_length)) 195 { 196 ret = TALER_MHD_reply_with_error (connection, 197 MHD_HTTP_BAD_REQUEST, 198 TALER_EC_GENERIC_PARAMETER_MALFORMED, 199 "unit_price"); 200 goto cleanup; 201 } 202 } 203 else 204 { 205 if (price_missing) 206 { 207 ret = TALER_MHD_reply_with_error (connection, 208 MHD_HTTP_BAD_REQUEST, 209 TALER_EC_GENERIC_PARAMETER_MALFORMED, 210 "price missing"); 211 goto cleanup; 212 } 213 pd.price_array = GNUNET_new_array (1, 214 struct TALER_Amount); 215 pd.price_array[0] = price; 216 pd.price_array_length = 1; 217 } 218 if (! unit_precision_missing) 219 { 220 if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) 221 { 222 ret = TALER_MHD_reply_with_error (connection, 223 MHD_HTTP_BAD_REQUEST, 224 TALER_EC_GENERIC_PARAMETER_MALFORMED, 225 "unit_precision_level"); 226 goto cleanup; 227 } 228 } 229 { 230 bool default_allow_fractional; 231 uint32_t default_precision_level; 232 233 if (GNUNET_OK != 234 TMH_unit_defaults_for_instance (mi, 235 pd.unit, 236 &default_allow_fractional, 237 &default_precision_level)) 238 { 239 GNUNET_break (0); 240 ret = TALER_MHD_reply_with_error (connection, 241 MHD_HTTP_INTERNAL_SERVER_ERROR, 242 TALER_EC_GENERIC_DB_FETCH_FAILED, 243 "unit defaults"); 244 goto cleanup; 245 } 246 if (unit_allow_fraction_missing) 247 unit_allow_fraction = default_allow_fractional; 248 if (unit_precision_missing) 249 unit_precision_level = default_precision_level; 250 251 if (! unit_allow_fraction) 252 unit_precision_level = 0; 253 pd.fractional_precision_level = unit_precision_level; 254 } 255 { 256 const char *eparam; 257 if (GNUNET_OK != 258 TALER_MERCHANT_vk_process_quantity_inputs ( 259 TALER_MERCHANT_VK_STOCK, 260 unit_allow_fraction, 261 total_stock_missing, 262 total_stock, 263 unit_total_stock_missing, 264 unit_total_stock, 265 &pd.total_stock, 266 &pd.total_stock_frac, 267 &eparam)) 268 { 269 ret = TALER_MHD_reply_with_error ( 270 connection, 271 MHD_HTTP_BAD_REQUEST, 272 TALER_EC_GENERIC_PARAMETER_MALFORMED, 273 eparam); 274 goto cleanup; 275 } 276 pd.allow_fractional_quantity = unit_allow_fraction; 277 } 278 if (NULL == pd.address) 279 pd.address = json_object (); 280 281 if (! TMH_location_object_valid (pd.address)) 282 { 283 GNUNET_break_op (0); 284 ret = TALER_MHD_reply_with_error (connection, 285 MHD_HTTP_BAD_REQUEST, 286 TALER_EC_GENERIC_PARAMETER_MALFORMED, 287 "address"); 288 goto cleanup; 289 } 290 num_cats = json_array_size (categories); 291 cats = GNUNET_new_array (num_cats, 292 uint64_t); 293 { 294 size_t idx; 295 json_t *val; 296 297 json_array_foreach (categories, idx, val) 298 { 299 if (! json_is_integer (val)) 300 { 301 GNUNET_break_op (0); 302 ret = TALER_MHD_reply_with_error (connection, 303 MHD_HTTP_BAD_REQUEST, 304 TALER_EC_GENERIC_PARAMETER_MALFORMED, 305 "categories"); 306 goto cleanup; 307 } 308 cats[idx] = json_integer_value (val); 309 } 310 } 311 312 if (NULL == pd.description_i18n) 313 pd.description_i18n = json_object (); 314 315 if (! TALER_JSON_check_i18n (pd.description_i18n)) 316 { 317 GNUNET_break_op (0); 318 ret = TALER_MHD_reply_with_error (connection, 319 MHD_HTTP_BAD_REQUEST, 320 TALER_EC_GENERIC_PARAMETER_MALFORMED, 321 "description_i18n"); 322 goto cleanup; 323 } 324 325 if (NULL == pd.taxes) 326 pd.taxes = json_array (); 327 /* check taxes is well-formed */ 328 if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) 329 { 330 GNUNET_break_op (0); 331 ret = TALER_MHD_reply_with_error (connection, 332 MHD_HTTP_BAD_REQUEST, 333 TALER_EC_GENERIC_PARAMETER_MALFORMED, 334 "taxes"); 335 goto cleanup; 336 } 337 338 if (NULL == pd.image) 339 pd.image = (char *) ""; 340 if (! TALER_MERCHANT_image_data_url_valid (pd.image)) 341 { 342 GNUNET_break_op (0); 343 ret = TALER_MHD_reply_with_error (connection, 344 MHD_HTTP_BAD_REQUEST, 345 TALER_EC_GENERIC_PARAMETER_MALFORMED, 346 "image"); 347 goto cleanup; 348 } 349 350 if ( (pd.total_stock < pd.total_sold + pd.total_lost) || 351 (pd.total_sold + pd.total_lost < pd.total_sold) /* integer overflow */) 352 { 353 GNUNET_break_op (0); 354 ret = TALER_MHD_reply_with_error ( 355 connection, 356 MHD_HTTP_BAD_REQUEST, 357 TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS, 358 NULL); 359 goto cleanup; 360 } 361 362 qs = TALER_MERCHANTDB_update_product (TMH_db, 363 mi->settings.id, 364 product_id, 365 &pd, 366 num_cats, 367 cats, 368 &no_cat, 369 &no_product, 370 &lost_reduced, 371 &sold_reduced, 372 &stock_reduced, 373 &no_group, 374 &no_pot); 375 switch (qs) 376 { 377 case GNUNET_DB_STATUS_HARD_ERROR: 378 GNUNET_break (0); 379 ret = TALER_MHD_reply_with_error (connection, 380 MHD_HTTP_INTERNAL_SERVER_ERROR, 381 TALER_EC_GENERIC_DB_STORE_FAILED, 382 NULL); 383 goto cleanup; 384 case GNUNET_DB_STATUS_SOFT_ERROR: 385 GNUNET_break (0); 386 ret = TALER_MHD_reply_with_error (connection, 387 MHD_HTTP_INTERNAL_SERVER_ERROR, 388 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 389 "unexpected serialization problem"); 390 goto cleanup; 391 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 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 problem in stored procedure"); 397 goto cleanup; 398 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 399 break; 400 } 401 402 if (-1 != no_cat) 403 { 404 char cat_str[24]; 405 406 GNUNET_snprintf (cat_str, 407 sizeof (cat_str), 408 "%llu", 409 (unsigned long long) no_cat); 410 ret = TALER_MHD_reply_with_error (connection, 411 MHD_HTTP_NOT_FOUND, 412 TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, 413 cat_str); 414 goto cleanup; 415 } 416 if (no_product) 417 { 418 ret = TALER_MHD_reply_with_error (connection, 419 MHD_HTTP_NOT_FOUND, 420 TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, 421 product_id); 422 goto cleanup; 423 } 424 if (no_group) 425 { 426 ret = TALER_MHD_reply_with_error ( 427 connection, 428 MHD_HTTP_NOT_FOUND, 429 TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, 430 NULL); 431 goto cleanup; 432 } 433 if (no_pot) 434 { 435 ret = TALER_MHD_reply_with_error ( 436 connection, 437 MHD_HTTP_NOT_FOUND, 438 TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, 439 NULL); 440 goto cleanup; 441 } 442 if (lost_reduced) 443 { 444 ret = TALER_MHD_reply_with_error ( 445 connection, 446 MHD_HTTP_CONFLICT, 447 TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED, 448 NULL); 449 goto cleanup; 450 } 451 if (sold_reduced) 452 { 453 ret = TALER_MHD_reply_with_error ( 454 connection, 455 MHD_HTTP_CONFLICT, 456 TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED, 457 NULL); 458 goto cleanup; 459 } 460 if (stock_reduced) 461 { 462 ret = TALER_MHD_reply_with_error ( 463 connection, 464 MHD_HTTP_CONFLICT, 465 TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED, 466 NULL); 467 goto cleanup; 468 } 469 /* success! */ 470 ret = TALER_MHD_reply_static (connection, 471 MHD_HTTP_NO_CONTENT, 472 NULL, 473 NULL, 474 0); 475 cleanup: 476 GNUNET_free (cats); 477 GNUNET_free (pd.price_array); 478 GNUNET_JSON_parse_free (spec); 479 return ret; 480 } 481 482 483 /* end of taler-merchant-httpd_patch-private-products-PRODUCT_ID.c */