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