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