merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

taler-merchant-httpd_patch-private-products-PRODUCT_ID.c (15974B)


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