LCOV - code coverage report
Current view: top level - backend - taler-merchant-httpd_post-private-products.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 67.5 % 151 102
Test Date: 2026-09-04 23:42:01 Functions: 100.0 % 1 1

            Line data    Source code
       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           44 : TMH_private_post_products (const struct TMH_RequestHandler *rh,
      33              :                            struct MHD_Connection *connection,
      34              :                            struct TMH_HandlerContext *hc)
      35              : {
      36           44 :   struct TMH_MerchantInstance *mi = hc->instance;
      37           44 :   struct TALER_MERCHANTDB_ProductDetails pd = { 0 };
      38           44 :   const json_t *categories = NULL;
      39              :   const char *product_id;
      40              :   int64_t total_stock;
      41           44 :   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           44 :     TALER_JSON_spec_slug ("product_id",
      53              :                           &product_id),
      54              :     /* new in protocol v20, thus optional for backwards-compatibility */
      55           44 :     GNUNET_JSON_spec_mark_optional (
      56              :       GNUNET_JSON_spec_string ("product_name",
      57              :                                (const char **) &pd.product_name),
      58              :       NULL),
      59           44 :     GNUNET_JSON_spec_string ("description",
      60              :                              (const char **) &pd.description),
      61           44 :     GNUNET_JSON_spec_mark_optional (
      62              :       GNUNET_JSON_spec_json ("description_i18n",
      63              :                              &pd.description_i18n),
      64              :       NULL),
      65           44 :     TALER_JSON_spec_slug ("unit",
      66              :                           (const char **) &pd.unit),
      67           44 :     GNUNET_JSON_spec_mark_optional (
      68              :       TALER_JSON_spec_amount_any ("price",
      69              :                                   &price),
      70              :       &price_missing),
      71           44 :     GNUNET_JSON_spec_mark_optional (
      72              :       GNUNET_JSON_spec_string ("image",
      73              :                                (const char **) &pd.image),
      74              :       NULL),
      75           44 :     GNUNET_JSON_spec_mark_optional (
      76              :       GNUNET_JSON_spec_json ("taxes",
      77              :                              &pd.taxes),
      78              :       NULL),
      79           44 :     GNUNET_JSON_spec_mark_optional (
      80              :       GNUNET_JSON_spec_array_const ("categories",
      81              :                                     &categories),
      82              :       NULL),
      83           44 :     GNUNET_JSON_spec_mark_optional (
      84              :       GNUNET_JSON_spec_string ("unit_total_stock",
      85              :                                &unit_total_stock),
      86              :       &unit_total_stock_missing),
      87           44 :     GNUNET_JSON_spec_mark_optional (
      88              :       GNUNET_JSON_spec_int64 ("total_stock",
      89              :                               &total_stock),
      90              :       &total_stock_missing),
      91           44 :     GNUNET_JSON_spec_mark_optional (
      92              :       GNUNET_JSON_spec_bool ("unit_allow_fraction",
      93              :                              &unit_allow_fraction),
      94              :       &unit_allow_fraction_missing),
      95           44 :     GNUNET_JSON_spec_mark_optional (
      96              :       GNUNET_JSON_spec_uint32 ("unit_precision_level",
      97              :                                &unit_precision_level),
      98              :       &unit_precision_missing),
      99           44 :     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           44 :     GNUNET_JSON_spec_mark_optional (
     105              :       GNUNET_JSON_spec_json ("address",
     106              :                              &pd.address),
     107              :       NULL),
     108           44 :     GNUNET_JSON_spec_mark_optional (
     109              :       GNUNET_JSON_spec_timestamp ("next_restock",
     110              :                                   &pd.next_restock),
     111              :       NULL),
     112           44 :     GNUNET_JSON_spec_mark_optional (
     113              :       GNUNET_JSON_spec_uint32 ("minimum_age",
     114              :                                &pd.minimum_age),
     115              :       NULL),
     116           44 :     GNUNET_JSON_spec_mark_optional (
     117              :       GNUNET_JSON_spec_uint64 ("money_pot_id",
     118              :                                &pd.money_pot_id),
     119              :       NULL),
     120           44 :     GNUNET_JSON_spec_mark_optional (
     121              :       GNUNET_JSON_spec_uint64 ("product_group_id",
     122              :                                &pd.product_group_id),
     123              :       NULL),
     124           44 :     GNUNET_JSON_spec_mark_optional (
     125              :       GNUNET_JSON_spec_bool ("price_is_net",
     126              :                              &pd.price_is_net),
     127              :       NULL),
     128           44 :     GNUNET_JSON_spec_end ()
     129              :   };
     130           44 :   size_t num_cats = 0;
     131           44 :   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           44 :   GNUNET_assert (NULL != mi);
     140              :   {
     141              :     enum GNUNET_GenericReturnValue res;
     142              : 
     143           44 :     res = TALER_MHD_parse_json_data (connection,
     144           44 :                                      hc->request_body,
     145              :                                      spec);
     146           44 :     if (GNUNET_OK != res)
     147              :     {
     148            0 :       GNUNET_break_op (0);
     149              :       return (GNUNET_NO == res)
     150              :              ? MHD_YES
     151            0 :              : 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           44 :     if (NULL == pd.product_name)
     156            2 :       pd.product_name = pd.description;
     157              : 
     158           44 :     if ( (! unit_price_missing) &&
     159           44 :          (0 == pd.price_array_length) )
     160              :     {
     161            0 :       GNUNET_break_op (0);
     162            0 :       ret = TALER_MHD_reply_with_error (connection,
     163              :                                         MHD_HTTP_BAD_REQUEST,
     164              :                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     165              :                                         "unit_price");
     166            0 :       goto cleanup;
     167              :     }
     168           44 :     if (! unit_price_missing)
     169              :     {
     170           44 :       if (! price_missing)
     171              :       {
     172            0 :         if (0 != TALER_amount_cmp (&price,
     173            0 :                                    &pd.price_array[0]))
     174              :         {
     175            0 :           GNUNET_break_op (0);
     176            0 :           ret = TALER_MHD_reply_with_error (connection,
     177              :                                             MHD_HTTP_BAD_REQUEST,
     178              :                                             TALER_EC_GENERIC_PARAMETER_MALFORMED,
     179              :                                             "price,unit_price mismatch");
     180            0 :           goto cleanup;
     181              :         }
     182              :       }
     183           44 :       if (GNUNET_OK !=
     184           44 :           TMH_validate_unit_price_array (pd.price_array,
     185              :                                          pd.price_array_length))
     186              :       {
     187            2 :         GNUNET_break_op (0);
     188            2 :         ret = TALER_MHD_reply_with_error (connection,
     189              :                                           MHD_HTTP_BAD_REQUEST,
     190              :                                           TALER_EC_GENERIC_PARAMETER_MALFORMED,
     191              :                                           "unit_price");
     192            2 :         goto cleanup;
     193              :       }
     194              :     }
     195              :     else
     196              :     {
     197            0 :       if (price_missing)
     198              :       {
     199            0 :         GNUNET_break_op (0);
     200            0 :         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            0 :         goto cleanup;
     205              :       }
     206            0 :       pd.price_array = GNUNET_new_array (1,
     207              :                                          struct TALER_Amount);
     208            0 :       pd.price_array[0] = price;
     209            0 :       pd.price_array_length = 1;
     210              :     }
     211              :   }
     212           42 :   if (! unit_precision_missing)
     213              :   {
     214            4 :     if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL)
     215              :     {
     216            0 :       GNUNET_break_op (0);
     217            0 :       ret = TALER_MHD_reply_with_error (connection,
     218              :                                         MHD_HTTP_BAD_REQUEST,
     219              :                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     220              :                                         "unit_precision_level");
     221            0 :       goto cleanup;
     222              :     }
     223              :   }
     224              :   {
     225              :     bool default_allow_fractional;
     226              :     uint32_t default_precision_level;
     227              : 
     228           42 :     if (GNUNET_OK !=
     229           42 :         TMH_unit_defaults_for_instance (mi,
     230           42 :                                         pd.unit,
     231              :                                         &default_allow_fractional,
     232              :                                         &default_precision_level))
     233              :     {
     234            0 :       GNUNET_break (0);
     235            0 :       ret = TALER_MHD_reply_with_error (connection,
     236              :                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
     237              :                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
     238              :                                         "unit defaults");
     239            0 :       goto cleanup;
     240              :     }
     241           42 :     if (unit_allow_fraction_missing)
     242           38 :       unit_allow_fraction = default_allow_fractional;
     243           42 :     if (unit_precision_missing)
     244           38 :       unit_precision_level = default_precision_level;
     245              :   }
     246           42 :   if (! unit_allow_fraction)
     247           36 :     unit_precision_level = 0;
     248           42 :   pd.fractional_precision_level = unit_precision_level;
     249              :   {
     250              :     const char *eparam;
     251              : 
     252           42 :     if (GNUNET_OK !=
     253           42 :         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            0 :       GNUNET_break_op (0);
     265            0 :       ret = TALER_MHD_reply_with_error (
     266              :         connection,
     267              :         MHD_HTTP_BAD_REQUEST,
     268              :         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     269              :         eparam);
     270            0 :       goto cleanup;
     271              :     }
     272           42 :     pd.allow_fractional_quantity = unit_allow_fraction;
     273              :   }
     274           42 :   num_cats = json_array_size (categories);
     275           42 :   cats = GNUNET_new_array (num_cats,
     276              :                            uint64_t);
     277              :   {
     278              :     size_t idx;
     279              :     json_t *val;
     280              : 
     281           50 :     json_array_foreach (categories, idx, val)
     282              :     {
     283           20 :       if ( (! json_is_integer (val)) ||
     284           10 :            (0 >= json_integer_value (val)) )
     285              :       {
     286            2 :         GNUNET_break_op (0);
     287            2 :         ret = TALER_MHD_reply_with_error (connection,
     288              :                                           MHD_HTTP_BAD_REQUEST,
     289              :                                           TALER_EC_GENERIC_PARAMETER_MALFORMED,
     290              :                                           "categories");
     291            2 :         goto cleanup;
     292              :       }
     293            8 :       cats[idx] = json_integer_value (val);
     294              :     }
     295              :   }
     296              : 
     297           40 :   if (NULL == pd.address)
     298            8 :     pd.address = json_object ();
     299           40 :   if (NULL == pd.description_i18n)
     300            6 :     pd.description_i18n = json_object ();
     301           40 :   if (NULL == pd.taxes)
     302            4 :     pd.taxes = json_array ();
     303              : 
     304              :   /* check taxes is well-formed */
     305           40 :   if (! TALER_MERCHANT_taxes_array_valid (pd.taxes))
     306              :   {
     307            0 :     GNUNET_break_op (0);
     308            0 :     ret = TALER_MHD_reply_with_error (connection,
     309              :                                       MHD_HTTP_BAD_REQUEST,
     310              :                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
     311              :                                       "taxes");
     312            0 :     goto cleanup;
     313              :   }
     314              : 
     315           40 :   if (! TMH_location_object_valid (pd.address))
     316              :   {
     317            0 :     GNUNET_break_op (0);
     318            0 :     ret = TALER_MHD_reply_with_error (connection,
     319              :                                       MHD_HTTP_BAD_REQUEST,
     320              :                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
     321              :                                       "address");
     322            0 :     goto cleanup;
     323              :   }
     324              : 
     325           40 :   if (! TALER_JSON_check_i18n (pd.description_i18n))
     326              :   {
     327            0 :     GNUNET_break_op (0);
     328            0 :     ret = TALER_MHD_reply_with_error (connection,
     329              :                                       MHD_HTTP_BAD_REQUEST,
     330              :                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
     331              :                                       "description_i18n");
     332            0 :     goto cleanup;
     333              :   }
     334              : 
     335           40 :   if (NULL == pd.image)
     336            0 :     pd.image = (char *) "";
     337           40 :   if (! TALER_MERCHANT_image_data_url_valid (pd.image))
     338              :   {
     339            0 :     GNUNET_break_op (0);
     340            0 :     ret = TALER_MHD_reply_with_error (connection,
     341              :                                       MHD_HTTP_BAD_REQUEST,
     342              :                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
     343              :                                       "image");
     344            0 :     goto cleanup;
     345              :   }
     346              : 
     347           40 :   qs = TALER_MERCHANTDB_insert_product (TMH_db,
     348           40 :                                         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           40 :   switch (qs)
     358              :   {
     359            0 :   case GNUNET_DB_STATUS_HARD_ERROR:
     360              :   case GNUNET_DB_STATUS_SOFT_ERROR:
     361            0 :     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            0 :     goto cleanup;
     369            0 :   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     370            0 :     ret = TALER_MHD_reply_with_error (
     371              :       connection,
     372              :       MHD_HTTP_INTERNAL_SERVER_ERROR,
     373              :       TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
     374              :       NULL);
     375            0 :     goto cleanup;
     376           40 :   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     377           40 :     break;
     378              :   }
     379           40 :   if (no_group)
     380              :   {
     381            0 :     ret = TALER_MHD_reply_with_error (
     382              :       connection,
     383              :       MHD_HTTP_NOT_FOUND,
     384              :       TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN,
     385              :       NULL);
     386            0 :     goto cleanup;
     387              :   }
     388           40 :   if (no_pot)
     389              :   {
     390            0 :     ret = TALER_MHD_reply_with_error (
     391              :       connection,
     392              :       MHD_HTTP_NOT_FOUND,
     393              :       TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN,
     394              :       NULL);
     395            0 :     goto cleanup;
     396              :   }
     397           40 :   if (conflict)
     398              :   {
     399            2 :     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            2 :     goto cleanup;
     405              :   }
     406           38 :   if (-1 != no_cat)
     407              :   {
     408              :     char nocats[24];
     409              : 
     410            2 :     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            2 :     GNUNET_assert (no_cat > 0);
     414            2 :     GNUNET_assert (no_cat <= (ssize_t) num_cats);
     415            2 :     GNUNET_snprintf (nocats,
     416              :                      sizeof (nocats),
     417              :                      "%llu",
     418            2 :                      (unsigned long long) cats[no_cat - 1]);
     419            2 :     ret = TALER_MHD_reply_with_error (
     420              :       connection,
     421              :       MHD_HTTP_NOT_FOUND,
     422              :       TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN,
     423              :       nocats);
     424            2 :     goto cleanup;
     425              :   }
     426           36 :   ret = TALER_MHD_reply_static (connection,
     427              :                                 MHD_HTTP_NO_CONTENT,
     428              :                                 NULL,
     429              :                                 NULL,
     430              :                                 0);
     431           44 : cleanup:
     432           44 :   GNUNET_JSON_parse_free (spec);
     433           44 :   GNUNET_free (pd.price_array);
     434           44 :   GNUNET_free (cats);
     435           44 :   return ret;
     436              : }
     437              : 
     438              : 
     439              : /* end of taler-merchant-httpd_post-private-products.c */
        

Generated by: LCOV version 2.0-1