diff options
author | Christian Blättler <blatc2@bfh.ch> | 2024-06-13 11:34:46 +0200 |
---|---|---|
committer | Christian Blättler <blatc2@bfh.ch> | 2024-06-13 11:34:46 +0200 |
commit | 599db2c13cc7b4eddab22f1e8ef55dde23086f68 (patch) | |
tree | 53f1060f6507cffdaf3a139428d7ab4614b13efa /src | |
parent | b9315cf4675bfb2ae94b71cf8931963b27873a5b (diff) | |
parent | 2e8cfbe844d4fbcf5fa6f086be0b184821e1bdc2 (diff) |
Merge branch 'master' into tokens-payment
# Conflicts:
# src/backend/taler-merchant-httpd_private-post-orders.c
# src/backenddb/Makefile.am
# src/backenddb/merchant-0006.sql
# src/backenddb/pg_insert_token_family_key.c
Diffstat (limited to 'src')
83 files changed, 3884 insertions, 685 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-exchange.c b/src/backend/taler-merchant-exchange.c index 7945cb50..72566e9b 100644 --- a/src/backend/taler-merchant-exchange.c +++ b/src/backend/taler-merchant-exchange.c @@ -32,8 +32,8 @@ * long-polling and do not want to wake up too often. */ #define EXCHANGE_TIMEOUT GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_MINUTES, \ - 30) + GNUNET_TIME_UNIT_MINUTES, \ + 30) /** * How many inquiries do we process concurrently at most. @@ -1154,7 +1154,8 @@ find_work (void *cls) return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No open inquiries found, waiting for notification to resume\n"); + "No open inquiries found, waiting for notification to resume\n") + ; } } @@ -1215,7 +1216,7 @@ run (void *cls, { GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); - global_ret = EXIT_NO_RESTART; + global_ret = EXIT_FAILURE; return; } if (NULL == @@ -1233,7 +1234,7 @@ run (void *cls, GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to connect to database\n"); GNUNET_SCHEDULER_shutdown (); - global_ret = EXIT_NO_RESTART; + global_ret = EXIT_FAILURE; return; } { diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c index 95ba9bf1..a7c1cbb4 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 3777904f..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:0:10" +#define MERCHANT_PROTOCOL_VERSION "16:0:12" /** @@ -82,14 +82,20 @@ MH_handler_config (struct TMH_RequestHandler *rh, struct TMH_HandlerContext *hc) { static struct MHD_Response *response; + static struct GNUNET_TIME_Absolute a; (void) rh; (void) hc; + if ( (GNUNET_TIME_absolute_is_past (a)) && + (NULL != response) ) + { + MHD_destroy_response (response); + response = NULL; + } if (NULL == response) { json_t *specs = json_object (); json_t *exchanges = json_array (); - struct GNUNET_TIME_Absolute a; struct GNUNET_TIME_Timestamp km; char dat[128]; diff --git a/src/backend/taler-merchant-httpd_contract.h b/src/backend/taler-merchant-httpd_contract.h index 006d0205..6389cc70 100644 --- a/src/backend/taler-merchant-httpd_contract.h +++ b/src/backend/taler-merchant-httpd_contract.h @@ -367,7 +367,7 @@ struct TALER_MerchantContractTokenFamily }; /** - * Struct to hold contract terms in v0 and v1 format. v0 contracts are mdoelled + * Struct to hold contract terms in v0 and v1 format. v0 contracts are modelled * as a v1 contract with a single choice and no inputs and outputs. Use the * version field to explicitly differentiate between v0 and v1 contracts. */ diff --git a/src/backend/taler-merchant-httpd_mhd.c b/src/backend/taler-merchant-httpd_mhd.c index e96acca5..0bb22b35 100644 --- a/src/backend/taler-merchant-httpd_mhd.c +++ b/src/backend/taler-merchant-httpd_mhd.c @@ -59,6 +59,7 @@ TMH_MHD_test_html_desired (struct MHD_Connection *connection) bool ret = false; const char *accept; + // FIXME: use TALER_MHD_check_accept here! accept = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_ACCEPT); 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-instances-ID-kyc.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c index 8a338e7d..0382b742 100644 --- a/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c +++ b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c @@ -684,7 +684,7 @@ kyc_with_exchange (void *cls, keys, ekr->exchange_kyc_serial, &h_payto, - ekr->kc->mi->settings.ut, + 1, /* FIXME: this will go away! */ GNUNET_TIME_absolute_get_remaining (kc->timeout), &exchange_check_cb, ekr); diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID.c b/src/backend/taler-merchant-httpd_private-get-instances-ID.c index adc99c39..c8cd2c12 100644 --- a/src/backend/taler-merchant-httpd_private-get-instances-ID.c +++ b/src/backend/taler-merchant-httpd_private-get-instances-ID.c @@ -79,7 +79,7 @@ get_instances_ID (struct TMH_MerchantInstance *mi, mi->settings.name), GNUNET_JSON_pack_string ( "user_type", - TALER_KYCLOGIC_kyc_user_type2s (mi->settings.ut)), + "business"), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("website", mi->settings.website)), diff --git a/src/backend/taler-merchant-httpd_private-get-instances.c b/src/backend/taler-merchant-httpd_private-get-instances.c index 50b5c0c2..0220d4df 100644 --- a/src/backend/taler-merchant-httpd_private-get-instances.c +++ b/src/backend/taler-merchant-httpd_private-get-instances.c @@ -76,7 +76,7 @@ add_instance (void *cls, mi->settings.name), GNUNET_JSON_pack_string ( "user_type", - TALER_KYCLOGIC_kyc_user_type2s (mi->settings.ut)), + "business"), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("website", mi->settings.website)), 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-instances-ID.c b/src/backend/taler-merchant-httpd_private-patch-instances-ID.c index 027d5869..70df009e 100644 --- a/src/backend/taler-merchant-httpd_private-patch-instances-ID.c +++ b/src/backend/taler-merchant-httpd_private-patch-instances-ID.c @@ -64,17 +64,12 @@ patch_instances_ID (struct TMH_MerchantInstance *mi, { struct TALER_MERCHANTDB_InstanceSettings is; const char *name; - const char *uts = "business"; struct TMH_WireMethod *wm_head = NULL; struct TMH_WireMethod *wm_tail = NULL; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("name", &name), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("user_type", - &uts), - NULL), - GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("website", (const char **) &is.website), NULL), @@ -115,19 +110,6 @@ patch_instances_ID (struct TMH_MerchantInstance *mi, ? MHD_YES : MHD_NO; } - if (NULL == uts) - uts = "business"; - if (GNUNET_OK != - TALER_KYCLOGIC_kyc_user_type_from_string (uts, - &is.ut)) - { - 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, - "user_type"); - } if (! TMH_location_object_valid (is.address)) { GNUNET_break_op (0); @@ -185,7 +167,6 @@ patch_instances_ID (struct TMH_MerchantInstance *mi, /* Check for equality of settings */ if (! ( (0 == strcmp (mi->settings.name, name)) && - (mi->settings.ut == is.ut) && ((mi->settings.email == is.email) || (NULL != is.email && NULL != mi->settings.email && 0 == strcmp (mi->settings.email, @@ -240,7 +221,6 @@ giveup: json_decref (mi->settings.jurisdiction); is.id = mi->settings.id; mi->settings = is; - // mi->settings.user_type = (enum TALER_KYCLOGIC_KycUserType) is.user_type; mi->settings.address = json_incref (mi->settings.address); mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); mi->settings.name = GNUNET_strdup (name); 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-instances.c b/src/backend/taler-merchant-httpd_private-post-instances.c index a4cf884d..f56e7098 100644 --- a/src/backend/taler-merchant-httpd_private-post-instances.c +++ b/src/backend/taler-merchant-httpd_private-post-instances.c @@ -103,20 +103,6 @@ TMH_private_post_instances (const struct TMH_RequestHandler *rh, ? MHD_YES : MHD_NO; } - if (NULL == uts) - uts = "business"; - if (GNUNET_OK != - TALER_KYCLOGIC_kyc_user_type_from_string (uts, - &is.ut)) - { - 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, - "user_type"); - } - { enum GNUNET_GenericReturnValue ret; diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c index e8332c70..a2e3b493 100644 --- a/src/backend/taler-merchant-httpd_private-post-orders.c +++ b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -960,6 +960,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]; @@ -970,10 +972,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 ( @@ -1337,6 +1342,7 @@ get_exchange_keys (void *cls, rx); } + /** * Get rounded time interval. @a start is calculated by rounding * @a ts down to the nearest multiple of @a precision. @a end is @@ -1449,9 +1455,7 @@ set_token_family (struct OrderContext *oc, struct TALER_MERCHANTDB_TokenFamilyKeyDetails key_details; struct TALER_MerchantContractTokenFamily *family = NULL; enum GNUNET_DB_QueryStatus qs; - // TODO: make this configurable. This is the granularity of token - // expiration dates. This should be stored in the - // database along the token family. + /* TODO: Implement rounding duration of token family and use this here. */ struct GNUNET_TIME_Relative precision = GNUNET_TIME_UNIT_MONTHS; struct GNUNET_TIME_Timestamp min_valid_after; struct GNUNET_TIME_Timestamp max_valid_after; @@ -1694,6 +1698,7 @@ set_token_family (struct OrderContext *oc, return GNUNET_OK; } + /** * Serialize order into @a oc->serialize_order.contract, * ready to be stored in the database. Upon success, continue @@ -1712,16 +1717,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; @@ -1733,7 +1738,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)); } @@ -1748,7 +1753,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)); } @@ -1855,10 +1860,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)); } @@ -1879,7 +1884,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)), @@ -1961,6 +1967,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 @@ -1996,6 +2003,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(). @@ -2119,7 +2127,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", @@ -2185,7 +2193,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; @@ -2200,11 +2208,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); @@ -2237,9 +2245,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); @@ -2516,6 +2524,7 @@ parse_order (struct OrderContext *oc) oc->phase++; } + /** * Parse contract choices. Upon success, continue * processing with merge_inventory(). @@ -2606,7 +2615,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 != @@ -2665,7 +2674,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; @@ -2682,14 +2692,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); @@ -2744,6 +2754,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 @@ -2809,11 +2820,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; @@ -2848,6 +2863,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; } diff --git a/src/backend/taler-merchant-webhook.c b/src/backend/taler-merchant-webhook.c index 80db78fd..1d17843e 100644 --- a/src/backend/taler-merchant-webhook.c +++ b/src/backend/taler-merchant-webhook.c @@ -506,7 +506,7 @@ run (void *cls, { GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); - global_ret = EXIT_NO_RESTART; + global_ret = EXIT_FAILURE; return; } if (NULL == @@ -524,7 +524,7 @@ run (void *cls, GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to connect to database\n"); GNUNET_SCHEDULER_shutdown (); - global_ret = EXIT_NO_RESTART; + global_ret = EXIT_FAILURE; return; } { diff --git a/src/backend/taler-merchant-wirewatch.c b/src/backend/taler-merchant-wirewatch.c index 17eb7a0a..bba101e3 100644 --- a/src/backend/taler-merchant-wirewatch.c +++ b/src/backend/taler-merchant-wirewatch.c @@ -641,7 +641,7 @@ run (void *cls, { GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); - global_ret = EXIT_NO_RESTART; + global_ret = EXIT_FAILURE; return; } if (NULL == @@ -659,7 +659,7 @@ run (void *cls, GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to connect to database\n"); GNUNET_SCHEDULER_shutdown (); - global_ret = EXIT_NO_RESTART; + global_ret = EXIT_FAILURE; return; } { diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am index 95efe07e..e6511fef 100644 --- a/src/backenddb/Makefile.am +++ b/src/backenddb/Makefile.am @@ -65,7 +65,7 @@ libtalermerchantdb_la_LIBADD = \ libtalermerchantdb_la_LDFLAGS = \ $(POSTGRESQL_LDFLAGS) \ - -version-info 3:0:1 \ + -version-info 4:0:2 \ -no-undefined libtaler_plugin_merchantdb_postgres_la_SOURCES = \ @@ -79,6 +79,12 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_insert_transfer_details.h pg_insert_transfer_details.c \ pg_store_wire_fee_by_exchange.h pg_store_wire_fee_by_exchange.c \ pg_select_open_transfers.h pg_select_open_transfers.c \ + pg_lookup_categories.h pg_lookup_categories.c \ + pg_select_category_by_name.h pg_select_category_by_name.c \ + pg_select_category.h pg_select_category.c \ + pg_update_category.h pg_update_category.c \ + pg_insert_category.h pg_insert_category.c \ + pg_delete_category.h pg_delete_category.c \ pg_lookup_instances.h pg_lookup_instances.c \ pg_lookup_transfers.h pg_lookup_transfers.c \ pg_update_transfer_status.h pg_update_transfer_status.c \ @@ -115,6 +121,7 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_update_template.h pg_update_template.c \ pg_lookup_templates.h pg_lookup_templates.c \ pg_lookup_template.h pg_lookup_template.c \ + pg_lookup_all_products.h pg_lookup_all_products.c \ pg_lookup_products.h pg_lookup_products.c \ pg_lookup_product.h pg_lookup_product.c \ pg_delete_product.h pg_delete_product.c \ diff --git a/src/backenddb/merchant-0001.sql b/src/backenddb/merchant-0001.sql index 2adb9996..ca753a04 100644 --- a/src/backenddb/merchant-0001.sql +++ b/src/backenddb/merchant-0001.sql @@ -194,7 +194,7 @@ CREATE TABLE IF NOT EXISTS merchant_inventory ,description TEXT NOT NULL ,description_i18n BYTEA NOT NULL ,unit TEXT NOT NULL - ,image BYTEA NOT NULL + ,image BYTEA NOT NULL -- NOTE: merchant-0006 changes this to TEXT! ,taxes BYTEA NOT NULL ,price taler_amount_currency NOT NULL ,total_stock BIGINT NOT NULL diff --git a/src/backenddb/merchant-0006.sql b/src/backenddb/merchant-0006.sql index c0059b18..e216b4ad 100644 --- a/src/backenddb/merchant-0006.sql +++ b/src/backenddb/merchant-0006.sql @@ -32,5 +32,37 @@ ALTER TABLE merchant_contract_terms COMMENT ON COLUMN merchant_contract_terms.choice_index IS 'Index of selected choice. Refers to the `choices` array in the contract terms. NULL for contracts without choices.'; +ALTER TABLE merchant_inventory + ALTER COLUMN image SET DATA TYPE TEXT; + +CREATE TABLE IF NOT EXISTS merchant_categories + (category_serial BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY + ,merchant_serial BIGINT NOT NULL + REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE + ,category_name TEXT NOT NULL UNIQUE + ,category_name_i18n BYTEA NOT NULL + ); + +COMMENT ON COLUMN merchant_categories.category_name + IS 'name of the category'; +COMMENT ON COLUMN merchant_categories.category_name_i18n + IS 'JSON with translations of the category name'; + +CREATE TABLE merchant_product_categories + (category_serial BIGINT NOT NULL + REFERENCES merchant_categories (category_serial) ON DELETE CASCADE + ,product_serial BIGINT NOT NULL + REFERENCES merchant_inventory (product_serial) ON DELETE CASCADE); +CREATE INDEX merchant_categories_by_category + ON merchant_product_categories (category_serial); +CREATE INDEX merchant_categories_by_product + ON merchant_product_categories (product_serial); + +COMMENT ON COLUMN merchant_product_categories.category_serial + IS 'Reference to a category the product is part of'; +COMMENT ON COLUMN merchant_product_categories.product_serial + IS 'Reference to a product which is in the given category'; + + -- Complete transaction COMMIT; diff --git a/src/backenddb/merchantdb_helper.c b/src/backenddb/merchantdb_helper.c index 5894525c..6c1d2ec6 100644 --- a/src/backenddb/merchantdb_helper.c +++ b/src/backenddb/merchantdb_helper.c @@ -83,4 +83,25 @@ TALER_MERCHANTDB_token_family_details_free ( } +void +TALER_MERCHANTDB_category_details_free ( + struct TALER_MERCHANTDB_CategoryDetails *cd) +{ + GNUNET_free (cd->category_name); + json_decref (cd->category_name_i18n); + for (unsigned int i = 0; i<cd->num_products; i++) + { + struct TALER_MERCHANTDB_ProductSummary *ps + = &cd->products[i]; + + GNUNET_free (ps->product_id); + GNUNET_free (ps->description); + json_decref (ps->description_i18n); + } + GNUNET_array_grow (cd->products, + cd->num_products, + 0); +} + + /* end of merchantdb_helper.c */ diff --git a/src/backenddb/pg_delete_category.c b/src/backenddb/pg_delete_category.c new file mode 100644 index 00000000..4a43aa37 --- /dev/null +++ b/src/backenddb/pg_delete_category.c @@ -0,0 +1,54 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_delete_category.c + * @brief Implementation of the delete_category function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_delete_category.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TMH_PG_delete_category (void *cls, + const char *instance_id, + uint64_t category_id) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_uint64 (&category_id), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + PREPARE (pg, + "delete_category", + "DELETE" + " FROM merchant_categories" + " WHERE merchant_serial=" + " (SELECT merchant_serial " + " FROM merchant_instances" + " WHERE merchant_id=$1)" + " AND category_serial=$2"); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "delete_category", + params); +} diff --git a/src/backenddb/pg_delete_category.h b/src/backenddb/pg_delete_category.h new file mode 100644 index 00000000..8837a6b8 --- /dev/null +++ b/src/backenddb/pg_delete_category.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_delete_category.h + * @brief implementation of the delete_category function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_DELETE_CATEGORY_H +#define PG_DELETE_CATEGORY_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Delete information about a product category. + * + * @param cls closure + * @param instance_id instance to delete category of + * @param category_id identifies the category to delete + * @return DB status code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS + * if template unknown. + */ +enum GNUNET_DB_QueryStatus +TMH_PG_delete_category (void *cls, + const char *instance_id, + uint64_t category_id); + + +#endif diff --git a/src/backenddb/pg_delete_otp.c b/src/backenddb/pg_delete_otp.c index 5f011a4b..60f20481 100644 --- a/src/backenddb/pg_delete_otp.c +++ b/src/backenddb/pg_delete_otp.c @@ -43,11 +43,11 @@ TMH_PG_delete_otp (void *cls, "delete_otp", "DELETE" " FROM merchant_otp_devices" - " WHERE merchant_otp_devices.merchant_serial=" + " WHERE merchant_serial=" " (SELECT merchant_serial " " FROM merchant_instances" " WHERE merchant_id=$1)" - " AND merchant_otp_devices.otp_id=$2"); + " AND otp_id=$2"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, "delete_otp", params); diff --git a/src/backenddb/pg_insert_category.c b/src/backenddb/pg_insert_category.c new file mode 100644 index 00000000..f820b2c6 --- /dev/null +++ b/src/backenddb/pg_insert_category.c @@ -0,0 +1,65 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_insert_category.c + * @brief Implementation of the insert_category function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_insert_category.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TMH_PG_insert_category (void *cls, + const char *instance_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t *category_id) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_string (category_name), + TALER_PQ_query_param_json (category_name_i18n), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("category_serial", + category_id), + GNUNET_PQ_result_spec_end + }; + + check_connection (pg); + PREPARE (pg, + "insert_category", + "INSERT INTO merchant_categories" + "(category_name" + ",category_name_i18n" + ")" + " SELECT merchant_serial," + " $2, $3" + " FROM merchant_instances" + " WHERE merchant_id=$1" + " RETURNING category_serial"); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "insert_category", + params, + rs); +} diff --git a/src/backenddb/pg_insert_category.h b/src/backenddb/pg_insert_category.h new file mode 100644 index 00000000..8f4bfc99 --- /dev/null +++ b/src/backenddb/pg_insert_category.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_insert_category.h + * @brief implementation of the insert_category function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_INSERT_CATEGORY_H +#define PG_INSERT_CATEGORY_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Insert new product category. + * + * @param cls closure + * @param instance_id instance to insert OTP device for + * @param category_name name of the category + * @param category_name_i18n translations of the category name + * @param[out] category_id set to the category id on success + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_insert_category (void *cls, + const char *instance_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t *category_id); + + +#endif diff --git a/src/backenddb/pg_insert_deposit_to_transfer.sql b/src/backenddb/pg_insert_deposit_to_transfer.sql index b2e587f1..ae43b613 100644 --- a/src/backenddb/pg_insert_deposit_to_transfer.sql +++ b/src/backenddb/pg_insert_deposit_to_transfer.sql @@ -114,7 +114,7 @@ SELECT deposit_confirmation_serial -- for the entire deposit confirmation UPDATE merchant_deposit_confirmations SET wire_pending=FALSE - WHERE (deposit_confirmation_serial=decose) + WHERE (deposit_confirmation_serial=my_decose) AND NOT EXISTS (SELECT 1 FROM merchant_deposits md diff --git a/src/backenddb/pg_insert_instance.c b/src/backenddb/pg_insert_instance.c index 6928a094..f1217e29 100644 --- a/src/backenddb/pg_insert_instance.c +++ b/src/backenddb/pg_insert_instance.c @@ -35,14 +35,12 @@ TMH_PG_insert_instance ( const struct TALER_MERCHANTDB_InstanceAuthSettings *ias) { struct PostgresClosure *pg = cls; - uint32_t ut32 = (uint32_t) is->ut; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (merchant_pub), GNUNET_PQ_query_param_auto_from_type (&ias->auth_hash), GNUNET_PQ_query_param_auto_from_type (&ias->auth_salt), GNUNET_PQ_query_param_string (is->id), GNUNET_PQ_query_param_string (is->name), - GNUNET_PQ_query_param_uint32 (&ut32), TALER_PQ_query_param_json (is->address), TALER_PQ_query_param_json (is->jurisdiction), GNUNET_PQ_query_param_bool (is->use_stefan), @@ -76,7 +74,6 @@ TMH_PG_insert_instance ( ",auth_salt" ",merchant_id" ",merchant_name" - ",user_type" ",address" ",jurisdiction" ",use_stefan" @@ -84,9 +81,10 @@ TMH_PG_insert_instance ( ",default_pay_delay" ",website" ",email" - ",logo)" + ",logo" + ",user_type)" "VALUES" - "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)"); + "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 0)"); qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, "insert_instance", params); diff --git a/src/backenddb/pg_insert_product.c b/src/backenddb/pg_insert_product.c index 55db57e9..0d0a8380 100644 --- a/src/backenddb/pg_insert_product.c +++ b/src/backenddb/pg_insert_product.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -30,7 +30,12 @@ enum GNUNET_DB_QueryStatus TMH_PG_insert_product (void *cls, const char *instance_id, const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd) + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *no_instance, + bool *conflict, + ssize_t *no_cat) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -42,36 +47,45 @@ TMH_PG_insert_product (void *cls, GNUNET_PQ_query_param_string (pd->image), TALER_PQ_query_param_json (pd->taxes), TALER_PQ_query_param_amount_with_currency (pg->conn, - &pd->price), + &pd->price), GNUNET_PQ_query_param_uint64 (&pd->total_stock), TALER_PQ_query_param_json (pd->address), GNUNET_PQ_query_param_timestamp (&pd->next_restock), GNUNET_PQ_query_param_uint32 (&pd->minimum_age), + GNUNET_PQ_query_param_array_uint64 (num_cats, + cats, + pg->conn), GNUNET_PQ_query_param_end }; + uint64_t ncat; + bool cats_found = true; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("conflict", + conflict), + GNUNET_PQ_result_spec_bool ("no_instance", + no_instance), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint64 ("no_cat", + &ncat), + &cats_found), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, "insert_product", - "INSERT INTO merchant_inventory" - "(merchant_serial" - ",product_id" - ",description" - ",description_i18n" - ",unit" - ",image" - ",taxes" - ",price" - ",total_stock" - ",address" - ",next_restock" - ",minimum_age" - ")" - " SELECT merchant_serial," - " $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12" - " FROM merchant_instances" - " WHERE merchant_id=$1"); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_product", - params); + "SELECT" + " out_conflict AS conflict" + ",out_no_cat AS no_cat" + ",out_no_instance AS no_instance" + " FROM merchant_do_insert_product" + "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "insert_product", + params, + rs); + GNUNET_PQ_cleanup_query_params_closures (params); + *no_cat = (cats_found) ? -1 : (ssize_t) ncat; + return qs; } diff --git a/src/backenddb/pg_insert_product.h b/src/backenddb/pg_insert_product.h index 169bd150..5e85228e 100644 --- a/src/backenddb/pg_insert_product.h +++ b/src/backenddb/pg_insert_product.h @@ -32,12 +32,24 @@ * @param instance_id instance to insert product for * @param product_id product identifier of product to insert * @param pd the product details to insert + * @param num_cats length of @a cats array + * @param cats array of categories the product is in + * @param[out] no_instance set to true if @a instance_id is unknown + * @param[out] conflict set to true if a conflicting + * product already exists in the database + * @param[out] no_cat set to index of non-existing category from @a cats, or -1 if all @a cats were found * @return database result code */ enum GNUNET_DB_QueryStatus TMH_PG_insert_product (void *cls, const char *instance_id, const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd); + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *no_instance, + bool *conflict, + ssize_t *no_cat); + #endif diff --git a/src/backenddb/pg_insert_product.sql b/src/backenddb/pg_insert_product.sql new file mode 100644 index 00000000..4abf4469 --- /dev/null +++ b/src/backenddb/pg_insert_product.sql @@ -0,0 +1,175 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU 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/> +-- + + +CREATE OR REPLACE FUNCTION merchant_do_insert_product ( + IN in_instance_id TEXT, + IN in_product_id TEXT, + IN in_description TEXT, + IN in_description_i18n BYTEA, + IN in_unit TEXT, + IN in_image TEXT, + IN in_taxes BYTEA, + IN in_price taler_amount_currency, + IN in_total_stock INT8, + IN in_address BYTEA, + IN in_next_restock INT8, + IN in_minimum_age INT4, + IN ina_categories INT8[], + OUT out_no_instance BOOL, + OUT out_conflict BOOL, + OUT out_no_cat INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + my_merchant_id INT8; + my_product_serial INT8; + i INT8; + ini_cat INT8; +BEGIN + +-- Which instance are we using? +SELECT merchant_serial + INTO my_merchant_id + FROM merchant_instances + WHERE merchant_id=in_instance_id; + +IF NOT FOUND +THEN + out_no_instance=TRUE; + out_conflict=FALSE; + out_no_cat=NULL; + RETURN; +END IF; +out_no_instance=FALSE; + +INSERT INTO merchant_inventory + (merchant_serial + ,product_id + ,description + ,description_i18n + ,unit + ,image + ,taxes + ,price + ,total_stock + ,address + ,next_restock + ,minimum_age +) VALUES ( + my_merchant_id + ,in_product_id + ,in_description + ,in_description_i18n + ,in_unit + ,in_image + ,in_taxes + ,in_price + ,in_total_stock + ,in_address + ,in_next_restock + ,in_minimum_age) +ON CONFLICT (merchant_serial, product_id) DO NOTHING + RETURNING product_serial + INTO my_product_serial; + + +IF NOT FOUND +THEN + -- Check for idempotency + SELECT product_serial + INTO my_product_serial + FROM merchant_inventory + WHERE merchant_serial=my_merchant_id + AND product_id=in_product_id + AND description=in_description + AND description_i18n=in_description_i18n + AND unit=in_unit + AND image=in_image + AND taxes=in_taxes + AND price=in_price + AND total_stock=in_total_stock + AND address=in_address + AND next_restock=in_next_restock + AND minimum_age=in_minimum_age; + IF NOT FOUND + THEN + out_conflict=TRUE; + out_no_cat=NULL; + RETURN; + END IF; + + -- Check categories match as well + FOR i IN 1..COALESCE(array_length(ina_categories,1),0) + LOOP + ini_cat=ina_categories[i]; + + PERFORM + FROM merchant_product_categories + WHERE product_serial=my_product_serial + AND category_serial=ini_cat; + IF NOT FOUND + THEN + out_conflict=TRUE; + out_no_cat=NULL; + RETURN; + END IF; + END LOOP; + + -- Also check there are no additional categories + -- in either set. + SELECT COUNT(*) + INTO i + FROM merchant_product_categories + WHERE product_serial=my_product_serial; + IF i != array_length(ina_categories,1) + THEN + out_conflict=TRUE; + out_no_cat=NULL; + RETURN; + END IF; + + -- Is idempotent! + out_conflict=FALSE; + out_no_cat=NULL; + RETURN; +END IF; +out_conflict=FALSE; + + +-- Add categories +FOR i IN 1..COALESCE(array_length(ina_categories,1),0) +LOOP + ini_cat=ina_categories[i]; + + INSERT INTO merchant_product_categories + (product_serial + ,category_serial) + VALUES + (my_product_serial + ,ini_cat) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + out_no_cat=i; + RETURN; + END IF; +END LOOP; + +-- Success! +out_no_cat=NULL; +END $$; diff --git a/src/backenddb/pg_lookup_all_products.c b/src/backenddb/pg_lookup_all_products.c new file mode 100644 index 00000000..31320d59 --- /dev/null +++ b/src/backenddb/pg_lookup_all_products.c @@ -0,0 +1,193 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_lookup_all_products.c + * @brief Implementation of the lookup_all_products function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_products.h" +#include "pg_helper.h" + +/** + * Context used for TMH_PG_lookup_all_products(). + */ +struct LookupProductsContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_ProductCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Postgres context. + */ + struct PostgresClosure *pg; + + /** + * Did database result extraction fail? + */ + bool extract_failed; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about products. + * + * @param[in,out] cls of type `struct LookupProductsContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_products_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupProductsContext *plc = cls; + struct PostgresClosure *pg = plc->pg; + + for (unsigned int i = 0; i < num_results; i++) + { + char *product_id; + uint64_t product_serial; + struct TALER_MERCHANTDB_ProductDetails pd; + size_t num_categories; + uint64_t *categories; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("product_id", + &product_id), + GNUNET_PQ_result_spec_uint64 ("product_serial", + &product_serial), + GNUNET_PQ_result_spec_string ("description", + &pd.description), + TALER_PQ_result_spec_json ("description_i18n", + &pd.description_i18n), + GNUNET_PQ_result_spec_string ("unit", + &pd.unit), + TALER_PQ_result_spec_amount_with_currency ("price", + &pd.price), + TALER_PQ_result_spec_json ("taxes", + &pd.taxes), + GNUNET_PQ_result_spec_uint64 ("total_stock", + &pd.total_stock), + GNUNET_PQ_result_spec_uint64 ("total_sold", + &pd.total_sold), + GNUNET_PQ_result_spec_uint64 ("total_lost", + &pd.total_lost), + GNUNET_PQ_result_spec_string ("image", + &pd.image), + TALER_PQ_result_spec_json ("address", + &pd.address), + GNUNET_PQ_result_spec_timestamp ("next_restock", + &pd.next_restock), + GNUNET_PQ_result_spec_uint32 ("minimum_age", + &pd.minimum_age), + GNUNET_PQ_result_spec_array_uint64 (pg->conn, + "categories", + &num_categories, + &categories), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + plc->extract_failed = true; + return; + } + plc->cb (plc->cb_cls, + product_serial, + product_id, + &pd, + num_categories, + categories); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_all_products (void *cls, + const char *instance_id, + TALER_MERCHANTDB_ProductCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupProductsContext plc = { + .cb = cb, + .cb_cls = cb_cls, + .pg = pg, + /* Can be overwritten by the lookup_products_cb */ + .extract_failed = false, + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_all_products", + "SELECT" + " description" + ",description_i18n" + ",unit" + ",price" + ",taxes" + ",total_stock" + ",total_sold" + ",total_lost" + ",image" + ",merchant_inventory.address" + ",next_restock" + ",minimum_age" + ",product_id" + ",product_serial" + " FROM merchant_inventory" + " JOIN merchant_instances" + " USING (merchant_serial)" + ",LATERAL (" + " SELECT ARRAY (" + " SELECT mpc.category_serial" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = mi.product_serial" + " ) AS category_array" + " ) t" + " WHERE merchant_instances.merchant_id=$1"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "lookup_all_products", + params, + &lookup_products_cb, + &plc); + /* If there was an error inside lookup_products_cb, return a hard error. */ + if (plc.extract_failed) + return GNUNET_DB_STATUS_HARD_ERROR; + return qs; +} diff --git a/src/backenddb/pg_lookup_all_products.h b/src/backenddb/pg_lookup_all_products.h new file mode 100644 index 00000000..390fa60d --- /dev/null +++ b/src/backenddb/pg_lookup_all_products.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_lookup_all_products.h + * @brief implementation of the lookup_all_products function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_LOOKUP_ALL_PRODUCTS_H +#define PG_LOOKUP_ALL_PRODUCTS_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Lookup all of the products the given instance has configured. + * + * @param cls closure + * @param instance_id instance to lookup products for + * @param offset transfer_serial number of the transfer we want to offset from + * @param limit number of entries to return, negative for descending, + * positive for ascending + * @param cb function to call on all products found + * @param cb_cls closure for @a cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_all_products (void *cls, + const char *instance_id, + TALER_MERCHANTDB_ProductCallback cb, + void *cb_cls); + +#endif diff --git a/src/backenddb/pg_lookup_categories.c b/src/backenddb/pg_lookup_categories.c new file mode 100644 index 00000000..9aea5477 --- /dev/null +++ b/src/backenddb/pg_lookup_categories.c @@ -0,0 +1,148 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_lookup_categories.c + * @brief Implementation of the lookup_categories function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_categories.h" +#include "pg_helper.h" + + +/** + * Context used for TMH_PG_lookup_categories(). + */ +struct LookupCategoryContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_CategoriesCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Did database result extraction fail? + */ + bool extract_failed; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about otp_device. + * + * @param[in,out] cls of type `struct LookupCategoryContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_categories_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupCategoryContext *tlc = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + uint64_t category_id; + char *category_name; + json_t *category_name_i18n; + uint64_t product_count; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("category_serial", + &category_id), + GNUNET_PQ_result_spec_string ("category_name", + &category_name), + TALER_PQ_result_spec_json ("category_name_i18n", + &category_name_i18n), + GNUNET_PQ_result_spec_uint64 ("product_count", + &product_count), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + tlc->extract_failed = true; + return; + } + tlc->cb (tlc->cb_cls, + category_id, + category_name, + category_name_i18n, + product_count); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_categories (void *cls, + const char *instance_id, + TALER_MERCHANTDB_CategoriesCallback cb, + void *cb_cls) +{ + + struct PostgresClosure *pg = cls; + struct LookupCategoryContext tlc = { + .cb = cb, + .cb_cls = cb_cls, + /* Can be overwritten by the lookup_categories_cb */ + .extract_failed = false, + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_categories", + "SELECT" + " mc.category_serial" + ",mc.category_name" + ",mc.category_name_i18n" + ",COALESCE(COUNT(mpc.product_serial),0)" + " AS product_count" + " FROM merchant_categories mc" + " JOIN merchant_product_categories mpc" + " JOIN merchant_instances mi" + " USING (merchant_serial)" + " WHERE mi.merchant_id=$1" + " GROUP BY mc.category_serial" + " ORDER BY mc.category_serial;"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "lookup_categories", + params, + &lookup_categories_cb, + &tlc); + /* If there was an error inside lookup_categories_cb, return a hard error. */ + if (tlc.extract_failed) + return GNUNET_DB_STATUS_HARD_ERROR; + return qs; +} diff --git a/src/backenddb/pg_lookup_categories.h b/src/backenddb/pg_lookup_categories.h new file mode 100644 index 00000000..500295c0 --- /dev/null +++ b/src/backenddb/pg_lookup_categories.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_lookup_categories.h + * @brief implementation of the lookup_categories function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_LOOKUP_CATEGORIES_H +#define PG_LOOKUP_CATEGORIES_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Lookup all of the product categories the given instance has configured. + * + * @param cls closure + * @param instance_id instance to lookup OTP devices for + * @param cb function to call on all categories found + * @param cb_cls closure for @a cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_categories (void *cls, + const char *instance_id, + TALER_MERCHANTDB_CategoriesCallback cb, + void *cb_cls); + +#endif diff --git a/src/backenddb/pg_lookup_instances.c b/src/backenddb/pg_lookup_instances.c index 323b1957..f4f820ac 100644 --- a/src/backenddb/pg_lookup_instances.c +++ b/src/backenddb/pg_lookup_instances.c @@ -173,7 +173,6 @@ lookup_instances_cb (void *cls, { bool no_auth; bool no_salt; - uint32_t ut32; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ("merchant_serial", &lic->instance_serial), @@ -191,8 +190,6 @@ lookup_instances_cb (void *cls, &lic->is.id), GNUNET_PQ_result_spec_string ("merchant_name", &lic->is.name), - GNUNET_PQ_result_spec_uint32 ("user_type", - &ut32), TALER_PQ_result_spec_json ("address", &lic->is.address), TALER_PQ_result_spec_json ("jurisdiction", @@ -200,7 +197,8 @@ lookup_instances_cb (void *cls, GNUNET_PQ_result_spec_bool ("use_stefan", &lic->is.use_stefan), GNUNET_PQ_result_spec_relative_time ("default_wire_transfer_delay", - &lic->is.default_wire_transfer_delay), + &lic->is.default_wire_transfer_delay) + , GNUNET_PQ_result_spec_relative_time ("default_pay_delay", &lic->is.default_pay_delay), GNUNET_PQ_result_spec_allow_null ( @@ -233,7 +231,6 @@ lookup_instances_cb (void *cls, lic->qs = GNUNET_DB_STATUS_HARD_ERROR; return; } - lic->is.ut = (enum TALER_KYCLOGIC_KycUserType) ut32; call_cb (lic); GNUNET_PQ_cleanup_result (rs); if (0 > lic->qs) @@ -270,7 +267,6 @@ TMH_PG_lookup_instances (void *cls, ",auth_salt" ",merchant_id" ",merchant_name" - ",user_type" ",address" ",jurisdiction" ",use_stefan" diff --git a/src/backenddb/pg_lookup_product.c b/src/backenddb/pg_lookup_product.c index 65a3c0e5..2948e6ca 100644 --- a/src/backenddb/pg_lookup_product.c +++ b/src/backenddb/pg_lookup_product.c @@ -29,7 +29,9 @@ enum GNUNET_DB_QueryStatus TMH_PG_lookup_product (void *cls, const char *instance_id, const char *product_id, - struct TALER_MERCHANTDB_ProductDetails *pd) + struct TALER_MERCHANTDB_ProductDetails *pd, + size_t *num_categories, + uint64_t **categories) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -38,6 +40,35 @@ TMH_PG_lookup_product (void *cls, GNUNET_PQ_query_param_end }; + PREPARE (pg, + "lookup_product", + "SELECT" + " mi.description" + ",mi.description_i18n" + ",mi.unit" + ",mi.price" + ",mi.taxes" + ",mi.total_stock" + ",mi.total_sold" + ",mi.total_lost" + ",mi.image" + ",mi.address" + ",mi.next_restock" + ",mi.minimum_age" + ",t.category_array AS categories" + " FROM merchant_inventory mi" + " JOIN merchant_instances inst" + " USING (merchant_serial)" + ",LATERAL (" + " SELECT ARRAY (" + " SELECT mpc.category_serial" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = mi.product_serial" + " ) AS category_array" + " ) t" + " WHERE inst.merchant_id=$1" + " AND mi.product_id=$2" + ); if (NULL == pd) { struct GNUNET_PQ_ResultSpec rs_null[] = { @@ -60,7 +91,7 @@ TMH_PG_lookup_product (void *cls, GNUNET_PQ_result_spec_string ("unit", &pd->unit), TALER_PQ_result_spec_amount_with_currency ("price", - &pd->price), + &pd->price), TALER_PQ_result_spec_json ("taxes", &pd->taxes), GNUNET_PQ_result_spec_uint64 ("total_stock", @@ -77,30 +108,14 @@ TMH_PG_lookup_product (void *cls, &pd->next_restock), GNUNET_PQ_result_spec_uint32 ("minimum_age", &pd->minimum_age), + GNUNET_PQ_result_spec_array_uint64 (pg->conn, + "categories", + num_categories, + categories), GNUNET_PQ_result_spec_end }; check_connection (pg); - PREPARE (pg, - "lookup_product", - "SELECT" - " description" - ",description_i18n" - ",unit" - ",price" - ",taxes" - ",total_stock" - ",total_sold" - ",total_lost" - ",image" - ",merchant_inventory.address" - ",next_restock" - ",minimum_age" - " FROM merchant_inventory" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND merchant_inventory.product_id=$2"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "lookup_product", params, diff --git a/src/backenddb/pg_lookup_product.h b/src/backenddb/pg_lookup_product.h index a6add4cb..7346cc0d 100644 --- a/src/backenddb/pg_lookup_product.h +++ b/src/backenddb/pg_lookup_product.h @@ -33,12 +33,17 @@ * @param product_id product to lookup * @param[out] pd set to the product details on success, can be NULL * (in that case we only want to check if the product exists) + * @param[out] num_categories set to length of @a categories array + * @param[out] categories set to array of categories the + * product is in, caller must free() it. * @return database result code */ enum GNUNET_DB_QueryStatus TMH_PG_lookup_product (void *cls, const char *instance_id, const char *product_id, - struct TALER_MERCHANTDB_ProductDetails *pd); + struct TALER_MERCHANTDB_ProductDetails *pd, + size_t *num_categories, + uint64_t **categories); #endif diff --git a/src/backenddb/pg_select_category.c b/src/backenddb/pg_select_category.c new file mode 100644 index 00000000..c20d7bb7 --- /dev/null +++ b/src/backenddb/pg_select_category.c @@ -0,0 +1,37 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_select_category.c + * @brief Implementation of the select_category function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_select_category.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TMH_PG_select_category (void *cls, + const char *instance_id, + uint64_t category_id, + struct TALER_MERCHANTDB_CategoryDetails *cd) +{ + GNUNET_break (0); // FIXME + return GNUNET_DB_STATUS_HARD_ERROR; +} diff --git a/src/backenddb/pg_select_category.h b/src/backenddb/pg_select_category.h new file mode 100644 index 00000000..9eeb14aa --- /dev/null +++ b/src/backenddb/pg_select_category.h @@ -0,0 +1,45 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_select_category.h + * @brief implementation of the select_category function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_SELECT_CATEGORY_H +#define PG_SELECT_CATEGORY_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + + +/** + * Lookup details about product category. + * + * @param cls closure + * @param instance_id instance to lookup template for + * @param category_id category to update + * @param[out] cd set to the category details on success, can be NULL + * (in that case we only want to check if the category exists) + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_select_category (void *cls, + const char *instance_id, + uint64_t category_id, + struct TALER_MERCHANTDB_CategoryDetails *cd); + +#endif diff --git a/src/backenddb/pg_select_category_by_name.c b/src/backenddb/pg_select_category_by_name.c new file mode 100644 index 00000000..9c2aa784 --- /dev/null +++ b/src/backenddb/pg_select_category_by_name.c @@ -0,0 +1,68 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_select_category_by_name.c + * @brief Implementation of the select_category_by_name function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_select_category_by_name.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TMH_PG_select_category_by_name (void *cls, + const char *instance_id, + const char *category_name, + json_t **name_i18n, + uint64_t *category_id) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_string (category_name), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("category_serial", + category_id), + TALER_PQ_result_spec_json ("category_name_i18n", + name_i18n), + GNUNET_PQ_result_spec_end + }; + + PREPARE (pg, + "select_category_by_name", + "SELECT" + " category_serial" + ",category_name_i18n" + " FROM merchant_categories mc" + " JOIN merchant_instances mi" + " USING (merchant_serial)" + " WHERE mi.merchant_id=$1" + " AND mc.category_name=$2"); + + + check_connection (pg); + return GNUNET_PQ_eval_prepared_singleton_select ( + pg->conn, + "select_category_by_name", + params, + rs); +} diff --git a/src/backenddb/pg_select_category_by_name.h b/src/backenddb/pg_select_category_by_name.h new file mode 100644 index 00000000..4f582828 --- /dev/null +++ b/src/backenddb/pg_select_category_by_name.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_select_category_by_name.h + * @brief implementation of the select_category_by_name function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_SELECT_CATEGORY_BY_NAME_H +#define PG_SELECT_CATEGORY_BY_NAME_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + + +/** + * Lookup details about product category by name. + * + * @param cls closure + * @param instance_id instance to lookup template for + * @param category_name category name to look for + * @param[out] name_i18n category name translation + * @param[out] category_id category ID + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_select_category_by_name (void *cls, + const char *instance_id, + const char *category_name, + json_t **name_i18n, + uint64_t *category_id); + +#endif diff --git a/src/backenddb/pg_update_category.c b/src/backenddb/pg_update_category.c new file mode 100644 index 00000000..3b07a266 --- /dev/null +++ b/src/backenddb/pg_update_category.c @@ -0,0 +1,59 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_update_category.c + * @brief Implementation of the update_category function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_update_category.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TMH_PG_update_category (void *cls, + const char *instance_id, + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_uint64 (&category_id), + GNUNET_PQ_query_param_string (category_name), + TALER_PQ_query_param_json (category_name_i18n), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + PREPARE (pg, + "update_category", + "UPDATE merchant_categories SET" + " category_name=$3" + ",category_name_i18n=$4" + " WHERE merchant_serial=" + " (SELECT merchant_serial" + " FROM merchant_instances" + " WHERE merchant_id=$1)" + " AND category_serial=$2"); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "update_category", + params); +} diff --git a/src/backenddb/pg_update_category.h b/src/backenddb/pg_update_category.h new file mode 100644 index 00000000..616a714a --- /dev/null +++ b/src/backenddb/pg_update_category.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 backenddb/pg_update_category.h + * @brief implementation of the update_category function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_UPDATE_CATEGORY_H +#define PG_UPDATE_CATEGORY_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + + +/** + * Update descriptions of a product category. + * + * @param cls closure + * @param instance_id instance to update OTP device for + * @param category_id category to update + * @param category_name name of the category + * @param category_name_i18n translations of the category name + * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the template + * does not yet exist. + */ +enum GNUNET_DB_QueryStatus +TMH_PG_update_category (void *cls, + const char *instance_id, + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n); + +#endif diff --git a/src/backenddb/pg_update_instance.c b/src/backenddb/pg_update_instance.c index 228e2031..91026638 100644 --- a/src/backenddb/pg_update_instance.c +++ b/src/backenddb/pg_update_instance.c @@ -32,7 +32,6 @@ TMH_PG_update_instance (void *cls, const struct TALER_MERCHANTDB_InstanceSettings *is) { struct PostgresClosure *pg = cls; - uint32_t ut32 = (uint32_t) is->ut; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (is->id), GNUNET_PQ_query_param_string (is->name), @@ -52,7 +51,6 @@ TMH_PG_update_instance (void *cls, (NULL == is->logo) ? GNUNET_PQ_query_param_null () : GNUNET_PQ_query_param_string (is->logo), - GNUNET_PQ_query_param_uint32 (&ut32), GNUNET_PQ_query_param_end }; @@ -69,7 +67,6 @@ TMH_PG_update_instance (void *cls, ",website=$8" ",email=$9" ",logo=$10" - ",user_type=$11" " WHERE merchant_id=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, "update_instance", diff --git a/src/backenddb/pg_update_otp.c b/src/backenddb/pg_update_otp.c index bdcb9624..218ae74e 100644 --- a/src/backenddb/pg_update_otp.c +++ b/src/backenddb/pg_update_otp.c @@ -26,17 +26,6 @@ #include "pg_helper.h" -/** - * Update details about a particular OTP device. - * - * @param cls closure - * @param instance_id instance to update OTP device for - * @param otp_id OTP device to update - * @param td update to the OTP device details on success, can be NULL - * (in that case we only want to check if the template exists) - * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the template - * does not yet exist. - */ enum GNUNET_DB_QueryStatus TMH_PG_update_otp (void *cls, const char *instance_id, @@ -52,7 +41,7 @@ TMH_PG_update_otp (void *cls, GNUNET_PQ_query_param_uint32 (&pos32), GNUNET_PQ_query_param_uint64 (&td->otp_ctr), (NULL == td->otp_key) - ? GNUNET_PQ_query_param_null() + ? GNUNET_PQ_query_param_null () : GNUNET_PQ_query_param_string (td->otp_key), GNUNET_PQ_query_param_end }; @@ -74,5 +63,3 @@ TMH_PG_update_otp (void *cls, "update_otp", params); } - - diff --git a/src/backenddb/pg_update_product.c b/src/backenddb/pg_update_product.c index cd7c1857..d7b63b82 100644 --- a/src/backenddb/pg_update_product.c +++ b/src/backenddb/pg_update_product.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -26,11 +26,20 @@ #include "pg_update_product.h" #include "pg_helper.h" + enum GNUNET_DB_QueryStatus TMH_PG_update_product (void *cls, const char *instance_id, const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd) + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *no_instance, + ssize_t *no_cat, + bool *no_product, + bool *lost_reduced, + bool *sold_reduced, + bool *stocked_reduced) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -42,14 +51,37 @@ TMH_PG_update_product (void *cls, GNUNET_PQ_query_param_string (pd->image), /* $6 */ TALER_PQ_query_param_json (pd->taxes), TALER_PQ_query_param_amount_with_currency (pg->conn, - &pd->price), /* $8 */ + &pd->price), /* $8 */ GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $9 */ GNUNET_PQ_query_param_uint64 (&pd->total_lost), TALER_PQ_query_param_json (pd->address), GNUNET_PQ_query_param_timestamp (&pd->next_restock), GNUNET_PQ_query_param_uint32 (&pd->minimum_age), + GNUNET_PQ_query_param_array_uint64 (num_cats, + cats, + pg->conn), GNUNET_PQ_query_param_end }; + uint64_t ncat; + bool cats_found = true; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("no_instance", + no_instance), + GNUNET_PQ_result_spec_bool ("no_product", + no_product), + GNUNET_PQ_result_spec_bool ("lost_reduced", + lost_reduced), + GNUNET_PQ_result_spec_bool ("sold_reduced", + sold_reduced), + GNUNET_PQ_result_spec_bool ("stocked_reduced", + stocked_reduced), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint64 ("no_cat", + &ncat), + &cats_found), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; if ( (pd->total_stock < pd->total_lost + pd->total_sold) || (pd->total_lost < pd->total_lost @@ -61,26 +93,20 @@ TMH_PG_update_product (void *cls, check_connection (pg); PREPARE (pg, "update_product", - "UPDATE merchant_inventory SET" - " description=$3" - ",description_i18n=$4" - ",unit=$5" - ",image=$6" - ",taxes=$7" - ",price=$8" - ",total_stock=$9" - ",total_lost=$10" - ",address=$11" - ",next_restock=$12" - ",minimum_age=$13" - " WHERE merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND product_id=$2" - " AND total_stock <= $9" - " AND total_lost <= $10"); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_product", - params); + "SELECT" + " out_lost_reduced AS lost_reduced" + ",out_sold_reduced AS sold_reduced" + ",out_stocked_reduced AS stocked_reduced" + ",out_no_product AS no_product" + ",out_no_cat AS no_cat" + ",out_no_instance AS no_instance" + " FROM merchant_do_update_product" + "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "update_product", + params, + rs); + GNUNET_PQ_cleanup_query_params_closures (params); + *no_cat = (cats_found) ? -1 : (ssize_t) ncat; + return qs; } diff --git a/src/backenddb/pg_update_product.h b/src/backenddb/pg_update_product.h index 3ad280ef..ffba1073 100644 --- a/src/backenddb/pg_update_product.h +++ b/src/backenddb/pg_update_product.h @@ -28,26 +28,40 @@ /** * Update details about a particular product. Note that the * transaction must enforce that the sold/stocked/lost counters - * are not reduced (i.e. by expanding the WHERE clause on the existing - * values). + * are not reduced. * * @param cls closure * @param instance_id instance to lookup products for * @param product_id product to lookup + * @param num_cats length of @a cats array + * @param cats number of categories the product is in + * @param[out] no_instance the update failed as the instance is unknown + * @param[out] no_cat set to -1 on success, otherwise the update failed and this is set + * to the index of a category in @a cats that is unknown + * @param[out] no_product the @a product_id is unknown + * @param[out] lost_reduced the update failed as the counter of units lost would have been lowered + * @param[out] sold_reduced the update failed as the counter of units sold would have been lowered + * @param[out] stocked_reduced the update failed as the counter of units stocked would have been lowered * @param[out] pd set to the product details on success, can be NULL * (in that case we only want to check if the product exists) * total_sold in @a pd is ignored, total_lost must not * exceed total_stock minus the existing total_sold; * total_sold and total_stock must be larger or equal to * the existing value; - * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the - * non-decreasing constraints are not met *or* if the product - * does not yet exist. + * @return database result code */ enum GNUNET_DB_QueryStatus TMH_PG_update_product (void *cls, const char *instance_id, const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd); + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *no_instance, + ssize_t *no_cat, + bool *no_product, + bool *lost_reduced, + bool *sold_reduced, + bool *stocked_reduced); #endif diff --git a/src/backenddb/pg_update_product.sql b/src/backenddb/pg_update_product.sql new file mode 100644 index 00000000..6b5a416b --- /dev/null +++ b/src/backenddb/pg_update_product.sql @@ -0,0 +1,139 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU 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/> +-- + + +CREATE OR REPLACE FUNCTION merchant_do_update_product ( + IN in_instance_id TEXT, + IN in_product_id TEXT, + IN in_description TEXT, + IN in_description_i18n BYTEA, + IN in_unit TEXT, + IN in_image TEXT, + IN in_taxes BYTEA, + IN in_price taler_amount_currency, + IN in_total_stock INT8, + IN in_total_lost INT8, + IN in_address BYTEA, + IN in_next_restock INT8, + IN in_minimum_age INT4, + IN ina_categories INT8[], + OUT out_no_instance BOOL, + OUT out_no_product BOOL, + OUT out_lost_reduced BOOL, + OUT out_sold_reduced BOOL, + OUT out_stocked_reduced BOOL, + OUT out_no_cat INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + my_merchant_id INT8; + my_product_serial INT8; + i INT8; + ini_cat INT8; + rec RECORD; +BEGIN + +out_no_instance=FALSE; +out_no_product=FALSE; +out_lost_reduced=FALSE; +out_sold_reduced=FALSE; -- We currently don't allow updating 'sold', hence always FALSE +out_stocked_reduced=FALSE; +out_no_cat=NULL; + +-- Which instance are we using? +SELECT merchant_serial + INTO my_merchant_id + FROM merchant_instances + WHERE merchant_id=in_instance_id; + +IF NOT FOUND +THEN + out_no_instance=TRUE; + RETURN; +END IF; + +-- Check existing entry satisfies constraints +SELECT total_stock + ,total_lost + ,product_serial + INTO rec + FROM merchant_inventory + WHERE merchant_serial=my_merchant_id + AND product_id=in_product_id; + +IF NOT FOUND +THEN + out_no_product=TRUE; + RETURN; +END IF; + +my_product_serial = rec.product_serial; + +IF rec.total_stock > in_total_stock +THEN + out_stocked_reduced=TRUE; + RETURN; +END IF; + +IF rec.total_lost > in_total_lost +THEN + out_lost_reduced=TRUE; + RETURN; +END IF; + +-- Remove old categories +DELETE FROM merchant_product_categories + WHERE product_serial=my_product_serial; + +-- Add new categories +FOR i IN 1..COALESCE(array_length(ina_categories,1),0) +LOOP + ini_cat=ina_categories[i]; + + INSERT INTO merchant_product_categories + (product_serial + ,category_serial) + VALUES + (my_product_serial + ,ini_cat) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + out_no_cat=i; + RETURN; + END IF; +END LOOP; + +UPDATE merchant_inventory SET + description=in_description + ,description_i18n=in_description_i18n + ,unit=in_unit + ,image=in_image + ,taxes=in_taxes + ,price=in_price + ,total_stock=in_total_stock + ,total_lost=in_total_lost + ,address=in_address + ,next_restock=in_next_restock + ,minimum_age=in_minimum_age + WHERE merchant_serial=my_merchant_id + AND product_serial=my_product_serial; -- could also match on product_id + +ASSERT FOUND,'SELECTED it earlier, should UPDATE it now'; + +-- Success! +END $$; diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c index a886acaf..2f499994 100644 --- a/src/backenddb/plugin_merchantdb_postgres.c +++ b/src/backenddb/plugin_merchantdb_postgres.c @@ -44,6 +44,11 @@ #include "pg_lookup_instances.h" #include "pg_lookup_transfers.h" #include "pg_lookup_pending_deposits.h" +#include "pg_lookup_categories.h" +#include "pg_select_category.h" +#include "pg_update_category.h" +#include "pg_insert_category.h" +#include "pg_delete_category.h" #include "pg_update_wirewatch_progress.h" #include "pg_select_wirewatch_accounts.h" #include "pg_select_open_transfers.h" @@ -57,12 +62,14 @@ #include "pg_account_kyc_set_status.h" #include "pg_account_kyc_get_status.h" #include "pg_delete_instance_private_key.h" +#include "pg_select_category_by_name.h" #include "pg_purge_instance.h" #include "pg_update_instance.h" #include "pg_update_instance_auth.h" #include "pg_inactivate_account.h" #include "pg_activate_account.h" #include "pg_lookup_products.h" +#include "pg_lookup_all_products.h" #include "pg_lookup_product.h" #include "pg_delete_product.h" #include "pg_insert_product.h" @@ -410,6 +417,8 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_update_transfer_status; plugin->lookup_products = &TMH_PG_lookup_products; + plugin->lookup_all_products + = &TMH_PG_lookup_all_products; plugin->lookup_product = &TMH_PG_lookup_product; plugin->delete_product @@ -564,6 +573,18 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_insert_pending_webhook; plugin->update_pending_webhook = &TMH_PG_update_pending_webhook; + plugin->lookup_categories + = &TMH_PG_lookup_categories; + plugin->select_category_by_name + = &TMH_PG_select_category_by_name; + plugin->select_category + = &TMH_PG_select_category; + plugin->update_category + = &TMH_PG_update_category; + plugin->insert_category + = &TMH_PG_insert_category; + plugin->delete_category + = &TMH_PG_delete_category; plugin->delete_exchange_accounts = &TMH_PG_delete_exchange_accounts; plugin->select_accounts_by_exchange diff --git a/src/backenddb/procedures.sql.in b/src/backenddb/procedures.sql.in index 3ebf8b8b..9afa246b 100644 --- a/src/backenddb/procedures.sql.in +++ b/src/backenddb/procedures.sql.in @@ -19,6 +19,8 @@ BEGIN; SET search_path TO merchant; #include "pg_insert_deposit_to_transfer.sql" +#include "pg_insert_product.sql" #include "pg_insert_transfer_details.sql" +#include "pg_update_product.sql" COMMIT; diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c index f113b01f..3591a133 100644 --- a/src/backenddb/test_merchantdb.c +++ b/src/backenddb/test_merchantdb.c @@ -797,20 +797,48 @@ check_products_equal (const struct TALER_MERCHANTDB_ProductDetails *a, * * @param instance the instance to insert the product for. * @param product the product data to insert. + * @param num_cats length of the @a cats array + * @param cats array of categories for the product * @param expected_result the result we expect the db to return. + * @param expect_conflict expected conflict status + * @param expect_no_instance expected instance missing status + * @param expected_no_cat expected category missing index * @return 0 when successful, 1 otherwise. */ static int test_insert_product (const struct InstanceData *instance, const struct ProductData *product, - enum GNUNET_DB_QueryStatus expected_result) + unsigned int num_cats, + const uint64_t *cats, + enum GNUNET_DB_QueryStatus expected_result, + bool expect_conflict, + bool expect_no_instance, + ssize_t expected_no_cat) { + bool conflict; + bool no_instance; + ssize_t no_cat; + TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_product (plugin->cls, instance->instance.id, product->id, - &product->product), + &product->product, + num_cats, + cats, + &no_instance, + &conflict, + &no_cat), "Insert product failed\n"); + if (expected_result > 0) + { + TEST_COND_RET_ON_FAIL (no_instance == expect_no_instance, + "No instance wrong"); + TEST_COND_RET_ON_FAIL (conflict == expect_conflict, + "Conflict wrong"); + TEST_COND_RET_ON_FAIL (no_cat == expected_no_cat, + "Wrong category missing returned"); + } return 0; } @@ -826,14 +854,54 @@ test_insert_product (const struct InstanceData *instance, static int test_update_product (const struct InstanceData *instance, const struct ProductData *product, - enum GNUNET_DB_QueryStatus expected_result) -{ - TEST_COND_RET_ON_FAIL (expected_result == - plugin->update_product (plugin->cls, - instance->instance.id, - product->id, - &product->product), - "Update product failed\n"); + unsigned int num_cats, + const uint64_t *cats, + enum GNUNET_DB_QueryStatus expected_result, + bool expect_no_instance, + bool expect_no_product, + bool expect_lost_reduced, + bool expect_sold_reduced, + bool expect_stocked_reduced, + ssize_t expected_no_cat) + +{ + bool no_instance; + ssize_t no_cat; + bool no_product; + bool lost_reduced; + bool sold_reduced; + bool stocked_reduced; + + TEST_COND_RET_ON_FAIL ( + expected_result == + plugin->update_product (plugin->cls, + instance->instance.id, + product->id, + &product->product, + num_cats, + cats, + &no_instance, + &no_cat, + &no_product, + &lost_reduced, + &sold_reduced, + &stocked_reduced), + "Update product failed\n"); + if (expected_result > 0) + { + TEST_COND_RET_ON_FAIL (no_instance == expect_no_instance, + "No instance wrong"); + TEST_COND_RET_ON_FAIL (no_product == expect_no_product, + "No product wrong"); + TEST_COND_RET_ON_FAIL (lost_reduced == expect_lost_reduced, + "No product wrong"); + TEST_COND_RET_ON_FAIL (stocked_reduced == expect_stocked_reduced, + "Stocked reduced wrong"); + TEST_COND_RET_ON_FAIL (sold_reduced == expect_sold_reduced, + "Sold reduced wrong"); + TEST_COND_RET_ON_FAIL (no_cat == expected_no_cat, + "Wrong category missing returned"); + } return 0; } @@ -850,25 +918,34 @@ test_lookup_product (const struct InstanceData *instance, const struct ProductData *product) { struct TALER_MERCHANTDB_ProductDetails lookup_result; + size_t num_categories = 0; + uint64_t *categories = NULL; + if (0 > plugin->lookup_product (plugin->cls, instance->instance.id, product->id, - &lookup_result)) + &lookup_result, + &num_categories, + &categories)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup product failed\n"); TALER_MERCHANTDB_product_details_free (&lookup_result); return 1; } - const struct TALER_MERCHANTDB_ProductDetails *to_cmp = &product->product; - if (0 != check_products_equal (&lookup_result, - to_cmp)) + GNUNET_free (categories); { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Lookup product failed: incorrect product returned\n"); - TALER_MERCHANTDB_product_details_free (&lookup_result); - return 1; + const struct TALER_MERCHANTDB_ProductDetails *to_cmp = &product->product; + + if (0 != check_products_equal (&lookup_result, + to_cmp)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Lookup product failed: incorrect product returned\n"); + TALER_MERCHANTDB_product_details_free (&lookup_result); + return 1; + } } TALER_MERCHANTDB_product_details_free (&lookup_result); return 0; @@ -1076,31 +1153,67 @@ run_test_products (struct TestProducts_Closure *cls) /* Test that insert without an instance fails */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[0], - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + true, + -1)); /* Insert the instance */ TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test inserting a product */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[0], - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); - /* Test that double insert fails */ + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + -1)); + /* Test that double insert succeeds */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[0], - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + -1)); + /* Test that conflicting insert fails */ + { + uint64_t cat = 42; + + TEST_RET_ON_FAIL (test_insert_product (&cls->instance, + &cls->products[0], + 1, + &cat, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + true, + false, + -1)); + } /* Test lookup of individual products */ TEST_RET_ON_FAIL (test_lookup_product (&cls->instance, &cls->products[0])); /* Make sure it fails correctly for products that don't exist */ - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != - plugin->lookup_product (plugin->cls, - cls->instance.instance.id, - "nonexistent_product", - NULL)) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Lookup product failed\n"); - return 1; + size_t num_categories = 0; + uint64_t *categories = NULL; + + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != + plugin->lookup_product (plugin->cls, + cls->instance.instance.id, + "nonexistent_product", + NULL, + &num_categories, + &categories)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Lookup product failed\n"); + return 1; + } + GNUNET_free (categories); } /* Test product update */ cls->products[0].product.description = @@ -1126,37 +1239,76 @@ run_test_products (struct TestProducts_Closure *cls) json_array_append_new (cls->products[0].product.address, json_string ("444 Some Street"))); cls->products[0].product.next_restock = GNUNET_TIME_timestamp_get (); - TEST_RET_ON_FAIL (test_update_product (&cls->instance, - &cls->products[0], - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + TEST_RET_ON_FAIL (test_update_product ( + &cls->instance, + &cls->products[0], + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + false, + false, + false, + -1)); { struct ProductData stock_dec = cls->products[0]; stock_dec.product.total_stock = 40; - TEST_RET_ON_FAIL (test_update_product (&cls->instance, - &stock_dec, - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)) - ; + TEST_RET_ON_FAIL (test_update_product ( + &cls->instance, + &stock_dec, + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + false, + false, + true, + -1)); } { struct ProductData lost_dec = cls->products[0]; lost_dec.product.total_lost = 1; - TEST_RET_ON_FAIL (test_update_product (&cls->instance, - &lost_dec, - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)) - ; + TEST_RET_ON_FAIL (test_update_product ( + &cls->instance, + &lost_dec, + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + true, + false, + false, + -1)); } TEST_RET_ON_FAIL (test_lookup_product (&cls->instance, &cls->products[0])); - TEST_RET_ON_FAIL (test_update_product (&cls->instance, - &cls->products[1], - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + TEST_RET_ON_FAIL (test_update_product ( + &cls->instance, + &cls->products[1], + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + true, + false, + false, + false, + -1)); /* Test collective product lookup */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[1], - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + -1)); TEST_RET_ON_FAIL (test_lookup_products (&cls->instance, 2, cls->products)); @@ -2061,7 +2213,12 @@ run_test_orders (struct TestOrders_Closure *cls) /* Test order lock */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->product, - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + -1)); if (1 != plugin->insert_order_lock (plugin->cls, cls->instance.instance.id, cls->orders[0].id, diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h index 24f5e61c..dc79303f 100644 --- a/src/include/taler_merchant_service.h +++ b/src/include/taler_merchant_service.h @@ -470,11 +470,6 @@ struct TALER_MERCHANT_InstanceInformation */ const json_t *payment_targets; - /** - * User type for the instance. - */ - enum TALER_KYCLOGIC_KycUserType ut; - }; @@ -602,7 +597,6 @@ TALER_MERCHANT_instances_post ( const char *backend_url, const char *instance_id, const char *name, - enum TALER_KYCLOGIC_KycUserType ut, const json_t *address, const json_t *jurisdiction, bool use_stefan, @@ -668,7 +662,6 @@ TALER_MERCHANT_instance_patch ( const char *backend_url, const char *instance_id, const char *name, - enum TALER_KYCLOGIC_KycUserType ut, const json_t *address, const json_t *jurisdiction, bool use_stefan, @@ -790,10 +783,6 @@ struct TALER_MERCHANT_InstanceDetails */ struct GNUNET_TIME_Relative default_pay_delay; - /** - * User type for the instance. - */ - enum TALER_KYCLOGIC_KycUserType ut; }; @@ -1516,12 +1505,12 @@ struct TALER_MERCHANT_ProductGetResponse struct TALER_Amount price; /** - * base64-encoded product image + * base64-encoded product image, can be NULL if none is set. */ const char *image; /** - * list of taxes paid by the merchant + * list of taxes paid by the merchant, can be NULL if no taxes were specified. */ const json_t *taxes; @@ -1544,7 +1533,7 @@ struct TALER_MERCHANT_ProductGetResponse uint64_t total_lost; /** - * where the product is in stock + * where the product is in stock, can be NULL if no location was given. */ const json_t *location; @@ -1707,6 +1696,54 @@ TALER_MERCHANT_products_post2 ( /** + * Make a POST /products request to add a product to the + * inventory. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend + * @param product_id identifier to use for the product + * @param description description of the product + * @param description_i18n Map from IETF BCP 47 language tags to localized descriptions + * @param unit unit in which the product is measured (liters, kilograms, packages, etc.) + * @param price the price for one @a unit of the product, zero is used to imply that + * this product is not sold separately or that the price is not fixed and + * must be supplied by the front-end. If non-zero, price must include + * applicable taxes. + * @param image base64-encoded product image + * @param taxes list of taxes paid by the merchant + * @param total_stock in @a units, -1 to indicate "infinite" (i.e. electronic books) + * @param address where the product is in stock + * @param next_restock when the next restocking is expected to happen, 0 for unknown, + * #GNUNET_TIME_UNIT_FOREVER_ABS for 'never'. + * @param minimum_age minimum age the buyer must have + * @param num_cats length of the @a cats array + * @param cats array of categories the product is in + * @param cb function to call with the backend's result + * @param cb_cls closure for @a cb + * @return the request handle; NULL upon error + */ +struct TALER_MERCHANT_ProductsPostHandle * +TALER_MERCHANT_products_post3 ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *product_id, + const char *description, + const json_t *description_i18n, + const char *unit, + const struct TALER_Amount *price, + const char *image, + const json_t *taxes, + int64_t total_stock, + const json_t *address, + struct GNUNET_TIME_Timestamp next_restock, + uint32_t minimum_age, + unsigned int num_cats, + const uint64_t *cats, + TALER_MERCHANT_ProductsPostCallback cb, + void *cb_cls); + + +/** * Cancel POST /products operation. * * @param pph operation to cancel diff --git a/src/include/taler_merchantdb_lib.h b/src/include/taler_merchantdb_lib.h index 3a641a54..a78d01f1 100644 --- a/src/include/taler_merchantdb_lib.h +++ b/src/include/taler_merchantdb_lib.h @@ -97,6 +97,16 @@ void TALER_MERCHANTDB_token_family_details_free ( struct TALER_MERCHANTDB_TokenFamilyDetails *tf); + +/** + * Free members of @a cd, but not @a cd itself. + * + * @param[in] cd token family details to clean up + */ +void +TALER_MERCHANTDB_category_details_free ( + struct TALER_MERCHANTDB_CategoryDetails *cd); + #endif /* MERCHANT_DB_H */ /* end of taler_merchantdb_lib.h */ diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h index aac39d17..f63fcd99 100644 --- a/src/include/taler_merchantdb_plugin.h +++ b/src/include/taler_merchantdb_plugin.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2023 Taler Systems SA + Copyright (C) 2014-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -210,10 +210,6 @@ struct TALER_MERCHANTDB_InstanceSettings */ struct GNUNET_TIME_Relative default_pay_delay; - /** - * Type of user this merchant represents. - */ - enum TALER_KYCLOGIC_KycUserType ut; }; @@ -335,6 +331,27 @@ struct TALER_MERCHANTDB_ProductDetails /** + * Typically called by `lookup_all_products`. + * + * @param cls a `json_t *` JSON array to build + * @param product_serial row ID of the product + * @param product_id ID of the product + * @param pd full product details + * @param num_categories length of @a categories array + * @param categories array of categories the + * product is in + */ +typedef void +(*TALER_MERCHANTDB_ProductCallback)( + void *cls, + uint64_t product_serial, + const char *product_id, + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_categories, + const uint64_t *categories); + + +/** * Typically called by `lookup_templates`. * * @param cls closure @@ -425,6 +442,75 @@ struct TALER_MERCHANTDB_OtpDeviceDetails /** + * Typically called by `lookup_categories`. + * + * @param cls closure + * @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 + */ +typedef void +(*TALER_MERCHANTDB_CategoriesCallback)( + void *cls, + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t product_count); + + +/** + * Details about a product category. + */ +struct TALER_MERCHANTDB_ProductSummary +{ + /** + * ID of the product. + */ + char *product_id; + + /** + * Description for the product. + */ + char *description; + + /** + * Translation of the @e description. + */ + json_t *description_i18n; + +}; + +/** + * Details about a product category. + */ +struct TALER_MERCHANTDB_CategoryDetails +{ + + /** + * Name of the category. + */ + char *category_name; + + /** + * Translations of the name of the category. + */ + json_t *category_name_i18n; + + /** + * Products in the category. + */ + struct TALER_MERCHANTDB_ProductSummary *products; + + /** + * Length of the @e products array. + */ + unsigned int num_products; + +}; + + +/** * Typically called by `lookup_webhooks`. * * @param cls a `json_t *` JSON array to build @@ -1640,6 +1726,22 @@ struct TALER_MERCHANTDB_Plugin TALER_MERCHANTDB_ProductsCallback cb, void *cb_cls); + + /** + * Lookup full details of all of the products the given instance has configured (expensive). + * + * @param cls closure + * @param instance_id instance to lookup products for + * @param cb function to call on all products found + * @param cb_cls closure for @a cb + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*lookup_all_products)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_ProductCallback cb, + void *cb_cls); + /** * Lookup details about a particular product. * @@ -1648,13 +1750,18 @@ struct TALER_MERCHANTDB_Plugin * @param product_id product to lookup * @param[out] pd set to the product details on success, can be NULL * (in that case we only want to check if the product exists) + * @param[out] num_categories set to length of @a categories array + * @param[out] categories set to array of categories the + * product is in, caller must free() it. * @return database result code */ enum GNUNET_DB_QueryStatus (*lookup_product)(void *cls, const char *instance_id, const char *product_id, - struct TALER_MERCHANTDB_ProductDetails *pd); + struct TALER_MERCHANTDB_ProductDetails *pd, + size_t *num_categories, + uint64_t **categories); /** * Delete information about a product. Note that the transaction must @@ -1678,13 +1785,24 @@ struct TALER_MERCHANTDB_Plugin * @param instance_id instance to insert product for * @param product_id product identifier of product to insert * @param pd the product details to insert + * @param num_cats length of @a cats array + * @param cats array of categories the product is in + * @param[out] no_instance set to true if @a instance_id is unknown + * @param[out] conflict set to true if a conflicting + * product already exists in the database + * @param[out] no_cat set to index of non-existing category from @a cats, or -1 if all @a cats were found * @return database result code */ enum GNUNET_DB_QueryStatus (*insert_product)(void *cls, const char *instance_id, const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd); + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *no_instance, + bool *conflict, + ssize_t *no_cat); /** * Update details about a particular product. Note that the @@ -1695,21 +1813,31 @@ struct TALER_MERCHANTDB_Plugin * @param cls closure * @param instance_id instance to lookup products for * @param product_id product to lookup - * @param pd set to the product details on success, can be NULL - * (in that case we only want to check if the product exists); - * total_sold in @a pd is ignored (!), total_lost must not - * exceed total_stock minus the existing total_sold; - * total_sold and total_stock must be larger or equal to - * the existing value; - * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the - * non-decreasing constraints are not met *or* if the product - * does not yet exist. + * @param pd product details with updated values + * @param num_cats length of @a cats array + * @param cats number of categories the product is in + * @param[out] no_instance the update failed as the instance is unknown + * @param[out] no_cat set to -1 on success, otherwise the update failed and this is set + * to the index of a category in @a cats that is unknown + * @param[out] no_product the @a product_id is unknown + * @param[out] lost_reduced the update failed as the counter of units lost would have been lowered + * @param[out] sold_reduced the update failed as the counter of units sold would have been lowered + * @param[out] stocked_reduced the update failed as the counter of units stocked would have been lowered + * @return database result code */ enum GNUNET_DB_QueryStatus (*update_product)(void *cls, const char *instance_id, const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd); + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *no_instance, + ssize_t *no_cat, + bool *no_product, + bool *lost_reduced, + bool *sold_reduced, + bool *stocked_reduced); /** * Lock stocks of a particular product. Note that the transaction must @@ -2920,6 +3048,24 @@ struct TALER_MERCHANTDB_Plugin /** + * Update details about a particular template. + * + * @param cls closure + * @param instance_id instance to update template for + * @param template_id template to update + * @param td update to the template details on success, can be NULL + * (in that case we only want to check if the template exists) + * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the template + * does not yet exist. + */ + enum GNUNET_DB_QueryStatus + (*update_template)(void *cls, + const char *instance_id, + const char *template_id, + const struct TALER_MERCHANTDB_TemplateDetails *td); + + + /** * Delete information about an OTP device. * * @param cls closure @@ -3015,21 +3161,104 @@ struct TALER_MERCHANTDB_Plugin /** - * Update details about a particular template. + * Delete information about a product category. * * @param cls closure - * @param instance_id instance to update template for - * @param template_id template to update - * @param td update to the template details on success, can be NULL - * (in that case we only want to check if the template exists) + * @param instance_id instance to delete category of + * @param category_id identifies the category to delete + * @return DB status code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS + * if template unknown. + */ + enum GNUNET_DB_QueryStatus + (*delete_category)(void *cls, + const char *instance_id, + uint64_t category_id); + + /** + * Insert new product category. + * + * @param cls closure + * @param instance_id instance to insert OTP device for + * @param category_name name of the category + * @param category_name_i18n translations of the category name + * @param[out] category_id set to the category id on success + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*insert_category)(void *cls, + const char *instance_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t *category_id); + + + /** + * Update descriptions of a product category. + * + * @param cls closure + * @param instance_id instance to update OTP device for + * @param category_id category to update + * @param category_name name of the category + * @param category_name_i18n translations of the category name * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the template * does not yet exist. */ enum GNUNET_DB_QueryStatus - (*update_template)(void *cls, + (*update_category)(void *cls, const char *instance_id, - const char *template_id, - const struct TALER_MERCHANTDB_TemplateDetails *td); + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n); + + /** + * Lookup all of the product categories the given instance has configured. + * + * @param cls closure + * @param instance_id instance to lookup OTP devices for + * @param cb function to call on all categories found + * @param cb_cls closure for @a cb + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*lookup_categories)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_CategoriesCallback cb, + void *cb_cls); + + + /** + * Lookup details about product category. + * + * @param cls closure + * @param instance_id instance to lookup template for + * @param category_id category to update + * @param[out] cd set to the category details on success, can be NULL + * (in that case we only want to check if the category exists) + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*select_category)(void *cls, + const char *instance_id, + uint64_t category_id, + struct TALER_MERCHANTDB_CategoryDetails *cd); + + + /** + * Lookup details about product category by name. + * + * @param cls closure + * @param instance_id instance to lookup template for + * @param category_name category name to look for + * @param[out] name_i18n category name translation + * @param[out] category_id category ID + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*select_category_by_name)(void *cls, + const char *instance_id, + const char *category_name, + json_t **name_i18n, + uint64_t *category_id); /** diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 1e7430d4..4e6a901a 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -10,7 +10,7 @@ lib_LTLIBRARIES = \ libtalermerchant.la libtalermerchant_la_LDFLAGS = \ - -version-info 5:2:0 \ + -version-info 6:0:0 \ -no-undefined libtalermerchant_la_SOURCES = \ diff --git a/src/lib/merchant_api_get_config.c b/src/lib/merchant_api_get_config.c index b4b700bd..9b501342 100644 --- a/src/lib/merchant_api_get_config.c +++ b/src/lib/merchant_api_get_config.c @@ -34,12 +34,12 @@ * Which version of the Taler protocol is implemented * by this library? Used to determine compatibility. */ -#define MERCHANT_PROTOCOL_CURRENT 14 +#define MERCHANT_PROTOCOL_CURRENT 16 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 2 +#define MERCHANT_PROTOCOL_AGE 4 /** * How many exchanges do we allow at most per merchant? diff --git a/src/lib/merchant_api_get_instance.c b/src/lib/merchant_api_get_instance.c index eef95b84..76f3691a 100644 --- a/src/lib/merchant_api_get_instance.c +++ b/src/lib/merchant_api_get_instance.c @@ -136,16 +136,6 @@ handle_get_instance_finished (void *cls, } igr.details.ok.details.address = address; igr.details.ok.details.jurisdiction = jurisdiction; - if (GNUNET_OK != - TALER_KYCLOGIC_kyc_user_type_from_string ( - uts, - &igr.details.ok.details.ut)) - { - GNUNET_break_op (0); - igr.hr.http_status = 0; - igr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - } igh->cb (igh->cb_cls, &igr); TALER_MERCHANT_instance_get_cancel (igh); diff --git a/src/lib/merchant_api_get_instances.c b/src/lib/merchant_api_get_instances.c index b2f99853..c47c4895 100644 --- a/src/lib/merchant_api_get_instances.c +++ b/src/lib/merchant_api_get_instances.c @@ -126,13 +126,6 @@ parse_instances (const json_t *json, GNUNET_break_op (0); return GNUNET_SYSERR; } - if (GNUNET_OK != - TALER_KYCLOGIC_kyc_user_type_from_string (uts, - &ii->ut)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } for (size_t i = 0; i<json_array_size (ii->payment_targets); i++) { if (! json_is_string (json_array_get (ii->payment_targets, diff --git a/src/lib/merchant_api_get_product.c b/src/lib/merchant_api_get_product.c index 3f026bf3..bb10a438 100644 --- a/src/lib/merchant_api_get_product.c +++ b/src/lib/merchant_api_get_product.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2023 Taler Systems SA + Copyright (C) 2014-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -105,12 +105,16 @@ handle_get_product_finished (void *cls, TALER_JSON_spec_amount_any ( "price", &pgr.details.ok.price), - GNUNET_JSON_spec_string ( - "image", - &pgr.details.ok.image), - GNUNET_JSON_spec_array_const ( - "taxes", - &pgr.details.ok.taxes), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ( + "image", + &pgr.details.ok.image), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ( + "taxes", + &pgr.details.ok.taxes), + NULL), GNUNET_JSON_spec_int64 ( "total_stock", &pgr.details.ok.total_stock), @@ -120,12 +124,15 @@ handle_get_product_finished (void *cls, GNUNET_JSON_spec_uint64 ( "total_lost", &pgr.details.ok.total_lost), - GNUNET_JSON_spec_object_const ( - "address", - &pgr.details.ok.location), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("next_restock", - &pgr.details.ok.next_restock), + GNUNET_JSON_spec_object_const ( + "address", + &pgr.details.ok.location), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ( + "next_restock", + &pgr.details.ok.next_restock), NULL), GNUNET_JSON_spec_end () }; diff --git a/src/lib/merchant_api_patch_instance.c b/src/lib/merchant_api_patch_instance.c index 420cd549..f4f39642 100644 --- a/src/lib/merchant_api_patch_instance.c +++ b/src/lib/merchant_api_patch_instance.c @@ -159,7 +159,6 @@ TALER_MERCHANT_instance_patch ( const char *backend_url, const char *instance_id, const char *name, - enum TALER_KYCLOGIC_KycUserType ut, const json_t *address, const json_t *jurisdiction, bool use_stefan, @@ -170,19 +169,10 @@ TALER_MERCHANT_instance_patch ( { struct TALER_MERCHANT_InstancePatchHandle *iph; json_t *req_obj; - const char *uts; - uts = TALER_KYCLOGIC_kyc_user_type2s (ut); - if (NULL == uts) - { - GNUNET_break (0); - return NULL; - } req_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("name", name), - GNUNET_JSON_pack_string ("user_type", - uts), GNUNET_JSON_pack_object_incref ("address", (json_t *) address), GNUNET_JSON_pack_object_incref ("jurisdiction", diff --git a/src/lib/merchant_api_post_instances.c b/src/lib/merchant_api_post_instances.c index 73d36369..c8e5add6 100644 --- a/src/lib/merchant_api_post_instances.c +++ b/src/lib/merchant_api_post_instances.c @@ -165,7 +165,6 @@ TALER_MERCHANT_instances_post ( const char *backend_url, const char *instance_id, const char *name, - enum TALER_KYCLOGIC_KycUserType ut, const json_t *address, const json_t *jurisdiction, bool use_stefan, @@ -178,14 +177,7 @@ TALER_MERCHANT_instances_post ( struct TALER_MERCHANT_InstancesPostHandle *iph; json_t *req_obj; json_t *auth_obj; - const char *uts; - uts = TALER_KYCLOGIC_kyc_user_type2s (ut); - if (NULL == uts) - { - GNUNET_break (0); - return NULL; - } if (NULL != auth_token) { if (0 != strncasecmp (RFC_8959_PREFIX, @@ -219,8 +211,6 @@ TALER_MERCHANT_instances_post ( instance_id), GNUNET_JSON_pack_string ("name", name), - GNUNET_JSON_pack_string ("user_type", - uts), GNUNET_JSON_pack_object_incref ("address", (json_t *) address), GNUNET_JSON_pack_object_incref ("jurisdiction", diff --git a/src/lib/merchant_api_post_products.c b/src/lib/merchant_api_post_products.c index 0f09f397..5d0ad27e 100644 --- a/src/lib/merchant_api_post_products.c +++ b/src/lib/merchant_api_post_products.c @@ -159,7 +159,7 @@ handle_post_products_finished (void *cls, struct TALER_MERCHANT_ProductsPostHandle * -TALER_MERCHANT_products_post2 ( +TALER_MERCHANT_products_post3 ( struct GNUNET_CURL_Context *ctx, const char *backend_url, const char *product_id, @@ -173,12 +173,28 @@ TALER_MERCHANT_products_post2 ( const json_t *address, struct GNUNET_TIME_Timestamp next_restock, uint32_t minimum_age, + unsigned int num_cats, + const uint64_t *cats, TALER_MERCHANT_ProductsPostCallback cb, void *cb_cls) { struct TALER_MERCHANT_ProductsPostHandle *pph; json_t *req_obj; + json_t *categories; + if (0 == num_cats) + { + categories = NULL; + } + else + { + categories = json_array (); + GNUNET_assert (NULL != categories); + for (unsigned int i = 0; i<num_cats; i++) + GNUNET_assert (0 == + json_array_append_new (categories, + json_integer (cats[i]))); + } req_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("product_id", product_id), @@ -187,6 +203,9 @@ TALER_MERCHANT_products_post2 ( GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("description_i18n", (json_t *) description_i18n)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("categories", + categories)), GNUNET_JSON_pack_string ("unit", unit), TALER_JSON_pack_amount ("price", @@ -243,6 +262,44 @@ TALER_MERCHANT_products_post2 ( struct TALER_MERCHANT_ProductsPostHandle * +TALER_MERCHANT_products_post2 ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *product_id, + const char *description, + const json_t *description_i18n, + const char *unit, + const struct TALER_Amount *price, + const char *image, + const json_t *taxes, + int64_t total_stock, + const json_t *address, + struct GNUNET_TIME_Timestamp next_restock, + uint32_t minimum_age, + TALER_MERCHANT_ProductsPostCallback cb, + void *cb_cls) +{ + return TALER_MERCHANT_products_post3 (ctx, + backend_url, + product_id, + description, + description_i18n, + unit, + price, + image, + taxes, + total_stock, + address, + next_restock, + minimum_age, + 0, + NULL, + cb, + cb_cls); +} + + +struct TALER_MERCHANT_ProductsPostHandle * TALER_MERCHANT_products_post ( struct GNUNET_CURL_Context *ctx, const char *backend_url, diff --git a/src/merchant-tools/taler-merchant-passwd.c b/src/merchant-tools/taler-merchant-passwd.c index bfd6534d..bcb856e5 100644 --- a/src/merchant-tools/taler-merchant-passwd.c +++ b/src/merchant-tools/taler-merchant-passwd.c @@ -54,7 +54,7 @@ run (void *cls, const char *pw = args[0]; struct TALER_MERCHANTDB_InstanceAuthSettings ias; enum GNUNET_DB_QueryStatus qs; - + if (NULL == pw) pw = getenv ("TALER_MERCHANT_PASSWORD"); if (NULL == pw) @@ -64,6 +64,16 @@ run (void *cls, global_ret = -1; return; } + if (0 != strncmp (pw, + RFC_8959_PREFIX, + strlen (RFC_8959_PREFIX))) + { + fprintf (stderr, + "Invalid password specified, does not begin with `%s'\n", + RFC_8959_PREFIX); + global_ret = 1; + return; + } if (NULL == instance) instance = GNUNET_strdup ("default"); cfg = GNUNET_CONFIGURATION_dup (config); @@ -112,7 +122,7 @@ run (void *cls, .size = ntohs (sizeof (es)), .type = ntohs (TALER_DBEVENT_MERCHANT_INSTANCE_SETTINGS) }; - + plugin->event_notify (plugin->cls, &es, instance, diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh index 0869cfdd..1b52b4af 100755 --- a/src/testing/test_merchant_order_creation.sh +++ b/src/testing/test_merchant_order_creation.sh @@ -263,7 +263,7 @@ echo " OK" echo -n "Creating v1 order with token family ..." STATUS=$(curl 'http://localhost:9966/private/orders' \ - -d '{"order":{"version":"1","amount":"TESTKUDOS:7","summary":"with_subscription","fulfillment_message":"Payed successfully","choices":[{"inputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}],"outputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}]}]}}' \ + -d '{"order":{"version":"1","amount":"TESTKUDOS:7","summary":"with_subscription","fulfillment_message":"Paid successfully","choices":[{"inputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}],"outputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}]}]}}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] diff --git a/src/testing/test_merchant_order_refund.sh b/src/testing/test_merchant_order_refund.sh new file mode 100755 index 00000000..af07d233 --- /dev/null +++ b/src/testing/test_merchant_order_refund.sh @@ -0,0 +1,233 @@ +#!/bin/bash +#!/bin/bash +# This file is in the public domain. + +set -eu + +function clean_wallet() { + echo rm -f "${WALLET_DB}" + exit_cleanup +} + + +# Replace with 0 for nexus... +USE_FAKEBANK=1 +if [ 1 = "$USE_FAKEBANK" ] +then + ACCOUNT="exchange-account-2" + BANK_FLAGS="-f -d x-taler-bank -u $ACCOUNT" + BANK_URL="http://localhost:8082/" +else + ACCOUNT="exchange-account-1" + BANK_FLAGS="-ns -d iban -u $ACCOUNT" + BANK_URL="http://localhost:18082/" + echo -n "Testing for libeufin-bank" + libeufin-bank --help >/dev/null </dev/null || exit_skip " MISSING" + echo " FOUND" + +fi + +. setup.sh + +echo -n "Testing for taler-harness" +taler-harness --help >/dev/null </dev/null || exit_skip " MISSING" +echo " FOUND" + +# Launch exchange, merchant and bank. +setup -c "test_template.conf" \ + -em \ + $BANK_FLAGS +LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX) +CONF="test_template.conf.edited" +WALLET_DB=$(mktemp -p "${TMPDIR:-/tmp}" test_wallet.json-XXXXXX) +EXCHANGE_URL="http://localhost:8081/" + +# Install cleanup handler (except for kill -9) +trap clean_wallet EXIT + +echo -n "First prepare wallet with coins ..." +rm -f "$WALLET_DB" +taler-wallet-cli \ + --no-throttle \ + --wallet-db="$WALLET_DB" \ + api \ + --expect-success 'withdrawTestBalance' \ + "$(jq -n ' + { + amount: "TESTKUDOS:99", + corebankApiBaseUrl: $BANK_URL, + exchangeBaseUrl: $EXCHANGE_URL + }' \ + --arg BANK_URL "${BANK_URL}" \ + --arg EXCHANGE_URL "$EXCHANGE_URL" + )" 2>wallet-withdraw-1.err >wallet-withdraw-1.out +echo -n "." +# FIXME-MS: add logic to have nexus check immediately here. +# sleep 10 +echo -n "." +# NOTE: once libeufin can do long-polling, we should +# be able to reduce the delay here and run wirewatch +# always in the background via setup +taler-exchange-wirewatch \ + -a "$ACCOUNT" \ + -L "INFO" \ + -c "$CONF" \ + -t &> taler-exchange-wirewatch.out +echo -n "." +taler-wallet-cli \ + --wallet-db="$WALLET_DB" \ + run-until-done \ + 2>wallet-withdraw-finish-1.err \ + >wallet-withdraw-finish-1.out +echo " OK" + +CURRENCY_COUNT=$(taler-wallet-cli --wallet-db="$WALLET_DB" balance | jq '.balances|length') +if [ "$CURRENCY_COUNT" = "0" ] +then + exit_fail "Expected least one currency, withdrawal failed. check log." +fi + +# +# CREATE INSTANCE FOR TESTING +# + +echo -n "Configuring merchant instance ..." + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Bearer secret-token:super_secret' \ + http://localhost:9966/management/instances \ + -d '{"auth":{"method":"external"},"id":"default","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000000},"default_pay_delay":{"d_us": 60000000000}}' \ + -w "%{http_code}" -s -o /dev/null) + +if [ "$STATUS" != "204" ] +then + exit_fail "Expected '204 No content' response. Got instead $STATUS" +fi +echo "Ok" + +echo -n "Configuring merchant account ..." + +if [ 1 = "$USE_FAKEBANK" ] +then + FORTYTHREE="payto://x-taler-bank/localhost/fortythree?receiver-name=fortythree" +else + FORTYTHREE=$(get_payto_uri fortythree x) +fi +# create with 2 bank account addresses +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Bearer secret-token:super_secret' \ + http://localhost:9966/private/accounts \ + -d '{"payto_uri":"'"$FORTYTHREE"'"}' \ + -w "%{http_code}" -s -o /dev/null) + +if [ "$STATUS" != "200" ] +then + exit_fail "Expected '200 OK' response. Got instead $STATUS" +fi + +echo "Ok" + + +# +# CREATE ORDER AND SELL IT +# + +echo -n "Creating order to be paid..." +STATUS=$(curl 'http://localhost:9966/private/orders' \ + -d '{"order":{"amount":"TESTKUDOS:5","summary":"payme","auto_refund":{"d_us":180000000}}}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq . < "$LAST_RESPONSE" + exit_fail "Expected 200, order created. got: $STATUS" +fi + +ORDER_ID=$(jq -e -r .order_id < "$LAST_RESPONSE") +TOKEN=$(jq -e -r .token < "$LAST_RESPONSE") + +STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq . < "$LAST_RESPONSE" + exit_fail "Expected 200, getting order info before claming it. got: $STATUS" +fi + +PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE") + +echo "OK" + +# +# PAY THE ORDER +# +# set -x +PAYMENT_START=$(date +%s) +echo "Pay first order ${PAY_URL} ..." +taler-wallet-cli --no-throttle -V --wallet-db="$WALLET_DB" handle-uri "${PAY_URL}" -y 2> wallet-pay1.err > wallet-pay1.log +PAYMENT_END=$(date +%s) +echo " OK (payment took $(( PAYMENT_END - PAYMENT_START )) secs )" + +STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq . < "$LAST_RESPONSE" + exit_fail "Expected 200, after pay. got: $STATUS" +fi + +ORDER_STATUS=$(jq -r .order_status < "$LAST_RESPONSE") + +if [ "$ORDER_STATUS" != "paid" ] +then + jq . < "$LAST_RESPONSE" + exit_fail "Order status should be 'paid'. got: $ORDER_STATUS" +fi + + +echo Sending refund for TESTKUDOS:1 + +STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}/refund" \ + -d '{"refund":"TESTKUDOS:1","reason":"duplicated"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq . < "$LAST_RESPONSE" + exit_fail "Expected 200, after refund. got: $STATUS" +fi + +REFUND_URI=$(jq -e -r .taler_refund_uri < "$LAST_RESPONSE") + +REFUND_START=$(date +%s) +echo "First refund order ${REFUND_URI} ..." +set -x +taler-wallet-cli --no-throttle --wallet-db="$WALLET_DB" handle-uri "${REFUND_URI}" -y 2> wallet-refund1.err > wallet-refund1.log +taler-wallet-cli --no-throttle --wallet-db="$WALLET_DB" run-pending -y 2> wallet-pending-refund1.err > wallet-pending-refund1.log +REFUND_END=$(date +%s) +echo " OK (refund1 took $(( REFUND_END - REFUND_START )) secs )" + +echo Increasing refund for TESTKUDOS:3 + +STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}/refund" \ + -d '{"refund":"TESTKUDOS:5","reason":"duplicated"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq . < "$LAST_RESPONSE" + exit_fail "Expected 200, after refund. got: $STATUS" +fi + +REFUND_URI=$(jq -e -r .taler_refund_uri < "$LAST_RESPONSE") + +REFUND2_START=$(date +%s) +echo "Second refund order ${REFUND_URI} ..." +taler-wallet-cli --no-throttle --wallet-db="$WALLET_DB" handle-uri "${REFUND_URI}" -y 2> wallet-refund2.err > wallet-refund2.log +taler-wallet-cli --no-throttle --wallet-db="$WALLET_DB" run-pending -y 2> wallet-pending-refund2.err > wallet-pending-refund2.log +REFUND2_END=$(date +%s) +echo " OK (refund2 took $(( REFUND2_END - REFUND_START )) secs )" + +exit 0 diff --git a/src/testing/testing_api_cmd_patch_instance.c b/src/testing/testing_api_cmd_patch_instance.c index cef38bec..f4ae5cf7 100644 --- a/src/testing/testing_api_cmd_patch_instance.c +++ b/src/testing/testing_api_cmd_patch_instance.c @@ -158,7 +158,6 @@ patch_instance_run (void *cls, pis->merchant_url, pis->instance_id, pis->name, - TALER_KYCLOGIC_KYC_UT_BUSINESS, pis->address, pis->jurisdiction, pis->use_stefan, diff --git a/src/testing/testing_api_cmd_post_instances.c b/src/testing/testing_api_cmd_post_instances.c index 0d081026..18afb2e8 100644 --- a/src/testing/testing_api_cmd_post_instances.c +++ b/src/testing/testing_api_cmd_post_instances.c @@ -164,7 +164,6 @@ post_instances_run (void *cls, pis->merchant_url, pis->instance_id, pis->name, - TALER_KYCLOGIC_KYC_UT_BUSINESS, pis->address, pis->jurisdiction, pis->use_stefan, |