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: 55.5 % 155 86
Test Date: 2026-01-01 16:44:56 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              :   struct TALER_Amount price;
      54              :   bool price_missing;
      55              :   bool unit_price_missing;
      56              :   bool unit_allow_fraction;
      57              :   bool unit_allow_fraction_missing;
      58              :   uint32_t unit_precision_level;
      59              :   bool unit_precision_missing;
      60              :   enum GNUNET_DB_QueryStatus qs;
      61              :   struct GNUNET_JSON_Specification spec[] = {
      62              :     /* new in protocol v20, thus optional for backwards-compatibility */
      63           14 :     GNUNET_JSON_spec_mark_optional (
      64              :       GNUNET_JSON_spec_string ("product_name",
      65              :                                (const char **) &pd.product_name),
      66              :       NULL),
      67           14 :     GNUNET_JSON_spec_string ("description",
      68              :                              (const char **) &pd.description),
      69           14 :     GNUNET_JSON_spec_mark_optional (
      70              :       GNUNET_JSON_spec_json ("description_i18n",
      71              :                              &pd.description_i18n),
      72              :       NULL),
      73           14 :     GNUNET_JSON_spec_string ("unit",
      74              :                              (const char **) &pd.unit),
      75              :     // FIXME: deprecated API
      76           14 :     GNUNET_JSON_spec_mark_optional (
      77              :       TALER_JSON_spec_amount_any ("price",
      78              :                                   &price),
      79              :       &price_missing),
      80           14 :     GNUNET_JSON_spec_mark_optional (
      81              :       TALER_JSON_spec_amount_any_array ("unit_price",
      82              :                                         &pd.price_array_length,
      83              :                                         &pd.price_array),
      84              :       &unit_price_missing),
      85           14 :     GNUNET_JSON_spec_mark_optional (
      86              :       GNUNET_JSON_spec_string ("image",
      87              :                                (const char **) &pd.image),
      88              :       NULL),
      89           14 :     GNUNET_JSON_spec_mark_optional (
      90              :       GNUNET_JSON_spec_json ("taxes",
      91              :                              &pd.taxes),
      92              :       NULL),
      93           14 :     GNUNET_JSON_spec_mark_optional (
      94              :       GNUNET_JSON_spec_array_const ("categories",
      95              :                                     &categories),
      96              :       NULL),
      97           14 :     GNUNET_JSON_spec_mark_optional (
      98              :       GNUNET_JSON_spec_string ("unit_total_stock",
      99              :                                &unit_total_stock),
     100              :       &unit_total_stock_missing),
     101           14 :     GNUNET_JSON_spec_mark_optional (
     102              :       GNUNET_JSON_spec_int64 ("total_stock",
     103              :                               &total_stock),
     104              :       &total_stock_missing),
     105           14 :     GNUNET_JSON_spec_mark_optional (
     106              :       GNUNET_JSON_spec_bool ("unit_allow_fraction",
     107              :                              &unit_allow_fraction),
     108              :       &unit_allow_fraction_missing),
     109           14 :     GNUNET_JSON_spec_mark_optional (
     110              :       GNUNET_JSON_spec_uint32 ("unit_precision_level",
     111              :                                &unit_precision_level),
     112              :       &unit_precision_missing),
     113           14 :     GNUNET_JSON_spec_mark_optional (
     114              :       GNUNET_JSON_spec_uint64 ("total_lost",
     115              :                                &pd.total_lost),
     116              :       NULL),
     117           14 :     GNUNET_JSON_spec_mark_optional (
     118              :       GNUNET_JSON_spec_json ("address",
     119              :                              &pd.address),
     120              :       NULL),
     121           14 :     GNUNET_JSON_spec_mark_optional (
     122              :       GNUNET_JSON_spec_timestamp ("next_restock",
     123              :                                   &pd.next_restock),
     124              :       NULL),
     125           14 :     GNUNET_JSON_spec_mark_optional (
     126              :       GNUNET_JSON_spec_uint32 ("minimum_age",
     127              :                                &pd.minimum_age),
     128              :       NULL),
     129           14 :     GNUNET_JSON_spec_end ()
     130              :   };
     131              :   MHD_RESULT ret;
     132           14 :   size_t num_cats = 0;
     133           14 :   uint64_t *cats = NULL;
     134              :   bool no_instance;
     135              :   ssize_t no_cat;
     136              :   bool no_product;
     137              :   bool lost_reduced;
     138              :   bool sold_reduced;
     139              :   bool stock_reduced;
     140              :   bool no_group;
     141              :   bool no_pot;
     142              : 
     143           14 :   pd.total_sold = 0; /* will be ignored anyway */
     144           14 :   GNUNET_assert (NULL != mi);
     145           14 :   GNUNET_assert (NULL != product_id);
     146              :   {
     147              :     enum GNUNET_GenericReturnValue res;
     148              : 
     149           14 :     res = TALER_MHD_parse_json_data (connection,
     150           14 :                                      hc->request_body,
     151              :                                      spec);
     152           14 :     if (GNUNET_OK != res)
     153              :       return (GNUNET_NO == res)
     154              :              ? MHD_YES
     155            0 :              : MHD_NO;
     156              :     /* For pre-v20 clients, we use the description given as the
     157              :        product name; remove once we make product_name mandatory. */
     158           14 :     if (NULL == pd.product_name)
     159            0 :       pd.product_name = pd.description;
     160              :   }
     161           14 :   if (! unit_price_missing)
     162              :   {
     163           14 :     if (! price_missing)
     164              :     {
     165            0 :       if (0 != TALER_amount_cmp (&price,
     166            0 :                                  &pd.price_array[0]))
     167              :       {
     168            0 :         ret = TALER_MHD_reply_with_error (connection,
     169              :                                           MHD_HTTP_BAD_REQUEST,
     170              :                                           TALER_EC_GENERIC_PARAMETER_MALFORMED,
     171              :                                           "price,unit_price mismatch");
     172            0 :         goto cleanup;
     173              :       }
     174              :     }
     175              :   }
     176              :   else
     177              :   {
     178            0 :     if (price_missing)
     179              :     {
     180            0 :       ret = TALER_MHD_reply_with_error (connection,
     181              :                                         MHD_HTTP_BAD_REQUEST,
     182              :                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     183              :                                         "price missing");
     184            0 :       goto cleanup;
     185              :     }
     186            0 :     pd.price_array = GNUNET_new_array (1,
     187              :                                        struct TALER_Amount);
     188            0 :     pd.price_array[0] = price;
     189            0 :     pd.price_array_length = 1;
     190              :   }
     191           14 :   if (! unit_precision_missing)
     192              :   {
     193            4 :     if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL)
     194              :     {
     195            0 :       ret = TALER_MHD_reply_with_error (connection,
     196              :                                         MHD_HTTP_BAD_REQUEST,
     197              :                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     198              :                                         "unit_precision_level");
     199            0 :       goto cleanup;
     200              :     }
     201              :   }
     202              :   {
     203              :     bool default_allow_fractional;
     204              :     uint32_t default_precision_level;
     205              : 
     206           14 :     if (GNUNET_OK !=
     207           14 :         TMH_unit_defaults_for_instance (mi,
     208           14 :                                         pd.unit,
     209              :                                         &default_allow_fractional,
     210              :                                         &default_precision_level))
     211              :     {
     212            0 :       GNUNET_break (0);
     213            0 :       ret = TALER_MHD_reply_with_error (connection,
     214              :                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
     215              :                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
     216              :                                         "unit defaults");
     217            0 :       goto cleanup;
     218              :     }
     219           14 :     if (unit_allow_fraction_missing)
     220           10 :       unit_allow_fraction = default_allow_fractional;
     221           14 :     if (unit_precision_missing)
     222           10 :       unit_precision_level = default_precision_level;
     223              : 
     224           14 :     if (! unit_allow_fraction)
     225           10 :       unit_precision_level = 0;
     226           14 :     pd.fractional_precision_level = unit_precision_level;
     227              :   }
     228              :   {
     229              :     const char *eparam;
     230           14 :     if (GNUNET_OK !=
     231           14 :         TALER_MERCHANT_vk_process_quantity_inputs (
     232              :           TALER_MERCHANT_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 (! TALER_MERCHANT_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 (! TALER_MERCHANT_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              :                                &no_group,
     348              :                                &no_pot);
     349           14 :   switch (qs)
     350              :   {
     351            0 :   case GNUNET_DB_STATUS_HARD_ERROR:
     352            0 :     GNUNET_break (0);
     353            0 :     ret = TALER_MHD_reply_with_error (connection,
     354              :                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
     355              :                                       TALER_EC_GENERIC_DB_STORE_FAILED,
     356              :                                       NULL);
     357            0 :     goto cleanup;
     358            0 :   case GNUNET_DB_STATUS_SOFT_ERROR:
     359            0 :     GNUNET_break (0);
     360            0 :     ret = TALER_MHD_reply_with_error (connection,
     361              :                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
     362              :                                       TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
     363              :                                       "unexpected serialization problem");
     364            0 :     goto cleanup;
     365            0 :   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     366            0 :     GNUNET_break (0);
     367            0 :     ret = TALER_MHD_reply_with_error (connection,
     368              :                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
     369              :                                       TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
     370              :                                       "unexpected problem in stored procedure");
     371            0 :     goto cleanup;
     372           14 :   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     373           14 :     break;
     374              :   }
     375              : 
     376           14 :   if (no_instance)
     377              :   {
     378            0 :     ret = TALER_MHD_reply_with_error (connection,
     379              :                                       MHD_HTTP_NOT_FOUND,
     380              :                                       TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
     381            0 :                                       mi->settings.id);
     382            0 :     goto cleanup;
     383              :   }
     384           14 :   if (-1 != no_cat)
     385              :   {
     386              :     char cat_str[24];
     387              : 
     388            0 :     GNUNET_snprintf (cat_str,
     389              :                      sizeof (cat_str),
     390              :                      "%llu",
     391              :                      (unsigned long long) no_cat);
     392            0 :     ret = TALER_MHD_reply_with_error (connection,
     393              :                                       MHD_HTTP_NOT_FOUND,
     394              :                                       TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN,
     395              :                                       cat_str);
     396            0 :     goto cleanup;
     397              :   }
     398           14 :   if (no_product)
     399              :   {
     400            2 :     ret = TALER_MHD_reply_with_error (connection,
     401              :                                       MHD_HTTP_NOT_FOUND,
     402              :                                       TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
     403              :                                       product_id);
     404            2 :     goto cleanup;
     405              :   }
     406           12 :   if (no_group)
     407              :   {
     408            0 :     ret = TALER_MHD_reply_with_error (
     409              :       connection,
     410              :       MHD_HTTP_NOT_FOUND,
     411              :       TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN,
     412              :       NULL);
     413            0 :     goto cleanup;
     414              :   }
     415           12 :   if (no_pot)
     416              :   {
     417            0 :     ret = TALER_MHD_reply_with_error (
     418              :       connection,
     419              :       MHD_HTTP_NOT_FOUND,
     420              :       TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN,
     421              :       NULL);
     422            0 :     goto cleanup;
     423              :   }
     424           12 :   if (lost_reduced)
     425              :   {
     426            0 :     ret = TALER_MHD_reply_with_error (
     427              :       connection,
     428              :       MHD_HTTP_CONFLICT,
     429              :       TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED,
     430              :       NULL);
     431            0 :     goto cleanup;
     432              :   }
     433           12 :   if (sold_reduced)
     434              :   {
     435            0 :     ret = TALER_MHD_reply_with_error (
     436              :       connection,
     437              :       MHD_HTTP_CONFLICT,
     438              :       TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED,
     439              :       NULL);
     440            0 :     goto cleanup;
     441              :   }
     442           12 :   if (stock_reduced)
     443              :   {
     444            0 :     ret = TALER_MHD_reply_with_error (
     445              :       connection,
     446              :       MHD_HTTP_CONFLICT,
     447              :       TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED,
     448              :       NULL);
     449            0 :     goto cleanup;
     450              :   }
     451              :   /* success! */
     452           12 :   ret = TALER_MHD_reply_static (connection,
     453              :                                 MHD_HTTP_NO_CONTENT,
     454              :                                 NULL,
     455              :                                 NULL,
     456              :                                 0);
     457           14 : cleanup:
     458           14 :   GNUNET_free (cats);
     459           14 :   GNUNET_free (pd.price_array);
     460           14 :   GNUNET_JSON_parse_free (spec);
     461           14 :   return ret;
     462              : }
     463              : 
     464              : 
     465              : /* end of taler-merchant-httpd_private-patch-products-ID.c */
        

Generated by: LCOV version 2.0-1