taler-merchant-httpd_post-private-products.c (14303B)
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 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 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 { 285 GNUNET_break_op (0); 286 ret = TALER_MHD_reply_with_error (connection, 287 MHD_HTTP_BAD_REQUEST, 288 TALER_EC_GENERIC_PARAMETER_MALFORMED, 289 "categories"); 290 goto cleanup; 291 } 292 cats[idx] = json_integer_value (val); 293 } 294 } 295 296 if (NULL == pd.address) 297 pd.address = json_object (); 298 if (NULL == pd.description_i18n) 299 pd.description_i18n = json_object (); 300 if (NULL == pd.taxes) 301 pd.taxes = json_array (); 302 303 /* check taxes is well-formed */ 304 if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) 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 "taxes"); 311 goto cleanup; 312 } 313 314 if (! TMH_location_object_valid (pd.address)) 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 "address"); 321 goto cleanup; 322 } 323 324 if (! TALER_JSON_check_i18n (pd.description_i18n)) 325 { 326 GNUNET_break_op (0); 327 ret = TALER_MHD_reply_with_error (connection, 328 MHD_HTTP_BAD_REQUEST, 329 TALER_EC_GENERIC_PARAMETER_MALFORMED, 330 "description_i18n"); 331 goto cleanup; 332 } 333 334 if (NULL == pd.image) 335 pd.image = (char *) ""; 336 if (! TALER_MERCHANT_image_data_url_valid (pd.image)) 337 { 338 GNUNET_break_op (0); 339 ret = TALER_MHD_reply_with_error (connection, 340 MHD_HTTP_BAD_REQUEST, 341 TALER_EC_GENERIC_PARAMETER_MALFORMED, 342 "image"); 343 goto cleanup; 344 } 345 346 qs = TALER_MERCHANTDB_insert_product (TMH_db, 347 mi->settings.id, 348 product_id, 349 &pd, 350 num_cats, 351 cats, 352 &conflict, 353 &no_cat, 354 &no_group, 355 &no_pot); 356 switch (qs) 357 { 358 case GNUNET_DB_STATUS_HARD_ERROR: 359 case GNUNET_DB_STATUS_SOFT_ERROR: 360 ret = TALER_MHD_reply_with_error ( 361 connection, 362 MHD_HTTP_INTERNAL_SERVER_ERROR, 363 (GNUNET_DB_STATUS_SOFT_ERROR == qs) 364 ? TALER_EC_GENERIC_DB_SOFT_FAILURE 365 : TALER_EC_GENERIC_DB_COMMIT_FAILED, 366 NULL); 367 goto cleanup; 368 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 369 ret = TALER_MHD_reply_with_error ( 370 connection, 371 MHD_HTTP_INTERNAL_SERVER_ERROR, 372 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 373 NULL); 374 goto cleanup; 375 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 376 break; 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 GNUNET_snprintf (nocats, 411 sizeof (nocats), 412 "%llu", 413 (unsigned long long) no_cat); 414 ret = TALER_MHD_reply_with_error ( 415 connection, 416 MHD_HTTP_NOT_FOUND, 417 TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, 418 nocats); 419 goto cleanup; 420 } 421 ret = TALER_MHD_reply_static (connection, 422 MHD_HTTP_NO_CONTENT, 423 NULL, 424 NULL, 425 0); 426 cleanup: 427 GNUNET_JSON_parse_free (spec); 428 GNUNET_free (pd.price_array); 429 GNUNET_free (cats); 430 return ret; 431 } 432 433 434 /* end of taler-merchant-httpd_post-private-products.c */