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