LCOV - code coverage report
Current view: top level - backend - taler-merchant-httpd_patch-private-products-PRODUCT_ID.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 63.3 % 166 105
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--2026 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_patch-private-products-PRODUCT_ID.c
      22              :  * @brief implementing PATCH /products/$ID request handling
      23              :  * @author Christian Grothoff
      24              :  */
      25              : #include "platform.h"
      26              : #include "taler-merchant-httpd_patch-private-products-PRODUCT_ID.h"
      27              : #include "taler-merchant-httpd_helper.h"
      28              : #include <taler/taler_json_lib.h>
      29              : #include "merchant-database/update_product.h"
      30              : 
      31              : 
      32              : /**
      33              :  * PATCH configuration of an existing instance, given its configuration.
      34              :  *
      35              :  * @param rh context of the handler
      36              :  * @param connection the MHD connection to handle
      37              :  * @param[in,out] hc context with further information about the request
      38              :  * @return MHD result code
      39              :  */
      40              : enum MHD_Result
      41           26 : TMH_private_patch_products_ID (
      42              :   const struct TMH_RequestHandler *rh,
      43              :   struct MHD_Connection *connection,
      44              :   struct TMH_HandlerContext *hc)
      45              : {
      46           26 :   struct TMH_MerchantInstance *mi = hc->instance;
      47           26 :   const char *product_id = hc->infix;
      48           26 :   struct TALER_MERCHANTDB_ProductDetails pd = {0};
      49           26 :   const json_t *categories = NULL;
      50              :   int64_t total_stock;
      51           26 :   const char *unit_total_stock = NULL;
      52              :   bool unit_total_stock_missing;
      53              :   bool total_stock_missing;
      54              :   struct TALER_Amount price;
      55              :   bool price_missing;
      56              :   bool unit_price_missing;
      57              :   bool unit_allow_fraction;
      58              :   bool unit_allow_fraction_missing;
      59              :   uint32_t unit_precision_level;
      60              :   bool unit_precision_missing;
      61              :   enum GNUNET_DB_QueryStatus qs;
      62              :   struct GNUNET_JSON_Specification spec[] = {
      63              :     /* new in protocol v20, thus optional for backwards-compatibility */
      64           26 :     GNUNET_JSON_spec_mark_optional (
      65              :       GNUNET_JSON_spec_string ("product_name",
      66              :                                (const char **) &pd.product_name),
      67              :       NULL),
      68           26 :     GNUNET_JSON_spec_string ("description",
      69              :                              (const char **) &pd.description),
      70           26 :     GNUNET_JSON_spec_mark_optional (
      71              :       GNUNET_JSON_spec_json ("description_i18n",
      72              :                              &pd.description_i18n),
      73              :       NULL),
      74           26 :     TALER_JSON_spec_slug ("unit",
      75              :                           (const char **) &pd.unit),
      76              :     // FIXME: deprecated API
      77           26 :     GNUNET_JSON_spec_mark_optional (
      78              :       TALER_JSON_spec_amount_any ("price",
      79              :                                   &price),
      80              :       &price_missing),
      81           26 :     GNUNET_JSON_spec_mark_optional (
      82              :       TALER_JSON_spec_amount_any_array ("unit_price",
      83              :                                         &pd.price_array_length,
      84              :                                         &pd.price_array),
      85              :       &unit_price_missing),
      86           26 :     GNUNET_JSON_spec_mark_optional (
      87              :       GNUNET_JSON_spec_string ("image",
      88              :                                (const char **) &pd.image),
      89              :       NULL),
      90           26 :     GNUNET_JSON_spec_mark_optional (
      91              :       GNUNET_JSON_spec_json ("taxes",
      92              :                              &pd.taxes),
      93              :       NULL),
      94           26 :     GNUNET_JSON_spec_mark_optional (
      95              :       GNUNET_JSON_spec_array_const ("categories",
      96              :                                     &categories),
      97              :       NULL),
      98           26 :     GNUNET_JSON_spec_mark_optional (
      99              :       GNUNET_JSON_spec_string ("unit_total_stock",
     100              :                                &unit_total_stock),
     101              :       &unit_total_stock_missing),
     102           26 :     GNUNET_JSON_spec_mark_optional (
     103              :       GNUNET_JSON_spec_int64 ("total_stock",
     104              :                               &total_stock),
     105              :       &total_stock_missing),
     106           26 :     GNUNET_JSON_spec_mark_optional (
     107              :       GNUNET_JSON_spec_bool ("unit_allow_fraction",
     108              :                              &unit_allow_fraction),
     109              :       &unit_allow_fraction_missing),
     110           26 :     GNUNET_JSON_spec_mark_optional (
     111              :       GNUNET_JSON_spec_uint32 ("unit_precision_level",
     112              :                                &unit_precision_level),
     113              :       &unit_precision_missing),
     114           26 :     GNUNET_JSON_spec_mark_optional (
     115              :       GNUNET_JSON_spec_uint64 ("total_lost",
     116              :                                &pd.total_lost),
     117              :       NULL),
     118           26 :     GNUNET_JSON_spec_mark_optional (
     119              :       GNUNET_JSON_spec_uint64 ("product_group_id",
     120              :                                &pd.product_group_id),
     121              :       NULL),
     122           26 :     GNUNET_JSON_spec_mark_optional (
     123              :       GNUNET_JSON_spec_uint64 ("money_pot_id",
     124              :                                &pd.money_pot_id),
     125              :       NULL),
     126           26 :     GNUNET_JSON_spec_mark_optional (
     127              :       GNUNET_JSON_spec_json ("address",
     128              :                              &pd.address),
     129              :       NULL),
     130           26 :     GNUNET_JSON_spec_mark_optional (
     131              :       GNUNET_JSON_spec_timestamp ("next_restock",
     132              :                                   &pd.next_restock),
     133              :       NULL),
     134           26 :     GNUNET_JSON_spec_mark_optional (
     135              :       GNUNET_JSON_spec_uint32 ("minimum_age",
     136              :                                &pd.minimum_age),
     137              :       NULL),
     138           26 :     GNUNET_JSON_spec_mark_optional (
     139              :       GNUNET_JSON_spec_bool ("price_is_net",
     140              :                              &pd.price_is_net),
     141              :       NULL),
     142           26 :     GNUNET_JSON_spec_end ()
     143              :   };
     144              :   enum MHD_Result ret;
     145           26 :   size_t num_cats = 0;
     146           26 :   uint64_t *cats = NULL;
     147              :   ssize_t no_cat;
     148              :   bool no_product;
     149              :   bool lost_reduced;
     150              :   bool sold_reduced;
     151              :   bool stock_reduced;
     152              :   bool no_group;
     153              :   bool no_pot;
     154              : 
     155           26 :   GNUNET_assert (NULL != mi);
     156           26 :   GNUNET_assert (NULL != product_id);
     157              :   {
     158              :     enum GNUNET_GenericReturnValue res;
     159              : 
     160           26 :     res = TALER_MHD_parse_json_data (connection,
     161           26 :                                      hc->request_body,
     162              :                                      spec);
     163           26 :     if (GNUNET_OK != res)
     164              :       return (GNUNET_NO == res)
     165              :              ? MHD_YES
     166            0 :              : MHD_NO;
     167              :     /* For pre-v20 clients, we use the description given as the
     168              :        product name; remove once we make product_name mandatory. */
     169           26 :     if (NULL == pd.product_name)
     170            0 :       pd.product_name = pd.description;
     171              :   }
     172           26 :   if ( (! unit_price_missing) &&
     173           26 :        (0 == pd.price_array_length) )
     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              :                                       "unit_price");
     180            0 :     goto cleanup;
     181              :   }
     182           26 :   if (! unit_price_missing)
     183              :   {
     184           26 :     if (! price_missing)
     185              :     {
     186            0 :       if (0 != TALER_amount_cmp (&price,
     187            0 :                                  &pd.price_array[0]))
     188              :       {
     189            0 :         ret = TALER_MHD_reply_with_error (connection,
     190              :                                           MHD_HTTP_BAD_REQUEST,
     191              :                                           TALER_EC_GENERIC_PARAMETER_MALFORMED,
     192              :                                           "price,unit_price mismatch");
     193            0 :         goto cleanup;
     194              :       }
     195              :     }
     196           26 :     if (GNUNET_OK !=
     197           26 :         TMH_validate_unit_price_array (pd.price_array,
     198              :                                        pd.price_array_length))
     199              :     {
     200            2 :       ret = TALER_MHD_reply_with_error (connection,
     201              :                                         MHD_HTTP_BAD_REQUEST,
     202              :                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     203              :                                         "unit_price");
     204            2 :       goto cleanup;
     205              :     }
     206              :   }
     207              :   else
     208              :   {
     209            0 :     if (price_missing)
     210              :     {
     211            0 :       ret = TALER_MHD_reply_with_error (connection,
     212              :                                         MHD_HTTP_BAD_REQUEST,
     213              :                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     214              :                                         "price missing");
     215            0 :       goto cleanup;
     216              :     }
     217            0 :     pd.price_array = GNUNET_new_array (1,
     218              :                                        struct TALER_Amount);
     219            0 :     pd.price_array[0] = price;
     220            0 :     pd.price_array_length = 1;
     221              :   }
     222           24 :   if (! unit_precision_missing)
     223              :   {
     224            4 :     if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL)
     225              :     {
     226            0 :       ret = TALER_MHD_reply_with_error (connection,
     227              :                                         MHD_HTTP_BAD_REQUEST,
     228              :                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     229              :                                         "unit_precision_level");
     230            0 :       goto cleanup;
     231              :     }
     232              :   }
     233              :   {
     234              :     bool default_allow_fractional;
     235              :     uint32_t default_precision_level;
     236              : 
     237           24 :     if (GNUNET_OK !=
     238           24 :         TMH_unit_defaults_for_instance (mi,
     239           24 :                                         pd.unit,
     240              :                                         &default_allow_fractional,
     241              :                                         &default_precision_level))
     242              :     {
     243            0 :       GNUNET_break (0);
     244            0 :       ret = TALER_MHD_reply_with_error (connection,
     245              :                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
     246              :                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
     247              :                                         "unit defaults");
     248            0 :       goto cleanup;
     249              :     }
     250           24 :     if (unit_allow_fraction_missing)
     251           20 :       unit_allow_fraction = default_allow_fractional;
     252           24 :     if (unit_precision_missing)
     253           20 :       unit_precision_level = default_precision_level;
     254              : 
     255           24 :     if (! unit_allow_fraction)
     256           20 :       unit_precision_level = 0;
     257           24 :     pd.fractional_precision_level = unit_precision_level;
     258              :   }
     259              :   {
     260              :     const char *eparam;
     261           24 :     if (GNUNET_OK !=
     262           24 :         TALER_MERCHANT_vk_process_quantity_inputs (
     263              :           TALER_MERCHANT_VK_STOCK,
     264              :           unit_allow_fraction,
     265              :           total_stock_missing,
     266              :           total_stock,
     267              :           unit_total_stock_missing,
     268              :           unit_total_stock,
     269              :           &pd.total_stock,
     270              :           &pd.total_stock_frac,
     271              :           &eparam))
     272              :     {
     273            0 :       ret = TALER_MHD_reply_with_error (
     274              :         connection,
     275              :         MHD_HTTP_BAD_REQUEST,
     276              :         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     277              :         eparam);
     278            0 :       goto cleanup;
     279              :     }
     280           24 :     pd.allow_fractional_quantity = unit_allow_fraction;
     281              :   }
     282           24 :   if (NULL == pd.address)
     283            2 :     pd.address = json_object ();
     284              : 
     285           24 :   if (! TMH_location_object_valid (pd.address))
     286              :   {
     287            0 :     GNUNET_break_op (0);
     288            0 :     ret = TALER_MHD_reply_with_error (connection,
     289              :                                       MHD_HTTP_BAD_REQUEST,
     290              :                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
     291              :                                       "address");
     292            0 :     goto cleanup;
     293              :   }
     294           24 :   num_cats = json_array_size (categories);
     295           24 :   cats = GNUNET_new_array (num_cats,
     296              :                            uint64_t);
     297              :   {
     298              :     size_t idx;
     299              :     json_t *val;
     300              : 
     301           32 :     json_array_foreach (categories, idx, val)
     302              :     {
     303           20 :       if ( (! json_is_integer (val)) ||
     304           10 :            (0 >= json_integer_value (val)) )
     305              :       {
     306            2 :         GNUNET_break_op (0);
     307            2 :         ret = TALER_MHD_reply_with_error (connection,
     308              :                                           MHD_HTTP_BAD_REQUEST,
     309              :                                           TALER_EC_GENERIC_PARAMETER_MALFORMED,
     310              :                                           "categories");
     311            2 :         goto cleanup;
     312              :       }
     313            8 :       cats[idx] = json_integer_value (val);
     314              :     }
     315              :   }
     316              : 
     317           22 :   if (NULL == pd.description_i18n)
     318            2 :     pd.description_i18n = json_object ();
     319              : 
     320           22 :   if (! TALER_JSON_check_i18n (pd.description_i18n))
     321              :   {
     322            0 :     GNUNET_break_op (0);
     323            0 :     ret = TALER_MHD_reply_with_error (connection,
     324              :                                       MHD_HTTP_BAD_REQUEST,
     325              :                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
     326              :                                       "description_i18n");
     327            0 :     goto cleanup;
     328              :   }
     329              : 
     330           22 :   if (NULL == pd.taxes)
     331            0 :     pd.taxes = json_array ();
     332              :   /* check taxes is well-formed */
     333           22 :   if (! TALER_MERCHANT_taxes_array_valid (pd.taxes))
     334              :   {
     335            0 :     GNUNET_break_op (0);
     336            0 :     ret = TALER_MHD_reply_with_error (connection,
     337              :                                       MHD_HTTP_BAD_REQUEST,
     338              :                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
     339              :                                       "taxes");
     340            0 :     goto cleanup;
     341              :   }
     342              : 
     343           22 :   if (NULL == pd.image)
     344            0 :     pd.image = (char *) "";
     345           22 :   if (! TALER_MERCHANT_image_data_url_valid (pd.image))
     346              :   {
     347            0 :     GNUNET_break_op (0);
     348            0 :     ret = TALER_MHD_reply_with_error (connection,
     349              :                                       MHD_HTTP_BAD_REQUEST,
     350              :                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
     351              :                                       "image");
     352            0 :     goto cleanup;
     353              :   }
     354              : 
     355           22 :   if ( (pd.total_stock < pd.total_sold + pd.total_lost) ||
     356           22 :        (pd.total_sold + pd.total_lost < pd.total_sold) /* integer overflow */)
     357              :   {
     358            0 :     GNUNET_break_op (0);
     359            0 :     ret = TALER_MHD_reply_with_error (
     360              :       connection,
     361              :       MHD_HTTP_BAD_REQUEST,
     362              :       TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS,
     363              :       NULL);
     364            0 :     goto cleanup;
     365              :   }
     366              : 
     367           22 :   qs = TALER_MERCHANTDB_update_product (TMH_db,
     368           22 :                                         mi->settings.id,
     369              :                                         product_id,
     370              :                                         &pd,
     371              :                                         num_cats,
     372              :                                         cats,
     373              :                                         &no_cat,
     374              :                                         &no_product,
     375              :                                         &lost_reduced,
     376              :                                         &sold_reduced,
     377              :                                         &stock_reduced,
     378              :                                         &no_group,
     379              :                                         &no_pot);
     380           22 :   switch (qs)
     381              :   {
     382            0 :   case GNUNET_DB_STATUS_HARD_ERROR:
     383            0 :     GNUNET_break (0);
     384            0 :     ret = TALER_MHD_reply_with_error (connection,
     385              :                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
     386              :                                       TALER_EC_GENERIC_DB_STORE_FAILED,
     387              :                                       NULL);
     388            0 :     goto cleanup;
     389            0 :   case GNUNET_DB_STATUS_SOFT_ERROR:
     390            0 :     GNUNET_break (0);
     391            0 :     ret = TALER_MHD_reply_with_error (connection,
     392              :                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
     393              :                                       TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
     394              :                                       "unexpected serialization problem");
     395            0 :     goto cleanup;
     396            0 :   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     397            0 :     GNUNET_break (0);
     398            0 :     ret = TALER_MHD_reply_with_error (connection,
     399              :                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
     400              :                                       TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
     401              :                                       "unexpected problem in stored procedure");
     402            0 :     goto cleanup;
     403           22 :   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     404           22 :     break;
     405              :   }
     406              : 
     407           22 :   if (-1 != no_cat)
     408              :   {
     409              :     char cat_str[24];
     410              : 
     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 (cat_str,
     416              :                      sizeof (cat_str),
     417              :                      "%llu",
     418            2 :                      (unsigned long long) cats[no_cat - 1]);
     419            2 :     ret = TALER_MHD_reply_with_error (connection,
     420              :                                       MHD_HTTP_NOT_FOUND,
     421              :                                       TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN,
     422              :                                       cat_str);
     423            2 :     goto cleanup;
     424              :   }
     425           20 :   if (no_product)
     426              :   {
     427            2 :     ret = TALER_MHD_reply_with_error (connection,
     428              :                                       MHD_HTTP_NOT_FOUND,
     429              :                                       TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
     430              :                                       product_id);
     431            2 :     goto cleanup;
     432              :   }
     433           18 :   if (no_group)
     434              :   {
     435            0 :     ret = TALER_MHD_reply_with_error (
     436              :       connection,
     437              :       MHD_HTTP_NOT_FOUND,
     438              :       TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN,
     439              :       NULL);
     440            0 :     goto cleanup;
     441              :   }
     442           18 :   if (no_pot)
     443              :   {
     444            0 :     ret = TALER_MHD_reply_with_error (
     445              :       connection,
     446              :       MHD_HTTP_NOT_FOUND,
     447              :       TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN,
     448              :       NULL);
     449            0 :     goto cleanup;
     450              :   }
     451           18 :   if (lost_reduced)
     452              :   {
     453            0 :     ret = TALER_MHD_reply_with_error (
     454              :       connection,
     455              :       MHD_HTTP_CONFLICT,
     456              :       TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED,
     457              :       NULL);
     458            0 :     goto cleanup;
     459              :   }
     460           18 :   if (sold_reduced)
     461              :   {
     462            0 :     ret = TALER_MHD_reply_with_error (
     463              :       connection,
     464              :       MHD_HTTP_CONFLICT,
     465              :       TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED,
     466              :       NULL);
     467            0 :     goto cleanup;
     468              :   }
     469           18 :   if (stock_reduced)
     470              :   {
     471            0 :     ret = TALER_MHD_reply_with_error (
     472              :       connection,
     473              :       MHD_HTTP_CONFLICT,
     474              :       TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED,
     475              :       NULL);
     476            0 :     goto cleanup;
     477              :   }
     478              :   /* success! */
     479           18 :   ret = TALER_MHD_reply_static (connection,
     480              :                                 MHD_HTTP_NO_CONTENT,
     481              :                                 NULL,
     482              :                                 NULL,
     483              :                                 0);
     484           26 : cleanup:
     485           26 :   GNUNET_free (cats);
     486           26 :   GNUNET_free (pd.price_array);
     487           26 :   GNUNET_JSON_parse_free (spec);
     488           26 :   return ret;
     489              : }
     490              : 
     491              : 
     492              : /* end of taler-merchant-httpd_patch-private-products-PRODUCT_ID.c */
        

Generated by: LCOV version 2.0-1