taler-merchant-httpd_post-private-products.c (13922B)
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 { 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 = TALER_MERCHANTDB_insert_product (TMH_db, 337 mi->settings.id, 338 product_id, 339 &pd, 340 num_cats, 341 cats, 342 &conflict, 343 &no_cat, 344 &no_group, 345 &no_pot); 346 switch (qs) 347 { 348 case GNUNET_DB_STATUS_HARD_ERROR: 349 case GNUNET_DB_STATUS_SOFT_ERROR: 350 ret = TALER_MHD_reply_with_error ( 351 connection, 352 MHD_HTTP_INTERNAL_SERVER_ERROR, 353 (GNUNET_DB_STATUS_SOFT_ERROR == qs) 354 ? TALER_EC_GENERIC_DB_SOFT_FAILURE 355 : TALER_EC_GENERIC_DB_COMMIT_FAILED, 356 NULL); 357 goto cleanup; 358 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 359 ret = TALER_MHD_reply_with_error ( 360 connection, 361 MHD_HTTP_INTERNAL_SERVER_ERROR, 362 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 363 NULL); 364 goto cleanup; 365 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 366 break; 367 } 368 if (no_group) 369 { 370 ret = TALER_MHD_reply_with_error ( 371 connection, 372 MHD_HTTP_NOT_FOUND, 373 TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, 374 NULL); 375 goto cleanup; 376 } 377 if (no_pot) 378 { 379 ret = TALER_MHD_reply_with_error ( 380 connection, 381 MHD_HTTP_NOT_FOUND, 382 TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, 383 NULL); 384 goto cleanup; 385 } 386 if (conflict) 387 { 388 ret = TALER_MHD_reply_with_error ( 389 connection, 390 MHD_HTTP_CONFLICT, 391 TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS, 392 product_id); 393 goto cleanup; 394 } 395 if (-1 != no_cat) 396 { 397 char nocats[24]; 398 399 GNUNET_break_op (0); 400 GNUNET_snprintf (nocats, 401 sizeof (nocats), 402 "%llu", 403 (unsigned long long) no_cat); 404 ret = TALER_MHD_reply_with_error ( 405 connection, 406 MHD_HTTP_NOT_FOUND, 407 TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, 408 nocats); 409 goto cleanup; 410 } 411 ret = TALER_MHD_reply_static (connection, 412 MHD_HTTP_NO_CONTENT, 413 NULL, 414 NULL, 415 0); 416 cleanup: 417 GNUNET_JSON_parse_free (spec); 418 GNUNET_free (pd.price_array); 419 GNUNET_free (cats); 420 return ret; 421 } 422 423 424 /* end of taler-merchant-httpd_post-private-products.c */