taler-merchant-httpd_post-private-products.c (14565B)
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 enum GNUNET_DB_QueryStatus qs; 137 enum MHD_Result ret; 138 139 GNUNET_assert (NULL != mi); 140 { 141 enum GNUNET_GenericReturnValue res; 142 143 res = TALER_MHD_parse_json_data (connection, 144 hc->request_body, 145 spec); 146 if (GNUNET_OK != res) 147 { 148 GNUNET_break_op (0); 149 return (GNUNET_NO == res) 150 ? MHD_YES 151 : MHD_NO; 152 } 153 /* For pre-v20 clients, we use the description given as the 154 product name; remove once we make product_name mandatory. */ 155 if (NULL == pd.product_name) 156 pd.product_name = pd.description; 157 158 if ( (! unit_price_missing) && 159 (0 == pd.price_array_length) ) 160 { 161 GNUNET_break_op (0); 162 ret = TALER_MHD_reply_with_error (connection, 163 MHD_HTTP_BAD_REQUEST, 164 TALER_EC_GENERIC_PARAMETER_MALFORMED, 165 "unit_price"); 166 goto cleanup; 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 GNUNET_break_op (0); 176 ret = TALER_MHD_reply_with_error (connection, 177 MHD_HTTP_BAD_REQUEST, 178 TALER_EC_GENERIC_PARAMETER_MALFORMED, 179 "price,unit_price mismatch"); 180 goto cleanup; 181 } 182 } 183 if (GNUNET_OK != 184 TMH_validate_unit_price_array (pd.price_array, 185 pd.price_array_length)) 186 { 187 GNUNET_break_op (0); 188 ret = TALER_MHD_reply_with_error (connection, 189 MHD_HTTP_BAD_REQUEST, 190 TALER_EC_GENERIC_PARAMETER_MALFORMED, 191 "unit_price"); 192 goto cleanup; 193 } 194 } 195 else 196 { 197 if (price_missing) 198 { 199 GNUNET_break_op (0); 200 ret = TALER_MHD_reply_with_error (connection, 201 MHD_HTTP_BAD_REQUEST, 202 TALER_EC_GENERIC_PARAMETER_MALFORMED, 203 "price and unit_price missing"); 204 goto cleanup; 205 } 206 pd.price_array = GNUNET_new_array (1, 207 struct TALER_Amount); 208 pd.price_array[0] = price; 209 pd.price_array_length = 1; 210 } 211 } 212 if (! unit_precision_missing) 213 { 214 if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) 215 { 216 GNUNET_break_op (0); 217 ret = TALER_MHD_reply_with_error (connection, 218 MHD_HTTP_BAD_REQUEST, 219 TALER_EC_GENERIC_PARAMETER_MALFORMED, 220 "unit_precision_level"); 221 goto cleanup; 222 } 223 } 224 { 225 bool default_allow_fractional; 226 uint32_t default_precision_level; 227 228 if (GNUNET_OK != 229 TMH_unit_defaults_for_instance (mi, 230 pd.unit, 231 &default_allow_fractional, 232 &default_precision_level)) 233 { 234 GNUNET_break (0); 235 ret = TALER_MHD_reply_with_error (connection, 236 MHD_HTTP_INTERNAL_SERVER_ERROR, 237 TALER_EC_GENERIC_DB_FETCH_FAILED, 238 "unit defaults"); 239 goto cleanup; 240 } 241 if (unit_allow_fraction_missing) 242 unit_allow_fraction = default_allow_fractional; 243 if (unit_precision_missing) 244 unit_precision_level = default_precision_level; 245 } 246 if (! unit_allow_fraction) 247 unit_precision_level = 0; 248 pd.fractional_precision_level = unit_precision_level; 249 { 250 const char *eparam; 251 252 if (GNUNET_OK != 253 TALER_MERCHANT_vk_process_quantity_inputs ( 254 TALER_MERCHANT_VK_STOCK, 255 unit_allow_fraction, 256 total_stock_missing, 257 total_stock, 258 unit_total_stock_missing, 259 unit_total_stock, 260 &pd.total_stock, 261 &pd.total_stock_frac, 262 &eparam)) 263 { 264 GNUNET_break_op (0); 265 ret = TALER_MHD_reply_with_error ( 266 connection, 267 MHD_HTTP_BAD_REQUEST, 268 TALER_EC_GENERIC_PARAMETER_MALFORMED, 269 eparam); 270 goto cleanup; 271 } 272 pd.allow_fractional_quantity = unit_allow_fraction; 273 } 274 num_cats = json_array_size (categories); 275 cats = GNUNET_new_array (num_cats, 276 uint64_t); 277 { 278 size_t idx; 279 json_t *val; 280 281 json_array_foreach (categories, idx, val) 282 { 283 if ( (! json_is_integer (val)) || 284 (0 >= json_integer_value (val)) ) 285 { 286 GNUNET_break_op (0); 287 ret = TALER_MHD_reply_with_error (connection, 288 MHD_HTTP_BAD_REQUEST, 289 TALER_EC_GENERIC_PARAMETER_MALFORMED, 290 "categories"); 291 goto cleanup; 292 } 293 cats[idx] = json_integer_value (val); 294 } 295 } 296 297 if (NULL == pd.address) 298 pd.address = json_object (); 299 if (NULL == pd.description_i18n) 300 pd.description_i18n = json_object (); 301 if (NULL == pd.taxes) 302 pd.taxes = json_array (); 303 304 /* check taxes is well-formed */ 305 if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) 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 "taxes"); 312 goto cleanup; 313 } 314 315 if (! TMH_location_object_valid (pd.address)) 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 "address"); 322 goto cleanup; 323 } 324 325 if (! TALER_JSON_check_i18n (pd.description_i18n)) 326 { 327 GNUNET_break_op (0); 328 ret = TALER_MHD_reply_with_error (connection, 329 MHD_HTTP_BAD_REQUEST, 330 TALER_EC_GENERIC_PARAMETER_MALFORMED, 331 "description_i18n"); 332 goto cleanup; 333 } 334 335 if (NULL == pd.image) 336 pd.image = (char *) ""; 337 if (! TALER_MERCHANT_image_data_url_valid (pd.image)) 338 { 339 GNUNET_break_op (0); 340 ret = TALER_MHD_reply_with_error (connection, 341 MHD_HTTP_BAD_REQUEST, 342 TALER_EC_GENERIC_PARAMETER_MALFORMED, 343 "image"); 344 goto cleanup; 345 } 346 347 qs = TALER_MERCHANTDB_insert_product (TMH_db, 348 mi->settings.id, 349 product_id, 350 &pd, 351 num_cats, 352 cats, 353 &conflict, 354 &no_cat, 355 &no_group, 356 &no_pot); 357 switch (qs) 358 { 359 case GNUNET_DB_STATUS_HARD_ERROR: 360 case GNUNET_DB_STATUS_SOFT_ERROR: 361 ret = TALER_MHD_reply_with_error ( 362 connection, 363 MHD_HTTP_INTERNAL_SERVER_ERROR, 364 (GNUNET_DB_STATUS_SOFT_ERROR == qs) 365 ? TALER_EC_GENERIC_DB_SOFT_FAILURE 366 : TALER_EC_GENERIC_DB_COMMIT_FAILED, 367 NULL); 368 goto cleanup; 369 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 370 ret = TALER_MHD_reply_with_error ( 371 connection, 372 MHD_HTTP_INTERNAL_SERVER_ERROR, 373 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 374 NULL); 375 goto cleanup; 376 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 377 break; 378 } 379 if (no_group) 380 { 381 ret = TALER_MHD_reply_with_error ( 382 connection, 383 MHD_HTTP_NOT_FOUND, 384 TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, 385 NULL); 386 goto cleanup; 387 } 388 if (no_pot) 389 { 390 ret = TALER_MHD_reply_with_error ( 391 connection, 392 MHD_HTTP_NOT_FOUND, 393 TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, 394 NULL); 395 goto cleanup; 396 } 397 if (conflict) 398 { 399 ret = TALER_MHD_reply_with_error ( 400 connection, 401 MHD_HTTP_CONFLICT, 402 TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS, 403 product_id); 404 goto cleanup; 405 } 406 if (-1 != no_cat) 407 { 408 char nocats[24]; 409 410 GNUNET_break_op (0); 411 /* 'no_cat' is the 1-based index into 'cats' of the unknown 412 category; report the category itself to the client. */ 413 GNUNET_assert (no_cat > 0); 414 GNUNET_assert (no_cat <= (ssize_t) num_cats); 415 GNUNET_snprintf (nocats, 416 sizeof (nocats), 417 "%llu", 418 (unsigned long long) cats[no_cat - 1]); 419 ret = TALER_MHD_reply_with_error ( 420 connection, 421 MHD_HTTP_NOT_FOUND, 422 TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, 423 nocats); 424 goto cleanup; 425 } 426 ret = TALER_MHD_reply_static (connection, 427 MHD_HTTP_NO_CONTENT, 428 NULL, 429 NULL, 430 0); 431 cleanup: 432 GNUNET_JSON_parse_free (spec); 433 GNUNET_free (pd.price_array); 434 GNUNET_free (cats); 435 return ret; 436 } 437 438 439 /* end of taler-merchant-httpd_post-private-products.c */