diff options
Diffstat (limited to 'src/backend')
22 files changed, 1552 insertions, 417 deletions
diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am index dbc7cde8..cce7faec 100644 --- a/src/backend/Makefile.am +++ b/src/backend/Makefile.am @@ -37,6 +37,8 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_mhd.h \ taler-merchant-httpd_private-delete-account-ID.c \ taler-merchant-httpd_private-delete-account-ID.h \ + taler-merchant-httpd_private-delete-categories-ID.c \ + taler-merchant-httpd_private-delete-categories-ID.h \ taler-merchant-httpd_private-delete-instances-ID.c \ taler-merchant-httpd_private-delete-instances-ID.h \ taler-merchant-httpd_private-delete-instances-ID-token.c \ @@ -59,12 +61,18 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_private-get-accounts.h \ taler-merchant-httpd_private-get-accounts-ID.c \ taler-merchant-httpd_private-get-accounts-ID.h \ + taler-merchant-httpd_private-get-categories.c \ + taler-merchant-httpd_private-get-categories.h \ + taler-merchant-httpd_private-get-categories-ID.c \ + taler-merchant-httpd_private-get-categories-ID.h \ taler-merchant-httpd_private-get-instances.c \ taler-merchant-httpd_private-get-instances.h \ taler-merchant-httpd_private-get-instances-ID.c \ taler-merchant-httpd_private-get-instances-ID.h \ taler-merchant-httpd_private-get-instances-ID-kyc.c \ taler-merchant-httpd_private-get-instances-ID-kyc.h \ + taler-merchant-httpd_private-get-pos.c \ + taler-merchant-httpd_private-get-pos.h \ taler-merchant-httpd_private-get-products.c \ taler-merchant-httpd_private-get-products.h \ taler-merchant-httpd_private-get-products-ID.c \ @@ -93,6 +101,8 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_private-get-webhooks-ID.h \ taler-merchant-httpd_private-patch-accounts-ID.c \ taler-merchant-httpd_private-patch-accounts-ID.h \ + taler-merchant-httpd_private-patch-categories-ID.c \ + taler-merchant-httpd_private-patch-categories-ID.h \ taler-merchant-httpd_private-patch-instances-ID.c \ taler-merchant-httpd_private-patch-instances-ID.h \ taler-merchant-httpd_private-patch-orders-ID-forget.c \ @@ -109,6 +119,8 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_private-patch-webhooks-ID.h \ taler-merchant-httpd_private-post-account.c \ taler-merchant-httpd_private-post-account.h \ + taler-merchant-httpd_private-post-categories.c \ + taler-merchant-httpd_private-post-categories.h \ taler-merchant-httpd_private-post-instances.c \ taler-merchant-httpd_private-post-instances.h \ taler-merchant-httpd_private-post-instances-ID-auth.c \ diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c index 7384bfc9..e9ee1cbf 100644 --- a/src/backend/taler-merchant-httpd.c +++ b/src/backend/taler-merchant-httpd.c @@ -34,6 +34,7 @@ #include "taler-merchant-httpd_get-templates-ID.h" #include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_private-delete-account-ID.h" +#include "taler-merchant-httpd_private-delete-categories-ID.h" #include "taler-merchant-httpd_private-delete-instances-ID.h" #include "taler-merchant-httpd_private-delete-instances-ID-token.h" #include "taler-merchant-httpd_private-delete-products-ID.h" @@ -45,9 +46,12 @@ #include "taler-merchant-httpd_private-delete-webhooks-ID.h" #include "taler-merchant-httpd_private-get-accounts.h" #include "taler-merchant-httpd_private-get-accounts-ID.h" +#include "taler-merchant-httpd_private-get-categories.h" +#include "taler-merchant-httpd_private-get-categories-ID.h" #include "taler-merchant-httpd_private-get-instances.h" #include "taler-merchant-httpd_private-get-instances-ID.h" #include "taler-merchant-httpd_private-get-instances-ID-kyc.h" +#include "taler-merchant-httpd_private-get-pos.h" #include "taler-merchant-httpd_private-get-products.h" #include "taler-merchant-httpd_private-get-products-ID.h" #include "taler-merchant-httpd_private-get-orders.h" @@ -62,6 +66,7 @@ #include "taler-merchant-httpd_private-get-webhooks.h" #include "taler-merchant-httpd_private-get-webhooks-ID.h" #include "taler-merchant-httpd_private-patch-accounts-ID.h" +#include "taler-merchant-httpd_private-patch-categories-ID.h" #include "taler-merchant-httpd_private-patch-instances-ID.h" #include "taler-merchant-httpd_private-patch-orders-ID-forget.h" #include "taler-merchant-httpd_private-patch-otp-devices-ID.h" @@ -70,6 +75,7 @@ #include "taler-merchant-httpd_private-patch-token-families-SLUG.h" #include "taler-merchant-httpd_private-patch-webhooks-ID.h" #include "taler-merchant-httpd_private-post-account.h" +#include "taler-merchant-httpd_private-post-categories.h" #include "taler-merchant-httpd_private-post-instances.h" #include "taler-merchant-httpd_private-post-instances-ID-auth.h" #include "taler-merchant-httpd_private-post-instances-ID-token.h" @@ -932,6 +938,52 @@ url_handler (void *cls, .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_instances_ID_kyc, }, + /* GET /pos: */ + { + .url_prefix = "/pos", + .method = MHD_HTTP_METHOD_GET, + .handler = &TMH_private_get_pos + }, + /* GET /categories: */ + { + .url_prefix = "/categories", + .method = MHD_HTTP_METHOD_GET, + .handler = &TMH_private_get_categories + }, + /* POST /categories: */ + { + .url_prefix = "/categories", + .method = MHD_HTTP_METHOD_POST, + .handler = &TMH_private_post_categories, + /* allow category data of up to 8 kb, that should be plenty */ + .max_upload = 1024 * 8 + }, + /* GET /categories/$ID: */ + { + .url_prefix = "/categories/", + .method = MHD_HTTP_METHOD_GET, + .have_id_segment = true, + .allow_deleted_instance = true, + .handler = &TMH_private_get_categories_ID + }, + /* DELETE /categories/$ID: */ + { + .url_prefix = "/categories/", + .method = MHD_HTTP_METHOD_DELETE, + .have_id_segment = true, + .allow_deleted_instance = true, + .handler = &TMH_private_delete_categories_ID + }, + /* PATCH /categories/$ID/: */ + { + .url_prefix = "/categories/", + .method = MHD_HTTP_METHOD_PATCH, + .have_id_segment = true, + .allow_deleted_instance = true, + .handler = &TMH_private_patch_categories_ID, + /* allow category data of up to 8 kb, that should be plenty */ + .max_upload = 1024 * 8 + }, /* GET /products: */ { .url_prefix = "/products", @@ -949,7 +1001,7 @@ url_handler (void *cls, in the code... */ .max_upload = 1024 * 1024 * 8 }, - /* GET /products/$ID/: */ + /* GET /products/$ID: */ { .url_prefix = "/products/", .method = MHD_HTTP_METHOD_GET, diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c index d1340249..10f0cd39 100644 --- a/src/backend/taler-merchant-httpd_config.c +++ b/src/backend/taler-merchant-httpd_config.c @@ -42,7 +42,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "14:1:10" +#define MERCHANT_PROTOCOL_VERSION "16:0:12" /** diff --git a/src/backend/taler-merchant-httpd_private-delete-categories-ID.c b/src/backend/taler-merchant-httpd_private-delete-categories-ID.c new file mode 100644 index 00000000..892dbd9c --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-delete-categories-ID.c @@ -0,0 +1,92 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-delete-categories-ID.c + * @brief implement DELETE /private/categories/$ID + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-delete-categories-ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a DELETE "/categories/$ID" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_delete_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + unsigned long long cnum; + char dummy; + + (void) rh; + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + if (1 != sscanf (hc->infix, + "%llu%c", + &cnum, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "category_id must be a number"); + } + qs = TMH_db->delete_category (TMH_db->cls, + mi->settings.id, + cnum); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_category"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "delete_category"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + hc->infix); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_private-delete-categories-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-categories-ID.h b/src/backend/taler-merchant-httpd_private-delete-categories-ID.h new file mode 100644 index 00000000..b17eed49 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-delete-categories-ID.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-delete-categories-ID.h + * @brief implement DELETE /private/categories/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_CATEGORIES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_CATEGORIES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/categories/$ID" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_delete_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_private-delete-categories-ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-otp-devices-ID.c b/src/backend/taler-merchant-httpd_private-delete-otp-devices-ID.c index b147b84f..ff9ad2ef 100644 --- a/src/backend/taler-merchant-httpd_private-delete-otp-devices-ID.c +++ b/src/backend/taler-merchant-httpd_private-delete-otp-devices-ID.c @@ -61,7 +61,7 @@ TMH_private_delete_otp_devices_ID (const struct TMH_RequestHandler *rh, case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, + TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, hc->infix); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: return TALER_MHD_reply_static (connection, diff --git a/src/backend/taler-merchant-httpd_private-delete-products-ID.c b/src/backend/taler-merchant-httpd_private-delete-products-ID.c index 7d314785..23b6de3c 100644 --- a/src/backend/taler-merchant-httpd_private-delete-products-ID.c +++ b/src/backend/taler-merchant-httpd_private-delete-products-ID.c @@ -59,22 +59,30 @@ TMH_private_delete_products_ID (const struct TMH_RequestHandler *rh, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "delete_product (soft)"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* check if deletion must have failed because of locks by - checking if the product exists */ - qs = TMH_db->lookup_product (TMH_db->cls, - mi->settings.id, - hc->infix, - NULL); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "lookup_product"); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - hc->infix); + { + size_t num_categories = 0; + uint64_t *categories = NULL; + + /* check if deletion must have failed because of locks by + checking if the product exists */ + qs = TMH_db->lookup_product (TMH_db->cls, + mi->settings.id, + hc->infix, + NULL, + &num_categories, + &categories); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "lookup_product"); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + hc->infix); + GNUNET_free (categories); + } return TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, diff --git a/src/backend/taler-merchant-httpd_private-get-categories-ID.c b/src/backend/taler-merchant-httpd_private-get-categories-ID.c new file mode 100644 index 00000000..02ef3495 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-categories-ID.c @@ -0,0 +1,119 @@ +/* + This file is part of TALER + (C) 2022-2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-categories-ID.c + * @brief implement GET /private/categories/$ID + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-get-categories-ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a GET "/private/categories/$ID" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + unsigned long long cnum; + char dummy; + struct TALER_MERCHANTDB_CategoryDetails cd; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + if (1 != sscanf (hc->infix, + "%llu%c", + &cnum, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "category_id must be a number"); + } + + qs = TMH_db->select_category (TMH_db->cls, + mi->settings.id, + cnum, + &cd); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_category"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + hc->infix); + } + { + MHD_RESULT ret; + json_t *products; + + products = json_array (); + GNUNET_assert (NULL != products); + for (unsigned int i = 0; i<cd.num_products; i++) + { + const struct TALER_MERCHANTDB_ProductSummary *product + = &cd.products[i]; + json_t *jprod; + + jprod = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_id", + product->product_id), + GNUNET_JSON_pack_string ("description", + product->description), + GNUNET_JSON_pack_object_steal ("description_i18n", + product->description_i18n)); + GNUNET_assert (0 == + json_array_append_new (products, + jprod)); + } + + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("name", + cd.category_name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("name_i18n", + cd.category_name_i18n)), + GNUNET_JSON_pack_array_steal ("products", + products)); + TALER_MERCHANTDB_category_details_free (&cd); + return ret; + } +} + + +/* end of taler-merchant-httpd_private-get-categories-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-categories-ID.h b/src/backend/taler-merchant-httpd_private-get-categories-ID.h new file mode 100644 index 00000000..c0226659 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-categories-ID.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-categories-ID.h + * @brief implement GET /private/categories/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/private/categories/$ID" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_private-get-categories-ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-categories.c b/src/backend/taler-merchant-httpd_private-get-categories.c new file mode 100644 index 00000000..8ebccb2b --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-categories.c @@ -0,0 +1,93 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-categories.c + * @brief implement GET /categories + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-get-categories.h" + + +/** + * Add category details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param category_id ID of the category + * @param category_name name of the category + * @param category_name_i18n translations of the @a category_name + * @param product_count number of products in the category + */ +static void +add_category (void *cls, + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t product_count) +{ + json_t *pa = cls; + + GNUNET_assert ( + 0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ( + "category_id", + category_id), + GNUNET_JSON_pack_string ( + "name", + category_name), + GNUNET_JSON_pack_object_incref ( + "name_i18n", + (json_t *) category_name_i18n), + GNUNET_JSON_pack_uint64 ( + "product_count", + product_count)))); +} + + +MHD_RESULT +TMH_private_get_categories (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *pa; + enum GNUNET_DB_QueryStatus qs; + + pa = json_array (); + GNUNET_assert (NULL != pa); + qs = TMH_db->lookup_categories (TMH_db->cls, + hc->instance->settings.id, + &add_category, + pa); + if (0 > qs) + { + GNUNET_break (0); + json_decref (pa); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("categories", + pa)); +} + + +/* end of taler-merchant-httpd_private-get-categories.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-categories.h b/src/backend/taler-merchant-httpd_private-get-categories.h new file mode 100644 index 00000000..68eed05e --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-categories.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-categories.h + * @brief implement GET /private/categories + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/private/categories" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_categories ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_private-get-categories.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-pos.c b/src/backend/taler-merchant-httpd_private-get-pos.c new file mode 100644 index 00000000..4c14b6ae --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-pos.c @@ -0,0 +1,148 @@ +/* + This file is part of TALER + (C) 2019, 2020, 2021, 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-pos.c + * @brief implement GET /private/pos + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-get-pos.h" + + +/** + * Closure for add_product(). + */ +struct Context +{ + /** + * JSON array of products we are building. + */ + json_t *pa; + + /** + * JSON array of categories we are building. + */ + json_t *ca; + +}; + + +/** + * Add product details to our JSON array. + * + * @param ctx a `struct Context` with JSON arrays to build + * @param product_id ID of the product + */ +static void +add_product (void *cls, + uint64_t product_serial, + const char *product_id, + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_categories, + const uint64_t *categories) +{ + struct Context *ctx = cls; + json_t *pa = ctx->pa; + json_t *cata; + + cata = json_array (); + GNUNET_assert (NULL != cata); + for (size_t i = 0; i<num_categories; i++) + GNUNET_assert ( + 0 == json_array_append_new ( + cata, + json_integer (categories[i]))); + GNUNET_assert ( + 0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("description", + pd->description), + GNUNET_JSON_pack_object_incref ("description_i18n", + (json_t *) pd->description_i18n), + GNUNET_JSON_pack_string ("unit", + pd->unit), + TALER_JSON_pack_amount ("price", + &pd->price), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("image", + pd->image)), + GNUNET_JSON_pack_array_steal ("categories", + cata), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("taxes", + (json_t *) pd->taxes)), + (INT64_MAX == pd->total_stock) + ? GNUNET_JSON_pack_int64 ("total_stock", + pd->total_stock) + : GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("total_stock", + NULL)), + GNUNET_JSON_pack_uint64 ("minimum_age", + pd->minimum_age), + GNUNET_JSON_pack_uint64 ("product_serial", + product_serial), + GNUNET_JSON_pack_string ("product_id", + product_id)))); +} + + +MHD_RESULT +TMH_private_get_pos (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct Context ctx; + enum GNUNET_DB_QueryStatus qs; + + ctx.pa = json_array (); + GNUNET_assert (NULL != ctx.pa); + ctx.ca = json_array (); + GNUNET_assert (NULL != ctx.ca); + GNUNET_assert ( + 0 == json_array_append_new ( + ctx.ca, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("id", + 0), + GNUNET_JSON_pack_string ("name", + "default")))); + qs = TMH_db->lookup_all_products (TMH_db->cls, + hc->instance->settings.id, + &add_product, + &ctx); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ctx.pa); + json_decref (ctx.ca); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("categories", + ctx.ca), + GNUNET_JSON_pack_array_steal ("products", + ctx.pa)); +} + + +/* end of taler-merchant-httpd_private-get-pos.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-pos.h b/src/backend/taler-merchant-httpd_private-get-pos.h new file mode 100644 index 00000000..ce266823 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-pos.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-pos.h + * @brief implement GET /pos + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_POS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_POS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/pos" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_pos (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_private-get-pos.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-products-ID.c b/src/backend/taler-merchant-httpd_private-get-products-ID.c index 1406349f..0729b1df 100644 --- a/src/backend/taler-merchant-httpd_private-get-products-ID.c +++ b/src/backend/taler-merchant-httpd_private-get-products-ID.c @@ -32,19 +32,25 @@ * @return MHD result code */ MHD_RESULT -TMH_private_get_products_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +TMH_private_get_products_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi = hc->instance; struct TALER_MERCHANTDB_ProductDetails pd = { 0 }; enum GNUNET_DB_QueryStatus qs; + size_t num_categories = 0; + uint64_t *categories = NULL; + json_t *jcategories; GNUNET_assert (NULL != mi); qs = TMH_db->lookup_product (TMH_db->cls, mi->settings.id, hc->infix, - &pd); + &pd, + &num_categories, + &categories); if (0 > qs) { GNUNET_break (0); @@ -60,6 +66,16 @@ TMH_private_get_products_ID (const struct TMH_RequestHandler *rh, TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, hc->infix); } + jcategories = json_array (); + GNUNET_assert (NULL != jcategories); + for (size_t i = 0; i<num_categories; i++) + { + GNUNET_assert (0 == + json_array_append_new (jcategories, + json_integer (categories[i]))); + } + GNUNET_free (categories); + { MHD_RESULT ret; @@ -72,12 +88,16 @@ TMH_private_get_products_ID (const struct TMH_RequestHandler *rh, pd.description_i18n), GNUNET_JSON_pack_string ("unit", pd.unit), + GNUNET_JSON_pack_array_steal ("categories", + jcategories), TALER_JSON_pack_amount ("price", &pd.price), - GNUNET_JSON_pack_string ("image", - pd.image), - GNUNET_JSON_pack_array_steal ("taxes", - pd.taxes), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("image", + pd.image)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("taxes", + pd.taxes)), GNUNET_JSON_pack_int64 ("total_stock", (INT64_MAX == pd.total_stock) ? -1LL @@ -86,8 +106,9 @@ TMH_private_get_products_ID (const struct TMH_RequestHandler *rh, pd.total_sold), GNUNET_JSON_pack_uint64 ("total_lost", pd.total_lost), - GNUNET_JSON_pack_object_steal ("address", - pd.address), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("address", + pd.address)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_timestamp ("next_restock", (pd.next_restock))), diff --git a/src/backend/taler-merchant-httpd_private-patch-categories-ID.c b/src/backend/taler-merchant-httpd_private-patch-categories-ID.c new file mode 100644 index 00000000..1aa489cf --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-patch-categories-ID.c @@ -0,0 +1,120 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ + +/** + * @file taler-merchant-httpd_private-patch-categories-ID.c + * @brief implementing PATCH /categories/$ID request handling + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-patch-categories-ID.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_patch_categories_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + unsigned long long cnum; + char dummy; + const char *category_name; + const json_t *category_name_i18n; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &category_name), + GNUNET_JSON_spec_object_const ("name_i18n", + &category_name_i18n), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + if (1 != sscanf (hc->infix, + "%llu%c", + &cnum, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "category_id must be a number"); + } + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + qs = TMH_db->update_category (TMH_db->cls, + mi->settings.id, + cnum, + category_name, + category_name_i18n); + { + MHD_RESULT ret = MHD_NO; + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_category"); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + category_name); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + break; + } + GNUNET_JSON_parse_free (spec); + return ret; + } +} + + +/* end of taler-merchant-httpd_private-patch-categories-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-categories-ID.h b/src/backend/taler-merchant-httpd_private-patch-categories-ID.h new file mode 100644 index 00000000..c88290a8 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-patch-categories-ID.h @@ -0,0 +1,45 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ + +/** + * @file taler-merchant-httpd_private-patch-categories-ID.h + * @brief implementing PATCH /private/categories/$ID request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_CATEGORIES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_CATEGORIES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * PATCH descriptions of an existing product category. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_patch_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-products-ID.c b/src/backend/taler-merchant-httpd_private-patch-products-ID.c index 7bc327cd..6e50cced 100644 --- a/src/backend/taler-merchant-httpd_private-patch-products-ID.c +++ b/src/backend/taler-merchant-httpd_private-patch-products-ID.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2020 Taler Systems SA + (C) 2020-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -29,76 +29,6 @@ /** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Determine the cause of the PATCH failure in more detail and report. - * - * @param connection connection to report on - * @param instance_id instance we are processing - * @param product_id ID of the product to patch - * @param pd product details we failed to set - */ -static MHD_RESULT -determine_cause (struct MHD_Connection *connection, - const char *instance_id, - const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd) -{ - struct TALER_MERCHANTDB_ProductDetails pdx; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_product (TMH_db->cls, - instance_id, - product_id, - &pdx); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - product_id); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; /* do below */ - } - - { - enum TALER_ErrorCode ec; - - ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - if (pdx.total_lost > pd->total_lost) - ec = TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED; - if (pdx.total_sold > pd->total_sold) - ec = TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED; - if (pdx.total_stock > pd->total_stock) - ec = TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED; - TALER_MERCHANTDB_product_details_free (&pdx); - GNUNET_break (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE != ec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - ec, - NULL); - } -} - - -/** * PATCH configuration of an existing instance, given its configuration. * * @param rh context of the handler @@ -107,13 +37,15 @@ determine_cause (struct MHD_Connection *connection, * @return MHD result code */ MHD_RESULT -TMH_private_patch_products_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +TMH_private_patch_products_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi = hc->instance; const char *product_id = hc->infix; struct TALER_MERCHANTDB_ProductDetails pd = {0}; + const json_t *categories = NULL; int64_t total_stock; enum GNUNET_DB_QueryStatus qs; struct GNUNET_JSON_Specification spec[] = { @@ -135,6 +67,10 @@ TMH_private_patch_products_ID (const struct TMH_RequestHandler *rh, GNUNET_JSON_spec_json ("taxes", &pd.taxes), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("categories", + &categories), + NULL), GNUNET_JSON_spec_int64 ("total_stock", &total_stock), GNUNET_JSON_spec_mark_optional ( @@ -155,6 +91,15 @@ TMH_private_patch_products_ID (const struct TMH_RequestHandler *rh, NULL), GNUNET_JSON_spec_end () }; + MHD_RESULT ret; + size_t num_cats = 0; + uint64_t *cats = NULL; + bool no_instance; + ssize_t no_cat; + bool no_product; + bool lost_reduced; + bool sold_reduced; + bool stock_reduced; pd.total_sold = 0; /* will be ignored anyway */ GNUNET_assert (NULL != mi); @@ -173,11 +118,11 @@ TMH_private_patch_products_ID (const struct TMH_RequestHandler *rh, if (total_stock < -1) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "total_stock"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "total_stock"); + goto cleanup; } if (-1 == total_stock) pd.total_stock = INT64_MAX; @@ -189,23 +134,45 @@ TMH_private_patch_products_ID (const struct TMH_RequestHandler *rh, if (! TMH_location_object_valid (pd.address)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "address"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "address"); + goto cleanup; + } + num_cats = json_array_size (categories); + cats = GNUNET_new_array (num_cats, + uint64_t); + { + size_t idx; + json_t *val; + + json_array_foreach (categories, idx, val) + { + if (! json_is_integer (val)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "categories"); + goto cleanup; + } + cats[idx] = json_integer_value (val); + } } + if (NULL == pd.description_i18n) pd.description_i18n = json_object (); if (! TALER_JSON_check_i18n (pd.description_i18n)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "description_i18n"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "description_i18n"); + goto cleanup; } if (NULL == pd.taxes) @@ -214,74 +181,143 @@ TMH_private_patch_products_ID (const struct TMH_RequestHandler *rh, if (! TMH_taxes_array_valid (pd.taxes)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "taxes"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "taxes"); + goto cleanup; } + if (NULL == pd.image) pd.image = ""; if (! TMH_image_data_url_valid (pd.image)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "image"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "image"); + goto cleanup; } + if ( (pd.total_stock < pd.total_sold + pd.total_lost) || (pd.total_sold + pd.total_lost < pd.total_sold) /* integer overflow */) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS, NULL); + goto cleanup; } + qs = TMH_db->update_product (TMH_db->cls, mi->settings.id, product_id, - &pd); + &pd, + num_cats, + cats, + &no_instance, + &no_cat, + &no_product, + &lost_reduced, + &sold_reduced, + &stock_reduced); + switch (qs) { - MHD_RESULT ret = MHD_NO; + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + goto cleanup; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected problem in stored procedure"); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - ret = determine_cause (connection, - mi->settings.id, - product_id, - &pd); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - break; - } - GNUNET_JSON_parse_free (spec); - return ret; + if (no_instance) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + mi->settings.id); + goto cleanup; + } + if (-1 != no_cat) + { + char cats[24]; + + GNUNET_snprintf (cats, + sizeof (cats), + "%llu", + (unsigned long long) no_cat); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + cats); + goto cleanup; + } + if (no_product) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + product_id); + goto cleanup; + } + if (lost_reduced) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED, + NULL); + goto cleanup; + } + if (sold_reduced) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED, + NULL); + goto cleanup; + } + if (stock_reduced) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED, + NULL); + goto cleanup; } + /* success! */ + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +cleanup: + GNUNET_free (cats); + GNUNET_JSON_parse_free (spec); + return ret; } diff --git a/src/backend/taler-merchant-httpd_private-post-categories.c b/src/backend/taler-merchant-httpd_private-post-categories.c new file mode 100644 index 00000000..675de765 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-post-categories.c @@ -0,0 +1,170 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-post-categories.c + * @brief implementing POST /private/categories request handling + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-post-categories.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +MHD_RESULT +TMH_private_post_categories (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *category_name; + const json_t *category_name_i18n; + uint64_t category_id; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &category_name), + GNUNET_JSON_spec_object_const ("name_i18n", + &category_name_i18n), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + } + + /* finally, interact with DB until no serialization error */ + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + json_t *xcategory_name_i18n; + + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "POST /categories")) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + qs = TMH_db->select_category_by_name (TMH_db->cls, + mi->settings.id, + category_name, + &xcategory_name_i18n, + &category_id); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + /* Clean up and fail hard */ + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + /* restart transaction */ + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Good, we can proceed! */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* idempotency check: is etp == tp? */ + { + bool eq; + + eq = (1 == json_equal (xcategory_name_i18n, + category_name_i18n)); + json_decref (xcategory_name_i18n); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return eq + ? TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("category_id", + category_id)) + : TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_CATEGORIES_CONFLICT_CATEGORY_EXISTS, + category_name); + } + } /* end switch (qs) */ + + qs = TMH_db->insert_category (TMH_db->cls, + mi->settings.id, + category_name, + category_name_i18n, + &category_id); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + TMH_db->rollback (TMH_db->cls); + break; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + qs = TMH_db->commit (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } +retry: + GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); + TMH_db->rollback (TMH_db->cls); + } /* for RETRIES loop */ + GNUNET_JSON_parse_free (spec); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + (GNUNET_DB_STATUS_SOFT_ERROR == qs) + ? TALER_EC_GENERIC_DB_SOFT_FAILURE + : TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("category_id", + category_id)); +} + + +/* end of taler-merchant-httpd_private-post-categories.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-categories.h b/src/backend/taler-merchant-httpd_private-post-categories.h new file mode 100644 index 00000000..a8431059 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-post-categories.h @@ -0,0 +1,45 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ + +/** + * @file taler-merchant-httpd_private-post-categories.h + * @brief implementing POST /categories request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_CATEGORIES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_CATEGORIES_H + +#include "taler-merchant-httpd.h" + + +/** + * Generate a product category. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_post_categories ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c index eedece55..8bdae3f1 100644 --- a/src/backend/taler-merchant-httpd_private-post-orders.c +++ b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -940,6 +940,8 @@ execute_order (struct OrderContext *oc) struct TALER_MERCHANTDB_ProductDetails pd; MHD_RESULT ret; const struct InventoryProduct *ip; + size_t num_categories = 0; + uint64_t *categories = NULL; ip = &oc->parse_request.inventory_products[ oc->execute_order.out_of_stock_index]; @@ -950,10 +952,13 @@ execute_order (struct OrderContext *oc) TMH_db->cls, oc->hc->instance->settings.id, ip->product_id, - &pd); + &pd, + &num_categories, + &categories); switch (qs) { case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_free (categories); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order creation failed: product out of stock\n"); ret = TALER_MHD_REPLY_JSON_PACK ( @@ -1317,6 +1322,7 @@ get_exchange_keys (void *cls, rx); } + /** * Fetch details about the token family with the given @a slug * and add them to the list of token authorities. Check if the @@ -1340,12 +1346,13 @@ set_token_authority (struct OrderContext *oc, // TODO: make this configurable. This is the granularity of token // expiration dates. GNUNET_TIME_UNIT_DAYS - ); + ); qs = TMH_db->lookup_token_family_key (TMH_db->cls, oc->hc->instance->settings.id, slug, - GNUNET_TIME_absolute_to_timestamp (min_start_date), + GNUNET_TIME_absolute_to_timestamp ( + min_start_date), start_date, &key_details); @@ -1390,9 +1397,10 @@ set_token_authority (struct OrderContext *oc, struct GNUNET_CRYPTO_BlindSignPrivateKey *priv; struct GNUNET_CRYPTO_BlindSignPublicKey *pub; struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); - struct GNUNET_TIME_Timestamp valid_before = GNUNET_TIME_absolute_to_timestamp( - GNUNET_TIME_absolute_add (now, - key_details.token_family.duration)); + struct GNUNET_TIME_Timestamp valid_before = + GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (now, + key_details.token_family.duration)); GNUNET_CRYPTO_blind_sign_keys_create (&priv, &pub, @@ -1411,13 +1419,16 @@ set_token_authority (struct OrderContext *oc, slug, &token_pub, &token_priv, - GNUNET_TIME_absolute_to_timestamp (now), + GNUNET_TIME_absolute_to_timestamp (now + ), valid_before); authority.token_expiration = valid_before; authority.pub = &token_pub; // GNUNET_CRYPTO_blind_sign_priv_decref (&token_priv.private_key); - } else { + } + else + { authority.token_expiration = key_details.valid_before; authority.pub = key_details.pub; } @@ -1428,11 +1439,13 @@ set_token_authority (struct OrderContext *oc, GNUNET_free (key_details.token_family.slug); GNUNET_free (key_details.token_family.name); - if (NULL != key_details.priv) { + if (NULL != key_details.priv) + { GNUNET_CRYPTO_blind_sign_priv_decref (&key_details.priv->private_key); } - switch (key_details.token_family.kind) { + switch (key_details.token_family.kind) + { case TALER_MERCHANTDB_TFK_Subscription: authority.kind = TALER_MCTK_SUBSCRIPTION; authority.details.subscription.start_date = key_details.valid_after; @@ -1454,6 +1467,7 @@ set_token_authority (struct OrderContext *oc, return MHD_YES; } + /** * Serialize order into @a oc->serialize_order.contract, * ready to be stored in the database. Upon success, continue @@ -1472,16 +1486,16 @@ serialize_order (struct OrderContext *oc) merchant = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("name", - settings->name), + settings->name), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("website", - settings->website)), + settings->website)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("email", - settings->email)), + settings->email)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("logo", - settings->logo))); + settings->logo))); GNUNET_assert (NULL != merchant); { json_t *loca; @@ -1493,7 +1507,7 @@ serialize_order (struct OrderContext *oc) loca = json_deep_copy (loca); GNUNET_assert (NULL != loca); GNUNET_assert (0 == - json_object_set_new (merchant, + json_object_set_new (merchant, "address", loca)); } @@ -1508,7 +1522,7 @@ serialize_order (struct OrderContext *oc) juri = json_deep_copy (juri); GNUNET_assert (NULL != juri); GNUNET_assert (0 == - json_object_set_new (merchant, + json_object_set_new (merchant, "jurisdiction", juri)); } @@ -1516,7 +1530,8 @@ serialize_order (struct OrderContext *oc) for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) { - struct TALER_MerchantContractTokenAuthority *authority = &oc->parse_choices.authorities[i]; + struct TALER_MerchantContractTokenAuthority *authority = &oc->parse_choices. + authorities[i]; // TODO: Finish spec to clearly define how token families are stored in // ContractTerms. @@ -1529,7 +1544,7 @@ serialize_order (struct OrderContext *oc) // so it's clear with key is referenced. json_t *jauthority = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("description", - authority->description), + authority->description), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("description_i18n", authority->description_i18n)), @@ -1539,10 +1554,10 @@ serialize_order (struct OrderContext *oc) &authority->pub->public_key.pub_key_hash), GNUNET_JSON_pack_timestamp ("token_expiration", authority->token_expiration) - ); + ); GNUNET_assert (0 == - json_object_set_new (token_types, + json_object_set_new (token_types, authority->label, jauthority)); } @@ -1561,20 +1576,21 @@ serialize_order (struct OrderContext *oc) json_t *jinput = GNUNET_JSON_PACK ( GNUNET_JSON_pack_int64 ("type", input->type) - ); + ); if (TALER_MCIT_TOKEN == input->type) { - GNUNET_assert(0 == - json_object_set_new(jinput, - "number", - json_integer ( - input->details.token.count))); - GNUNET_assert(0 == - json_object_set_new(jinput, - "token_family_slug", - json_string ( - input->details.token.token_family_slug))); + GNUNET_assert (0 == + json_object_set_new (jinput, + "number", + json_integer ( + input->details.token.count))); + GNUNET_assert (0 == + json_object_set_new (jinput, + "token_family_slug", + json_string ( + input->details.token. + token_family_slug))); } GNUNET_assert (0 == json_array_append_new (inputs, jinput)); @@ -1587,21 +1603,22 @@ serialize_order (struct OrderContext *oc) json_t *joutput = GNUNET_JSON_PACK ( GNUNET_JSON_pack_int64 ("type", output->type) - ); + ); if (TALER_MCOT_TOKEN == output->type) { - GNUNET_assert(0 == - json_object_set_new(joutput, - "number", - json_integer ( - output->details.token.count))); - - GNUNET_assert(0 == - json_object_set_new(joutput, - "token_family_slug", - json_string ( - output->details.token.token_family_slug))); + GNUNET_assert (0 == + json_object_set_new (joutput, + "number", + json_integer ( + output->details.token.count))); + + GNUNET_assert (0 == + json_object_set_new (joutput, + "token_family_slug", + json_string ( + output->details.token. + token_family_slug))); } GNUNET_assert (0 == json_array_append (outputs, joutput)); @@ -1609,10 +1626,10 @@ serialize_order (struct OrderContext *oc) json_t *jchoice = GNUNET_JSON_PACK ( GNUNET_JSON_pack_array_incref ("inputs", - inputs), + inputs), GNUNET_JSON_pack_array_incref ("outputs", - outputs) - ); + outputs) + ); GNUNET_assert (0 == json_array_append (choices, jchoice)); } @@ -1633,7 +1650,8 @@ serialize_order (struct OrderContext *oc) oc->parse_order.fulfillment_message)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("fulfillment_message_i18n", - oc->parse_order.fulfillment_message_i18n)), + oc->parse_order.fulfillment_message_i18n)) + , GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("fulfillment_url", oc->parse_order.fulfillment_url)), @@ -1674,12 +1692,12 @@ serialize_order (struct OrderContext *oc) &oc->parse_order.brutto), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_array_incref ("choices", - choices) - ), + choices) + ), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("token_types", token_types) - ), + ), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("extra", (json_t *) oc->parse_order.extra)) @@ -1715,6 +1733,7 @@ serialize_order (struct OrderContext *oc) oc->phase++; } + /** * Set max_fee in @a oc based on STEFAN value if * not yet present. Upon success, continue @@ -1750,6 +1769,7 @@ set_max_fee (struct OrderContext *oc) oc->phase++; } + /** * Set list of acceptable exchanges in @a oc. Upon success, continue * processing with set_max_fee(). @@ -1871,7 +1891,7 @@ parse_order (struct OrderContext *oc) NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_array_const ("choices", - &oc->parse_order.choices), + &oc->parse_order.choices), NULL), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_web_url ("merchant_base_url", @@ -1937,7 +1957,7 @@ parse_order (struct OrderContext *oc) ret); return; } - if (NULL == version || 0 == strcmp("0", version)) + if (NULL == version || 0 == strcmp ("0", version)) { oc->parse_order.version = TALER_MCV_V0; @@ -1952,11 +1972,11 @@ parse_order (struct OrderContext *oc) return; } } - else if (0 == strcmp("1", version)) + else if (0 == strcmp ("1", version)) { oc->parse_order.version = TALER_MCV_V1; - if (! json_is_array(oc->parse_order.choices)) + if (! json_is_array (oc->parse_order.choices)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); @@ -1989,9 +2009,9 @@ parse_order (struct OrderContext *oc) return; } if ( (! no_fee) && - (GNUNET_OK != - TALER_amount_cmp_currency (&oc->parse_order.brutto, - &oc->parse_order.max_fee)) ) + (GNUNET_OK != + TALER_amount_cmp_currency (&oc->parse_order.brutto, + &oc->parse_order.max_fee)) ) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); @@ -2268,6 +2288,7 @@ parse_order (struct OrderContext *oc) oc->phase++; } + /** * Parse contract choices. Upon success, continue * processing with merge_inventory(). @@ -2355,7 +2376,7 @@ parse_choices (struct OrderContext *oc) GNUNET_JSON_spec_uint32 ("count", &input.details.token.count), NULL), - GNUNET_JSON_spec_end() + GNUNET_JSON_spec_end () }; if (GNUNET_OK != @@ -2431,7 +2452,8 @@ parse_choices (struct OrderContext *oc) size_t idx; json_array_foreach ((json_t *) joutputs, idx, joutput) { - struct TALER_MerchantContractOutput output = { .details.token.count = 1 }; + struct TALER_MerchantContractOutput output = { .details.token.count = 1} + ; const char *kind; const char *ierror_name; unsigned int ierror_line; @@ -2447,14 +2469,14 @@ parse_choices (struct OrderContext *oc) GNUNET_JSON_spec_uint32 ("count", &output.details.token.count), NULL), - GNUNET_JSON_spec_end() + GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (joutput, - ispec, - &ierror_name, - &ierror_line)) + ispec, + &ierror_name, + &ierror_line)) { GNUNET_JSON_parse_free (spec); GNUNET_JSON_parse_free (ispec); @@ -2526,6 +2548,7 @@ parse_choices (struct OrderContext *oc) oc->phase++; } + /** * Process the @a payment_target and add the details of how the * order could be paid to @a order. On success, continue @@ -2591,11 +2614,15 @@ merge_inventory (struct OrderContext *oc) = &oc->parse_request.inventory_products[i]; struct TALER_MERCHANTDB_ProductDetails pd; enum GNUNET_DB_QueryStatus qs; + size_t num_categories = 0; + uint64_t *categories = NULL; qs = TMH_db->lookup_product (TMH_db->cls, oc->hc->instance->settings.id, ip->product_id, - &pd); + &pd, + &num_categories, + &categories); if (qs <= 0) { enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; @@ -2631,6 +2658,7 @@ merge_inventory (struct OrderContext *oc) ip->product_id); return; } + GNUNET_free (categories); oc->parse_order.minimum_age = GNUNET_MAX (oc->parse_order.minimum_age, pd.minimum_age); diff --git a/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c b/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c index 184f1d28..844b2ec8 100644 --- a/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c +++ b/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c @@ -29,9 +29,10 @@ MHD_RESULT -TMH_private_post_products_ID_lock (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +TMH_private_post_products_ID_lock ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi = hc->instance; const char *product_id = hc->infix; @@ -75,35 +76,48 @@ TMH_private_post_products_ID_lock (const struct TMH_RequestHandler *rh, switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "Serialization error for single-statment request"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Serialization error for single-statment request"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - qs = TMH_db->lookup_product (TMH_db->cls, - mi->settings.id, - product_id, - NULL); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "lookup_product"); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - product_id); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_GONE, - TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS, - product_id); + { + size_t num_categories = 0; + uint64_t *categories = NULL; + + qs = TMH_db->lookup_product (TMH_db->cls, + mi->settings.id, + product_id, + NULL, + &num_categories, + &categories); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "lookup_product"); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + product_id); + GNUNET_free (categories); + } + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_GONE, + TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS, + product_id); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: return TALER_MHD_reply_static (connection, MHD_HTTP_NO_CONTENT, diff --git a/src/backend/taler-merchant-httpd_private-post-products.c b/src/backend/taler-merchant-httpd_private-post-products.c index 3cad91a9..3edc0c16 100644 --- a/src/backend/taler-merchant-httpd_private-post-products.c +++ b/src/backend/taler-merchant-httpd_private-post-products.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2020 Taler Systems SA + (C) 2020-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -28,50 +28,6 @@ #include <taler/taler_json_lib.h> -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Check if the two products are identical. - * - * @param p1 product to compare - * @param p2 other product to compare - * @return true if they are 'equal', false if not or of payto_uris is not an array - */ -static bool -products_equal (const struct TALER_MERCHANTDB_ProductDetails *p1, - const struct TALER_MERCHANTDB_ProductDetails *p2) -{ - return ( (0 == strcmp (p1->description, - p2->description)) && - (1 == json_equal (p1->description_i18n, - p2->description_i18n)) && - (0 == strcmp (p1->unit, - p2->unit)) && - (GNUNET_OK == - TALER_amount_cmp_currency (&p1->price, - &p2->price)) && - (0 == TALER_amount_cmp (&p1->price, - &p2->price)) && - (1 == json_equal (p1->taxes, - p2->taxes)) && - (p1->total_stock == p2->total_stock) && - (p1->total_sold == p2->total_sold) && - (p1->total_lost == p2->total_lost) && - (p1->minimum_age == p2->minimum_age) && - (0 == strcmp (p1->image, - p2->image)) && - (1 == json_equal (p1->address, - p2->address)) && - (GNUNET_TIME_timestamp_cmp (p1->next_restock, - ==, - p2->next_restock) ) ); -} - - MHD_RESULT TMH_private_post_products (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, @@ -79,9 +35,9 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, { struct TMH_MerchantInstance *mi = hc->instance; struct TALER_MERCHANTDB_ProductDetails pd = { 0 }; + const json_t *categories = NULL; const char *product_id; int64_t total_stock; - enum GNUNET_DB_QueryStatus qs; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("product_id", &product_id), @@ -103,6 +59,10 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, GNUNET_JSON_spec_json ("taxes", &pd.taxes), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("categories", + &categories), + NULL), GNUNET_JSON_spec_int64 ("total_stock", &total_stock), GNUNET_JSON_spec_mark_optional ( @@ -119,6 +79,13 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, NULL), GNUNET_JSON_spec_end () }; + size_t num_cats = 0; + uint64_t *cats = NULL; + bool conflict; + bool no_instance; + ssize_t no_cat; + enum GNUNET_DB_QueryStatus qs; + MHD_RESULT ret; GNUNET_assert (NULL != mi); { @@ -138,13 +105,33 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, if (total_stock < -1) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "total_stock"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "total_stock"); + goto cleanup; } + num_cats = json_array_size (categories); + cats = GNUNET_new_array (num_cats, + uint64_t); + { + size_t idx; + json_t *val; + json_array_foreach (categories, idx, val) + { + if (! json_is_integer (val)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "categories"); + goto cleanup; + } + cats[idx] = json_integer_value (val); + } + } if (-1 == total_stock) pd.total_stock = INT64_MAX; @@ -162,31 +149,31 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, if (! TMH_taxes_array_valid (pd.taxes)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "taxes"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "taxes"); + goto cleanup; } if (! TMH_location_object_valid (pd.address)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "address"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "address"); + goto cleanup; } if (! TALER_JSON_check_i18n (pd.description_i18n)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "description_i18n"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "description_i18n"); + goto cleanup; } if (NULL == pd.image) @@ -194,110 +181,88 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, if (! TMH_image_data_url_valid (pd.image)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "image"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "image"); + goto cleanup; } - /* finally, interact with DB until no serialization error */ - for (unsigned int i = 0; i<MAX_RETRIES; i++) + qs = TMH_db->insert_product (TMH_db->cls, + mi->settings.id, + product_id, + &pd, + num_cats, + cats, + &no_instance, + &conflict, + &no_cat); + switch (qs) { - /* Test if an product of this id is known */ - struct TALER_MERCHANTDB_ProductDetails epd; - - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "/post products")) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - qs = TMH_db->lookup_product (TMH_db->cls, - mi->settings.id, - product_id, - &epd); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - /* restart transaction */ - goto retry; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* Good, we can proceed! */ - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* idempotency check: is epd == pd? */ - { - bool eq; - - eq = products_equal (&pd, - &epd); - TALER_MERCHANTDB_product_details_free (&epd); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return eq - ? TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0) - : TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS, - product_id); - } - } /* end switch (qs) */ - - qs = TMH_db->insert_product (TMH_db->cls, - mi->settings.id, - product_id, - &pd); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - TMH_db->rollback (TMH_db->cls); - break; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - qs = TMH_db->commit (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } -retry: - GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); - TMH_db->rollback (TMH_db->cls); - } /* for RETRIES loop */ - GNUNET_JSON_parse_free (spec); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, (GNUNET_DB_STATUS_SOFT_ERROR == qs) ? TALER_EC_GENERIC_DB_SOFT_FAILURE : TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; } - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); + if (no_instance) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + mi->settings.id); + goto cleanup; + } + if (conflict) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS, + product_id); + goto cleanup; + } + if (-1 != no_cat) + { + char nocats[24]; + + GNUNET_break_op (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_snprintf (nocats, + sizeof (nocats), + "%llu", + (unsigned long long) no_cat); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + nocats); + goto cleanup; + } + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +cleanup: + GNUNET_JSON_parse_free (spec); + GNUNET_free (cats); + return ret; } |