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

Generated by: LCOV version 2.0-1