taler-merchant-httpd_post-private-products.c (14204B)
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 GNUNET_JSON_spec_string ("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 GNUNET_JSON_spec_string ("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 bool no_instance; 134 ssize_t no_cat; 135 bool no_group; 136 bool no_pot; 137 enum GNUNET_DB_QueryStatus qs; 138 enum MHD_Result ret; 139 140 GNUNET_assert (NULL != mi); 141 { 142 enum GNUNET_GenericReturnValue res; 143 144 res = TALER_MHD_parse_json_data (connection, 145 hc->request_body, 146 spec); 147 if (GNUNET_OK != res) 148 { 149 GNUNET_break_op (0); 150 return (GNUNET_NO == res) 151 ? MHD_YES 152 : MHD_NO; 153 } 154 /* For pre-v20 clients, we use the description given as the 155 product name; remove once we make product_name mandatory. */ 156 if (NULL == pd.product_name) 157 pd.product_name = pd.description; 158 159 if (! unit_price_missing) 160 { 161 if (! price_missing) 162 { 163 if (0 != TALER_amount_cmp (&price, 164 &pd.price_array[0])) 165 { 166 GNUNET_break_op (0); 167 ret = TALER_MHD_reply_with_error (connection, 168 MHD_HTTP_BAD_REQUEST, 169 TALER_EC_GENERIC_PARAMETER_MALFORMED, 170 "price,unit_price mismatch"); 171 goto cleanup; 172 } 173 } 174 if (GNUNET_OK != 175 TMH_validate_unit_price_array (pd.price_array, 176 pd.price_array_length)) 177 { 178 GNUNET_break_op (0); 179 ret = TALER_MHD_reply_with_error (connection, 180 MHD_HTTP_BAD_REQUEST, 181 TALER_EC_GENERIC_PARAMETER_MALFORMED, 182 "unit_price"); 183 goto cleanup; 184 } 185 } 186 else 187 { 188 if (price_missing) 189 { 190 GNUNET_break_op (0); 191 ret = TALER_MHD_reply_with_error (connection, 192 MHD_HTTP_BAD_REQUEST, 193 TALER_EC_GENERIC_PARAMETER_MALFORMED, 194 "price and unit_price missing"); 195 goto cleanup; 196 } 197 pd.price_array = GNUNET_new_array (1, 198 struct TALER_Amount); 199 pd.price_array[0] = price; 200 pd.price_array_length = 1; 201 } 202 } 203 if (! unit_precision_missing) 204 { 205 if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) 206 { 207 GNUNET_break_op (0); 208 ret = TALER_MHD_reply_with_error (connection, 209 MHD_HTTP_BAD_REQUEST, 210 TALER_EC_GENERIC_PARAMETER_MALFORMED, 211 "unit_precision_level"); 212 goto cleanup; 213 } 214 } 215 { 216 bool default_allow_fractional; 217 uint32_t default_precision_level; 218 219 if (GNUNET_OK != 220 TMH_unit_defaults_for_instance (mi, 221 pd.unit, 222 &default_allow_fractional, 223 &default_precision_level)) 224 { 225 GNUNET_break (0); 226 ret = TALER_MHD_reply_with_error (connection, 227 MHD_HTTP_INTERNAL_SERVER_ERROR, 228 TALER_EC_GENERIC_DB_FETCH_FAILED, 229 "unit defaults"); 230 goto cleanup; 231 } 232 if (unit_allow_fraction_missing) 233 unit_allow_fraction = default_allow_fractional; 234 if (unit_precision_missing) 235 unit_precision_level = default_precision_level; 236 } 237 if (! unit_allow_fraction) 238 unit_precision_level = 0; 239 pd.fractional_precision_level = unit_precision_level; 240 { 241 const char *eparam; 242 243 if (GNUNET_OK != 244 TALER_MERCHANT_vk_process_quantity_inputs ( 245 TALER_MERCHANT_VK_STOCK, 246 unit_allow_fraction, 247 total_stock_missing, 248 total_stock, 249 unit_total_stock_missing, 250 unit_total_stock, 251 &pd.total_stock, 252 &pd.total_stock_frac, 253 &eparam)) 254 { 255 GNUNET_break_op (0); 256 ret = TALER_MHD_reply_with_error ( 257 connection, 258 MHD_HTTP_BAD_REQUEST, 259 TALER_EC_GENERIC_PARAMETER_MALFORMED, 260 eparam); 261 goto cleanup; 262 } 263 pd.allow_fractional_quantity = unit_allow_fraction; 264 } 265 num_cats = json_array_size (categories); 266 cats = GNUNET_new_array (num_cats, 267 uint64_t); 268 { 269 size_t idx; 270 json_t *val; 271 272 json_array_foreach (categories, idx, val) 273 { 274 if (! json_is_integer (val)) 275 { 276 GNUNET_break_op (0); 277 ret = TALER_MHD_reply_with_error (connection, 278 MHD_HTTP_BAD_REQUEST, 279 TALER_EC_GENERIC_PARAMETER_MALFORMED, 280 "categories"); 281 goto cleanup; 282 } 283 cats[idx] = json_integer_value (val); 284 } 285 } 286 287 if (NULL == pd.address) 288 pd.address = json_object (); 289 if (NULL == pd.description_i18n) 290 pd.description_i18n = json_object (); 291 if (NULL == pd.taxes) 292 pd.taxes = json_array (); 293 294 /* check taxes is well-formed */ 295 if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) 296 { 297 GNUNET_break_op (0); 298 ret = TALER_MHD_reply_with_error (connection, 299 MHD_HTTP_BAD_REQUEST, 300 TALER_EC_GENERIC_PARAMETER_MALFORMED, 301 "taxes"); 302 goto cleanup; 303 } 304 305 if (! TMH_location_object_valid (pd.address)) 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 "address"); 312 goto cleanup; 313 } 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.image) 326 pd.image = (char *) ""; 327 if (! TALER_MERCHANT_image_data_url_valid (pd.image)) 328 { 329 GNUNET_break_op (0); 330 ret = TALER_MHD_reply_with_error (connection, 331 MHD_HTTP_BAD_REQUEST, 332 TALER_EC_GENERIC_PARAMETER_MALFORMED, 333 "image"); 334 goto cleanup; 335 } 336 337 qs = TALER_MERCHANTDB_insert_product (TMH_db, 338 mi->settings.id, 339 product_id, 340 &pd, 341 num_cats, 342 cats, 343 &no_instance, 344 &conflict, 345 &no_cat, 346 &no_group, 347 &no_pot); 348 switch (qs) 349 { 350 case GNUNET_DB_STATUS_HARD_ERROR: 351 case GNUNET_DB_STATUS_SOFT_ERROR: 352 ret = TALER_MHD_reply_with_error ( 353 connection, 354 MHD_HTTP_INTERNAL_SERVER_ERROR, 355 (GNUNET_DB_STATUS_SOFT_ERROR == qs) 356 ? TALER_EC_GENERIC_DB_SOFT_FAILURE 357 : TALER_EC_GENERIC_DB_COMMIT_FAILED, 358 NULL); 359 goto cleanup; 360 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 361 ret = TALER_MHD_reply_with_error ( 362 connection, 363 MHD_HTTP_INTERNAL_SERVER_ERROR, 364 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 365 NULL); 366 goto cleanup; 367 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 368 break; 369 } 370 if (no_instance) 371 { 372 ret = TALER_MHD_reply_with_error ( 373 connection, 374 MHD_HTTP_NOT_FOUND, 375 TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, 376 mi->settings.id); 377 goto cleanup; 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 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 */