merchant

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

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


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