merchant

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

commit 5c7fbb8fe6fa87b0d74f118ff3f21b16ea45f194
parent 706748172cfe93b12f93c5dcdd33cc75a50b2db0
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Thu,  1 Jan 2026 01:18:51 +0100

basic stuff for templates + paivana template skeleton

Diffstat:
Msrc/backend/taler-merchant-httpd_get-templates-ID.c | 473+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/backend/taler-merchant-httpd_helper.c | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backend/taler-merchant-httpd_helper.h | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backend/taler-merchant-httpd_post-using-templates.c | 827+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/backend/taler-merchant-httpd_private-patch-templates-ID.c | 24+++++++++++++++++++++++-
Msrc/backend/taler-merchant-httpd_private-post-templates.c | 26+++++++++++++++++++++++++-
Msrc/backenddb/Makefile.am | 4++++
Msrc/backenddb/merchantdb_helper.c | 1+
Msrc/backenddb/pg_insert_template.c | 9++++++---
Msrc/backenddb/pg_lookup_all_products.c | 1+
Asrc/backenddb/pg_lookup_categories_by_ids.c | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_categories_by_ids.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_custom_units_by_names.c | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_custom_units_by_names.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_inventory_products.c | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_inventory_products.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_inventory_products_filtered.c | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_inventory_products_filtered.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_lookup_product.c | 1+
Msrc/backenddb/pg_lookup_template.c | 21+++++++++++++--------
Msrc/backenddb/pg_update_template.c | 9++++++---
Msrc/backenddb/plugin_merchantdb_postgres.c | 12++++++++++++
Msrc/include/taler_merchant_service.h | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/include/taler_merchant_testing_lib.h | 224++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/include/taler_merchant_util.h | 35+++++++++++++++++++++++++++++++++++
Msrc/include/taler_merchantdb_plugin.h | 1205+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/lib/Makefile.am | 1+
Asrc/lib/merchant_api_post_categories.c | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/merchant_api_post_templates.c | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/lib/merchant_api_post_using_templates.c | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/testing/Makefile.am | 2++
Msrc/testing/test_merchant_api.c | 437+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testing/testing_api_cmd_post_categories.c | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/testing_api_cmd_post_products.c | 128++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/testing/testing_api_cmd_post_using_templates.c | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Asrc/testing/testing_api_cmd_wallet_get_template.c | 449+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
36 files changed, 5341 insertions(+), 747 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_get-templates-ID.c b/src/backend/taler-merchant-httpd_get-templates-ID.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2022-2024 Taler Systems SA + (C) 2022-2025 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 @@ -20,9 +20,442 @@ */ #include "platform.h" #include "taler-merchant-httpd_get-templates-ID.h" +#include "taler-merchant-httpd_helper.h" #include <taler/taler_json_lib.h> +/** + * Context for building inventory template payloads. + */ +struct InventoryPayloadContext +{ + /** + * Selected category IDs (as JSON array). + */ + const json_t *selected_categories; + + /** + * Selected product IDs (as JSON array) from contract_template. + */ + const json_t *selected_products; + + /** + * Whether all products are selected. + */ + bool selected_all; + + /** + * JSON array of products to build. + */ + json_t *products; + + /** + * JSON array of categories to build. + */ + json_t *category_payload; + + /** + * JSON array of units to build. + */ + json_t *unit_payload; + + /** + * Set of categories referenced by the products. + */ + struct TMH_CategorySet category_set; + + /** + * Set of unit identifiers referenced by the products. + */ + struct TMH_UnitSet unit_set; +}; + + +/** + * Release resources associated with an inventory payload context. + * + * @param ipc inventory payload context + */ +static void +inventory_payload_cleanup (struct InventoryPayloadContext *ipc) +{ + if (NULL != ipc->products) + json_decref (ipc->products); + if (NULL != ipc->category_payload) + json_decref (ipc->category_payload); + if (NULL != ipc->unit_payload) + json_decref (ipc->unit_payload); + GNUNET_free (ipc->category_set.ids); + TMH_unit_set_clear (&ipc->unit_set); + ipc->products = NULL; + ipc->category_payload = NULL; + ipc->unit_payload = NULL; + ipc->category_set.ids = NULL; + ipc->category_set.len = 0; +} + + +/** + * Add inventory product to JSON payload. + * + * @param cls inventory payload context + * @param product_id product identifier + * @param pd product details + * @param num_categories number of categories + * @param categories category IDs + */ +static void +add_inventory_product (void *cls, + const char *product_id, + const struct + TALER_MERCHANTDB_InventoryProductDetails *pd, + size_t num_categories, + const uint64_t *categories) +{ + struct InventoryPayloadContext *ipc = cls; + json_t *jcategories; + json_t *product; + + jcategories = json_array (); + GNUNET_assert (NULL != jcategories); + for (size_t i = 0; i < num_categories; i++) + { + /* Adding per product category */ + TMH_category_set_add (&ipc->category_set, + categories[i]); + GNUNET_assert (0 == + json_array_append_new (jcategories, + json_integer (categories[i]))); + } + GNUNET_assert (0 < pd->price_array_length); + product = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_id", + product_id), + GNUNET_JSON_pack_string ("product_name", + pd->product_name), + GNUNET_JSON_pack_string ("description", + pd->description), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("description_i18n", + pd->description_i18n)), + GNUNET_JSON_pack_string ("unit", + pd->unit), + TALER_JSON_pack_amount_array ("unit_prices", + pd->price_array_length, + pd->price_array), + GNUNET_JSON_pack_bool ("unit_allow_fraction", + pd->allow_fractional_quantity), + GNUNET_JSON_pack_uint64 ("unit_precision_level", + pd->fractional_precision_level), + GNUNET_JSON_pack_array_steal ("categories", + jcategories), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("taxes", + pd->taxes)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("image_hash", + pd->image_hash))); + + /* Adding per product unit */ + TMH_unit_set_add (&ipc->unit_set, + pd->unit); + + GNUNET_assert (0 == + json_array_append_new (ipc->products, + product)); +} + + +/** + * Add an inventory category to the payload if referenced. + * + * @param cls category payload context + * @param category_id category identifier + * @param category_name category name + * @param category_name_i18n translated names + * @param product_count number of products (unused) + */ +static void +add_inventory_category (void *cls, + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t product_count) +{ + struct InventoryPayloadContext *ipc = cls; + json_t *category; + + (void) product_count; + category = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("category_id", + category_id), + GNUNET_JSON_pack_string ("category_name", + category_name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("category_name_i18n", + (json_t *) category_name_i18n))); + GNUNET_assert (0 == + json_array_append_new (ipc->category_payload, + category)); +} + + +/** + * Add an inventory unit to the payload if referenced and non-builtin. + * + * @param cls unit payload context + * @param unit_serial unit identifier + * @param ud unit details + */ +static void +add_inventory_unit (void *cls, + uint64_t unit_serial, + const struct TALER_MERCHANTDB_UnitDetails *ud) +{ + struct InventoryPayloadContext *ipc = cls; + json_t *unit; + + (void) unit_serial; + + unit = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("unit", + ud->unit), + GNUNET_JSON_pack_string ("unit_name_long", + ud->unit_name_long), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("unit_name_long_i18n", + ud->unit_name_long_i18n)), + GNUNET_JSON_pack_string ("unit_name_short", + ud->unit_name_short), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("unit_name_short_i18n", + ud->unit_name_short_i18n)), + GNUNET_JSON_pack_bool ("unit_allow_fraction", + ud->unit_allow_fraction), + GNUNET_JSON_pack_uint64 ("unit_precision_level", + ud->unit_precision_level)); + GNUNET_assert (0 == + json_array_append_new (ipc->unit_payload, + unit)); +} + + +/** + * Build wallet-facing payload for inventory templates. + * + * @param connection HTTP connection + * @param mi merchant instance + * @param tp template details + * @return MHD result + */ +static MHD_RESULT +handle_get_templates_inventory ( + struct MHD_Connection *connection, + const struct TMH_MerchantInstance *mi, + const struct TALER_MERCHANTDB_TemplateDetails *tp) +{ + struct InventoryPayloadContext ipc; + const char **product_ids = NULL; + uint64_t *category_ids = NULL; + size_t num_product_ids = 0; + size_t num_category_ids = 0; + json_t *inventory_payload; + json_t *template_contract; + enum GNUNET_DB_QueryStatus qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + + memset (&ipc, + 0, + sizeof (ipc)); + ipc.products = json_array (); + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_categories", + &ipc.selected_categories), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_products", + &ipc.selected_products), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("selected_all", + &ipc.selected_all), + NULL), + GNUNET_JSON_spec_end () + }; + const char *err_name; + unsigned int err_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (tp->template_contract, + spec, + &err_name, + &err_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid inventory template_contract for field %s\n", + err_name); + inventory_payload_cleanup (&ipc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + err_name); + } + } + + if (! ipc.selected_all) + { + // TODO: maybe, these 2 sections would be better to save to the db, in the post/patch part + // and not here, yet it is quite lightweight and straightforward so I wouldn't really care... + if (NULL != ipc.selected_products) + { + size_t max_ids; + + max_ids = json_array_size (ipc.selected_products); + if (0 < max_ids) + product_ids = GNUNET_new_array (max_ids, + const char *); + for (size_t i = 0; i < max_ids; i++) + { + const json_t *entry = json_array_get (ipc.selected_products, + i); + + if (json_is_string (entry)) + product_ids[num_product_ids++] = json_string_value (entry); + } + } + if (NULL != ipc.selected_categories) + { + size_t max_categories; + + max_categories = json_array_size (ipc.selected_categories); + if (0 < max_categories) + category_ids = GNUNET_new_array (max_categories, + uint64_t); + for (size_t i = 0; i < max_categories; i++) + { + const json_t *entry = json_array_get (ipc.selected_categories, + i); + + if (json_is_integer (entry) && + (0 <= json_integer_value (entry))) + category_ids[num_category_ids++] + = (uint64_t) json_integer_value (entry); + } + } + } + + if (ipc.selected_all) + qs = TMH_db->lookup_inventory_products (TMH_db->cls, + mi->settings.id, + &add_inventory_product, + &ipc); + else if ( (0 < num_product_ids) || + (0 < num_category_ids) ) + { + qs = TMH_db->lookup_inventory_products_filtered (TMH_db->cls, + mi->settings.id, + product_ids, + num_product_ids, + category_ids, + num_category_ids, + &add_inventory_product, + &ipc); + + GNUNET_free (product_ids); + GNUNET_free (category_ids); + } + if (0 > qs) + { + GNUNET_break (0); + inventory_payload_cleanup (&ipc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_inventory_products"); + } + + ipc.category_payload = json_array (); + GNUNET_assert (NULL != ipc.category_payload); + if (0 < ipc.category_set.len) + { + qs = TMH_db->lookup_categories_by_ids (TMH_db->cls, + mi->settings.id, + ipc.category_set.ids, + ipc.category_set.len, + &add_inventory_category, + &ipc); + if (0 > qs) + { + GNUNET_break (0); + inventory_payload_cleanup (&ipc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_categories_by_ids"); + } + } + + ipc.unit_payload = json_array (); + GNUNET_assert (NULL != ipc.unit_payload); + if (0 < ipc.unit_set.len) + { + qs = TMH_db->lookup_custom_units_by_names ( + TMH_db->cls, + mi->settings.id, + (const char *const *) ipc.unit_set. + units, + ipc.unit_set.len, + &add_inventory_unit, + &ipc); + if (0 > qs) + { + GNUNET_break (0); + inventory_payload_cleanup (&ipc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_custom_units_by_names"); + } + } + + inventory_payload = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("products", + ipc.products), + GNUNET_JSON_pack_array_steal ("categories", + ipc.category_payload), + GNUNET_JSON_pack_array_steal ("units", + ipc.unit_payload)); + ipc.products = NULL; + ipc.category_payload = NULL; + ipc.unit_payload = NULL; + + template_contract = json_deep_copy (tp->template_contract); + GNUNET_assert (NULL != template_contract); + json_object_set_new (template_contract, + "inventory_payload", + inventory_payload); + { + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("editable_defaults", + tp->editable_defaults)), + GNUNET_JSON_pack_object_steal ("template_contract", + template_contract)); + inventory_payload_cleanup (&ipc); + return ret; + } +} + + MHD_RESULT TMH_get_templates_ID ( const struct TMH_RequestHandler *rh, @@ -58,14 +491,36 @@ TMH_get_templates_ID ( { MHD_RESULT ret; - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("editable_defaults", - tp.editable_defaults)), - GNUNET_JSON_pack_object_incref ("template_contract", - tp.template_contract)); + switch (TMH_template_type_from_contract (tp.template_contract)) + { + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + ret = handle_get_templates_inventory (connection, + mi, + &tp); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + /* TODO: PAIVANA add paivana-specific payload elements. + (Yet, I think, everything will be in the contract json so we can just pack it) */ + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("editable_defaults", + tp.editable_defaults)), + GNUNET_JSON_pack_object_incref ("template_contract", + tp.template_contract)); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + default: + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "template_type"); + break; + } TALER_MERCHANTDB_template_details_free (&tp); return ret; } diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c @@ -466,9 +466,44 @@ TMH_products_array_valid (const json_t *products) } +enum TALER_MERCHANT_TemplateType +TMH_template_type_from_contract (const json_t *template_contract) +{ + const json_t *type_val; + + if (NULL == template_contract) + return TALER_MERCHANT_TEMPLATE_TYPE_INVALID; + type_val = json_object_get (template_contract, + "template_type"); + if (! json_is_string (type_val)) + return TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER; + return TALER_MERCHANT_template_type_from_string ( + json_string_value (type_val)); +} + + +const char * +TMH_template_type_to_string (enum TALER_MERCHANT_TemplateType template_type) +{ + switch (template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + return "fixed-order"; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + return "inventory-cart"; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + return "paivana"; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + break; + } + return NULL; +} + + bool TMH_template_contract_valid (const json_t *template_contract) { + enum TALER_MERCHANT_TemplateType template_type; const char *summary; const char *currency; struct TALER_Amount amount = { .value = 0}; @@ -496,6 +531,134 @@ TMH_template_contract_valid (const json_t *template_contract) const char *ename; unsigned int eline; + template_type = TMH_template_type_from_contract (template_contract); + if (TALER_MERCHANT_TEMPLATE_TYPE_INVALID == template_type) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid template_type\n"); + return false; + } + + if (TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART == template_type) + { + bool selected_all = false; + bool choose_one = false; + bool request_tip = false; + const json_t *selected_categories = NULL; + const json_t *selected_products = NULL; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("summary", + &summary), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("request_tip", + &request_tip), + NULL), + GNUNET_JSON_spec_relative_time ("pay_duration", + &pay_duration), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("selected_all", + &selected_all), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_categories", + &selected_categories), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_products", + &selected_products), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("choose_one", + &choose_one), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (template_contract, + ispec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid inventory template_contract for field %s\n", + ename); + return false; + } + if (NULL != selected_categories) + { + const json_t *entry; + size_t idx; + + json_array_foreach ((json_t *) selected_categories, idx, entry) + { + if (! json_is_integer (entry)) + { + GNUNET_break_op (0); + return false; + } + if (0 > json_integer_value (entry)) + { + GNUNET_break_op (0); + return false; + } + } + } + if (NULL != selected_products) + { + const json_t *entry; + size_t idx; + + json_array_foreach ((json_t *) selected_products, idx, entry) + { + if (! json_is_string (entry)) + { + GNUNET_break_op (0); + return false; + } + } + } + return true; + } + + if (TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA == template_type) + { + const char *paivana_id; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_string ("paivana_id", + &paivana_id), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (template_contract, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid paivana template_contract for field %s\n", + ename); + return false; + } + if (GNUNET_OK != + GNUNET_JSON_parse (template_contract, + pspec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid paivana template_contract for field %s\n", + ename); + return false; + } + (void) paivana_id; + /* TODO: PAIVANA validate paivana-specific fields beyond presence. */ + return true; + } + if (GNUNET_OK != GNUNET_JSON_parse (template_contract, spec, @@ -511,6 +674,100 @@ TMH_template_contract_valid (const json_t *template_contract) } +bool +TMH_category_set_contains (const struct TMH_CategorySet *set, + uint64_t id) +{ + for (size_t i = 0; i < set->len; i++) + if (set->ids[i] == id) + return true; + return false; +} + + +void +TMH_category_set_add (struct TMH_CategorySet *set, + uint64_t id) +{ + if (TMH_category_set_contains (set, + id)) + return; + GNUNET_array_append (set->ids, + set->len, + id); +} + + +bool +TMH_unit_set_contains (const struct TMH_UnitSet *set, + const char *unit) +{ + for (unsigned int i = 0; i < set->len; i++) + if (0 == strcmp (set->units[i], + unit)) + return true; + return false; +} + + +void +TMH_unit_set_add (struct TMH_UnitSet *set, + const char *unit) +{ + if (TMH_unit_set_contains (set, + unit)) + return; + GNUNET_array_append (set->units, + set->len, + GNUNET_strdup (unit)); +} + + +void +TMH_unit_set_clear (struct TMH_UnitSet *set) +{ + for (unsigned int i = 0; i < set->len; i++) + GNUNET_free (set->units[i]); + GNUNET_free (set->units); + set->units = NULL; + set->len = 0; +} + + +bool +TMH_taxes_array_valid (const json_t *taxes) +{ + json_t *tax; + size_t idx; + + if (! json_is_array (taxes)) + return false; + json_array_foreach (taxes, idx, tax) + { + struct TALER_Amount amount; + const char *name; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &name), + TALER_JSON_spec_amount_any ("tax", + &amount), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (NULL, + tax, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + return false; + } + } + return true; +} + + struct TMH_WireMethod * TMH_setup_wire_account ( struct TALER_FullPayto payto_uri, diff --git a/src/backend/taler-merchant-httpd_helper.h b/src/backend/taler-merchant-httpd_helper.h @@ -27,6 +27,7 @@ #include "taler-merchant-httpd.h" +#include <taler/taler_merchant_util.h> /** * check @a accounts for well-formedness @@ -65,6 +66,163 @@ TMH_products_array_valid (const json_t *products); /** + * Parse decimal quantity expressed as string for request handling. + * + * @param value string to parse + * @param[out] integer_part result integer component + * @param[out] fractional_part result fractional component (0..MERCHANT_UNIT_FRAC_BASE-1) + * @return #GNUNET_OK on success, #GNUNET_SYSERR on validation failure + */ +enum GNUNET_GenericReturnValue +TMH_parse_fractional_string (const char *value, + int64_t *integer_part, + uint32_t *fractional_part); + +/** + * Kind of fixed-decimal value the helpers operate on. + * Primarily distinguishes how special sentinel values (such as "-1" + * meaning infinity for stock) must be encoded. + */ +enum TMH_ValueKind +{ + TMH_VK_QUANTITY, /* -1 is illegal */ + TMH_VK_STOCK /* -1 means "infinity" */ +}; + +/** + * Determine template type from a template contract. + * + * @param template_contract contract JSON + * @return template type (defaults to fixed order) + */ +enum TALER_MERCHANT_TemplateType +TMH_template_type_from_contract (const json_t *template_contract); + +/** + * Convert template type to its string representation. + * + * @param template_type template type to convert + * @return string name or NULL for invalid types + */ +const char * +TMH_template_type_to_string (enum TALER_MERCHANT_TemplateType template_type); + +/** + * Set of category IDs. + */ +struct TMH_CategorySet +{ + /** + * Category IDs. + */ + uint64_t *ids; + + /** + * Number of entries in @e ids. + */ + unsigned int len; +}; + +/** + * Set of unit identifiers. + */ +struct TMH_UnitSet +{ + /** + * Unit identifiers. + */ + char **units; + + /** + * Number of entries in @e units. + */ + unsigned int len; +}; +/** + * Check if a category set already contains a given ID. + * + * @param set category set + * @param id category id + * @return true if present + */ +bool +TMH_category_set_contains (const struct TMH_CategorySet *set, + uint64_t id); + +/** + * Add a category ID to a set if not already present. + * + * @param set category set + * @param id category id + */ +void +TMH_category_set_add (struct TMH_CategorySet *set, + uint64_t id); + +/** + * Check if a unit set already contains a given unit. + * + * @param set unit set + * @param unit unit identifier + * @return true if present + */ +bool +TMH_unit_set_contains (const struct TMH_UnitSet *set, + const char *unit); + +/** + * Add a unit identifier to a set if not already present. + * + * @param set unit set + * @param unit unit identifier + */ +void +TMH_unit_set_add (struct TMH_UnitSet *set, + const char *unit); + +/** + * Clear a unit set and free its contents. + * + * @param set unit set to clear + */ +void +TMH_unit_set_clear (struct TMH_UnitSet *set); + +/** + * Extract a fixed-decimal number that may be supplied either + * - as pure integer (e.g. "total_stock"), or + * - as decimal text (e.g. "unit_total_stock"). + * + * Rules: + * - If both forms are missing -> error. + * - If both are present -> they must match and the decimal must have no fraction. + * - For kind == TMH_VK_STOCK the integer value -1 represents infinity. + * + * @param kind See #TMH_ValueKind + * @param allow_fractional False: any fractional part is rejected + * @param int_missing True if client omitted the integer field + * @param int_raw Raw integer (undefined if @a int_missing is true) + * @param str_missing True if client omitted the string field + * @param str_raw Raw UTF-8 string (undefined if @a str_missing is true) + * @param[out] int_out Canonicalised integer part + * @param[out] frac_out Canonicalised fractional part + * @param[out] error_param Set to offending field name on failure + * @param int_field Integer field name (for error reporting) + * @param str_field String field name (for error reporting) + * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise + */ +enum GNUNET_GenericReturnValue +TMH_process_quantity_inputs (enum TMH_ValueKind kind, + bool allow_fractional, + bool int_missing, + int64_t int_raw, + bool str_missing, + const char *str_raw, + uint64_t *int_out, + uint32_t *frac_out, + const char **error_param); + +/** * Lookup the defaults for @a unit within @a mi and fall back to sane * values (disallow fractional quantities, zero precision) if no data * is available. diff --git a/src/backend/taler-merchant-httpd_post-using-templates.c b/src/backend/taler-merchant-httpd_post-using-templates.c @@ -28,6 +28,7 @@ #include "taler-merchant-httpd_private-post-orders.h" #include "taler-merchant-httpd_helper.h" #include <taler/taler_json_lib.h> +#include <taler/taler_merchant_util.h> /** @@ -71,18 +72,153 @@ cleanup_use_context (void *cls) } -MHD_RESULT -TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +/** + * Check if a product ID appears in the selected_products list. + * + * @param selected_products JSON array of selected product IDs, may be NULL + * @param product_id product ID to check + * @return true if the product ID is selected + */ +static bool +product_id_selected (const json_t *selected_products, + const char *product_id) +{ + const json_t *entry; + size_t idx; + + if (NULL == selected_products) + return false; + json_array_foreach ((json_t *) selected_products, idx, entry) + { + if (! json_is_string (entry)) + continue; + if (0 == strcmp (json_string_value (entry), + product_id)) + return true; + } + return false; +} + + +/** + * Check if any product category is in the selected_categories list. + * + * @param selected_categories JSON array of selected category IDs, may be NULL + * @param num_categories length of @a categories + * @param categories list of category IDs for the product + * @return true if any category matches + */ +static bool +categories_selected (const json_t *selected_categories, + size_t num_categories, + const uint64_t *categories) +{ + const json_t *entry; + size_t idx; + + if (NULL == selected_categories) + return false; + json_array_foreach ((json_t *) selected_categories, idx, entry) + { + uint64_t selected_id; + + if (! json_is_integer (entry)) + continue; + if (0 > json_integer_value (entry)) + continue; + selected_id = (uint64_t) json_integer_value (entry); + for (size_t i = 0; i < num_categories; i++) + { + if (categories[i] == selected_id) + return true; + } + } + return false; +} + + +/** + * Compute the line total for a product based on quantity. + * + * @param unit_price price per unit + * @param quantity integer quantity + * @param quantity_frac fractional quantity (0..MERCHANT_UNIT_FRAC_BASE-1) + * @param[out] line_total resulting line total + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +compute_line_total (const struct TALER_Amount *unit_price, + uint64_t quantity, + uint32_t quantity_frac, + struct TALER_Amount *line_total) +{ + struct TALER_Amount tmp; + + if (quantity > UINT32_MAX) + return GNUNET_SYSERR; + if (GNUNET_OK != + TALER_amount_set_zero (unit_price->currency, + line_total)) + return GNUNET_SYSERR; + if (0 != quantity) + { + if (0 > + TALER_amount_multiply (line_total, + unit_price, + (uint32_t) quantity)) + return GNUNET_SYSERR; + } + if (0 != quantity_frac) + { + if (0 > + TALER_amount_multiply (&tmp, + unit_price, + quantity_frac)) + return GNUNET_SYSERR; + TALER_amount_divide (&tmp, + &tmp, + MERCHANT_UNIT_FRAC_BASE); + if (0 > + TALER_amount_add (line_total, + line_total, + &tmp)) + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Handle POST /templates/$ID for inventory templates. + * + * @param connection connection to reply on + * @param hc handler context + * @param uc use context + * @param summary summary override (optional) + * @param fulfillment_url fulfillment URL (optional) + * @param fulfillment_message fulfillment message (optional) + * @return MHD result + */ +static MHD_RESULT +handle_using_templates_inventory (struct MHD_Connection *connection, + struct TMH_HandlerContext *hc, + struct UseContext *uc, + const char *summary, + const char *fulfillment_url, + const char *fulfillment_message) { struct TMH_MerchantInstance *mi = hc->instance; - const char *template_id = hc->infix; - const char *summary = NULL; - const char *fulfillment_url = NULL; - const char *fulfillment_message = NULL; + const json_t *inventory_selection = NULL; + const json_t *selected_categories = NULL; + const json_t *selected_products = NULL; + const char *tsummary = NULL; struct TALER_Amount amount; + struct TALER_Amount tip; bool no_amount; + bool no_tip; + bool choose_one = false; + bool request_tip = false; + bool selected_all = false; bool no_summary; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( @@ -93,75 +229,449 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, TALER_JSON_spec_amount_any ("amount", &amount), &no_amount), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("tip", + &tip), + &no_tip), + GNUNET_JSON_spec_array_const ("inventory_selection", + &inventory_selection), GNUNET_JSON_spec_end () }; - struct UseContext *uc = hc->ctx; + enum GNUNET_GenericReturnValue res; + json_t *inventory_products; + json_t *tip_products = NULL; + struct TALER_Amount total; + bool total_init = false; - if (NULL == uc) + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) { - uc = GNUNET_new (struct UseContext); - hc->ctx = uc; - hc->cc = &cleanup_use_context; - uc->ihc.instance = hc->instance; + GNUNET_break_op (0); + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; } + if (no_amount) { - enum GNUNET_GenericReturnValue res; + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "amount"); + } + if ( (NULL == inventory_selection) || + (! json_is_array (inventory_selection)) ) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_selection"); + } - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); + { + const json_t *tsummary_val; + const json_t *selected_categories_val; + const json_t *selected_products_val; + const json_t *choose_one_val; + const json_t *request_tip_val; + const json_t *selected_all_val; + + tsummary_val = json_object_get (uc->etp.template_contract, + "summary"); + if (json_is_string (tsummary_val)) + tsummary = json_string_value (tsummary_val); + selected_categories_val = json_object_get (uc->etp.template_contract, + "selected_categories"); + if (json_is_array (selected_categories_val)) + selected_categories = selected_categories_val; + selected_products_val = json_object_get (uc->etp.template_contract, + "selected_products"); + if (json_is_array (selected_products_val)) + selected_products = selected_products_val; + choose_one_val = json_object_get (uc->etp.template_contract, + "choose_one"); + if (json_is_boolean (choose_one_val)) + choose_one = json_boolean_value (choose_one_val); + request_tip_val = json_object_get (uc->etp.template_contract, + "request_tip"); + if (json_is_boolean (request_tip_val)) + request_tip = json_boolean_value (request_tip_val); + selected_all_val = json_object_get (uc->etp.template_contract, + "selected_all"); + if (json_is_boolean (selected_all_val)) + selected_all = json_boolean_value (selected_all_val); + } + + if ( (NULL != summary) && + (NULL != tsummary) ) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT, + NULL); + } + if ( (NULL == summary) && + (NULL == tsummary) ) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY, + NULL); + } + no_summary = (NULL == summary); + + if (! no_tip && ! request_tip) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "tip"); + } + + if (choose_one && + (1 != json_array_size (inventory_selection))) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_selection"); + } + + inventory_products = json_array (); + GNUNET_assert (NULL != inventory_products); + for (size_t i = 0; i < json_array_size (inventory_selection); i++) + { + const char *product_id; + const char *quantity; + struct TALER_MERCHANTDB_ProductDetails pd; + enum GNUNET_DB_QueryStatus qs; + size_t num_categories = 0; + uint64_t *categories = NULL; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("product_id", + &product_id), + GNUNET_JSON_spec_string ("quantity", + &quantity), + GNUNET_JSON_spec_end () + }; + const char *err_name; + unsigned int err_line; + struct TALER_Amount line_total; + uint64_t quantity_value = 0; + uint32_t quantity_frac = 0; + const char *eparam = NULL; + + res = GNUNET_JSON_parse (json_array_get (inventory_selection, + i), + ispec, + &err_name, + &err_line); if (GNUNET_OK != res) { GNUNET_break_op (0); - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; + GNUNET_JSON_parse_free (spec); + json_decref (inventory_products); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_selection"); } - } - if (! uc->have_etp) - { - enum GNUNET_DB_QueryStatus qs; + qs = TMH_db->lookup_product (TMH_db->cls, + mi->settings.id, + product_id, + &pd, + &num_categories, + &categories); + if (qs <= 0) + { + enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + unsigned int http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - qs = TMH_db->lookup_template (TMH_db->cls, - mi->settings.id, - template_id, - &uc->etp); - switch (qs) + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ec = TALER_EC_GENERIC_DB_FETCH_FAILED; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + http_status = MHD_HTTP_NOT_FOUND; + ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_assert (0); + } + GNUNET_JSON_parse_free (spec); + json_decref (inventory_products); + return TALER_MHD_reply_with_error (connection, + http_status, + ec, + product_id); + } + + if (! selected_all) + { + bool allowed = false; + + if (product_id_selected (selected_products, + product_id)) + allowed = true; + else if (categories_selected (selected_categories, + num_categories, + categories)) + allowed = true; + + if (! allowed) + { + GNUNET_JSON_parse_free (spec); + json_decref (inventory_products); + GNUNET_free (categories); + TALER_MERCHANTDB_product_details_free (&pd); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_selection"); + } + } + + if (GNUNET_OK != + TMH_process_quantity_inputs (TMH_VK_QUANTITY, + pd.allow_fractional_quantity, + true, + 0, + false, + quantity, + &quantity_value, + &quantity_frac, + &eparam)) { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); GNUNET_JSON_parse_free (spec); + json_decref (inventory_products); + GNUNET_free (categories); + TALER_MERCHANTDB_product_details_free (&pd); 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: - /* this should be impossible (single select) */ - GNUNET_break (0); + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + eparam); + } + + if (GNUNET_OK != + compute_line_total (&pd.price, + quantity_value, + quantity_frac, + &line_total)) + { GNUNET_JSON_parse_free (spec); + json_decref (inventory_products); + GNUNET_free (categories); + TALER_MERCHANTDB_product_details_free (&pd); return TALER_MHD_reply_with_error ( connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* template not found! */ + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "quantity"); + } + + if (! total_init) + { + total = line_total; + total_init = true; + } + else + { + if (0 > + TALER_amount_add (&total, + &total, + &line_total)) + { + GNUNET_JSON_parse_free (spec); + json_decref (inventory_products); + GNUNET_free (categories); + TALER_MERCHANTDB_product_details_free (&pd); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + line_total.currency); + } + } + + GNUNET_assert (0 == + json_array_append_new ( + inventory_products, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_id", + product_id), + GNUNET_JSON_pack_string ("unit_quantity", + quantity)))); + GNUNET_free (categories); + TALER_MERCHANTDB_product_details_free (&pd); + } + + if (! total_init) + { + GNUNET_JSON_parse_free (spec); + json_decref (inventory_products); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "inventory_selection"); + } + + if (! no_tip) + { + if (0 > + TALER_amount_add (&total, + &total, + &tip)) + { GNUNET_JSON_parse_free (spec); + json_decref (inventory_products); return TALER_MHD_reply_with_error ( connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, - template_id); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* all good */ - uc->have_etp = true; - break; - } /* End of the switch */ + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + tip.currency); + } + tip_products = json_array (); + GNUNET_assert (0 == + json_array_append_new ( + tip_products, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("description", + "tip"), + GNUNET_JSON_pack_uint64 ("quantity", + 1), + TALER_JSON_pack_amount ("price", + &tip)))); + } + + if (0 != TALER_amount_cmp (&amount, + &total)) + { + GNUNET_JSON_parse_free (spec); + json_decref (inventory_products); + json_decref (tip_products); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + NULL); + } + + { + json_t *body; + + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("otp_id", + uc->etp.otp_id)), + GNUNET_JSON_pack_array_steal ("inventory_products", + inventory_products), + GNUNET_JSON_pack_object_steal ( + "order", + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &total), + GNUNET_JSON_pack_string ("summary", + no_summary + ? tsummary + : summary), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_url", + fulfillment_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_message", + fulfillment_message)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("products", + tip_products))))); + GNUNET_assert (NULL != body); + uc->ihc.request_body = body; } + GNUNET_JSON_parse_free (spec); + return TMH_private_post_orders ( + NULL, /* not even used */ + connection, + &uc->ihc); +} + + +/** + * Handle POST /templates/$ID for fixed-order templates. + * + * @param connection connection to reply on + * @param hc handler context + * @param uc use context + * @param summary summary override (optional) + * @param fulfillment_url fulfillment URL (optional) + * @param fulfillment_message fulfillment message (optional) + * @return MHD result + */ +static MHD_RESULT +handle_using_templates_fixed (struct MHD_Connection *connection, + struct TMH_HandlerContext *hc, + struct UseContext *uc, + const char *summary, + const char *fulfillment_url, + const char *fulfillment_message) +{ + struct TALER_Amount amount; + struct TALER_Amount tip; + bool no_amount; + bool no_tip; + bool no_summary; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("summary", + &summary), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("amount", + &amount), + &no_amount), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("tip", + &tip), + &no_tip), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + json_t *tip_products = NULL; + + 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; + } + if (NULL == uc->ihc.request_body) { /* template */ @@ -190,10 +700,8 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, &pay_duration), GNUNET_JSON_spec_end () }; - json_t *fake_body; { - enum GNUNET_GenericReturnValue res; const char *err_name; unsigned int err_line; @@ -250,6 +758,24 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, NULL); } + if (! no_tip) + { + const struct TALER_Amount *total_amount; + + total_amount = no_amount ? &tamount : &amount; + if (0 != TALER_amount_cmp_currency (&tip, + total_amount)) + { + GNUNET_JSON_parse_free (spec); + json_decref (tip_products); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + tip.currency); + } + } + if ( (NULL != summary) && (NULL != tsummary) ) { @@ -272,32 +798,50 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, NULL); } no_summary = (NULL == summary); - fake_body = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("otp_id", - uc->etp.otp_id)), - GNUNET_JSON_pack_object_steal ( - "order", - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - no_amount ? - &tamount : - &amount), - GNUNET_JSON_pack_string ("summary", - no_summary ? - tsummary : - summary), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "fulfillment_url", - fulfillment_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "fulfillment_message", - fulfillment_message)) - )) - ); - uc->ihc.request_body = fake_body; + + { + json_t *body; + if (! no_tip) + { + tip_products = json_array (); + GNUNET_assert (0 == + json_array_append_new ( + tip_products, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("description", + "tip"), + GNUNET_JSON_pack_uint64 ("quantity", + 1), + TALER_JSON_pack_amount ("price", + &tip)))); + } + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("otp_id", + uc->etp.otp_id)), + GNUNET_JSON_pack_object_steal ( + "order", + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + no_amount + ? &tamount + : &amount), + GNUNET_JSON_pack_string ("summary", + no_summary + ? tsummary + : summary), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_url", + fulfillment_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_message", + fulfillment_message)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("products", + tip_products))))); + GNUNET_assert (NULL != body); + uc->ihc.request_body = body; + } } return TMH_private_post_orders ( @@ -305,3 +849,126 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, connection, &uc->ihc); } + + +/** + * Handle POST /templates/$ID for paivana templates. + * + * @param connection connection to reply on + * @param hc handler context + * @param uc use context + * @param summary summary override (optional) + * @param fulfillment_url fulfillment URL (optional) + * @param fulfillment_message fulfillment message (optional) + * @return MHD result + */ +static MHD_RESULT +handle_using_templates_paivana (struct MHD_Connection *connection, + struct TMH_HandlerContext *hc, + struct UseContext *uc, + const char *summary, + const char *fulfillment_url, + const char *fulfillment_message) +{ + /* TODO: PAIVANA include paivana_id in instantiation flow. */ + return handle_using_templates_fixed (connection, + hc, + uc, + summary, + fulfillment_url, + fulfillment_message); +} + + +MHD_RESULT +TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *template_id = hc->infix; + const char *summary = NULL; + const char *fulfillment_url = NULL; + const char *fulfillment_message = NULL; + struct UseContext *uc = hc->ctx; + + if (NULL == uc) + { + uc = GNUNET_new (struct UseContext); + hc->ctx = uc; + hc->cc = &cleanup_use_context; + uc->ihc.instance = hc->instance; + } + + if (! uc->have_etp) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_template (TMH_db->cls, + mi->settings.id, + template_id, + &uc->etp); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + /* Clean up and fail hard */ + 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: + /* this should be impossible (single select) */ + 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_SUCCESS_NO_RESULTS: + /* template not found! */ + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, + template_id); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* all good */ + uc->have_etp = true; + break; + } /* End of the switch */ + } + switch (TMH_template_type_from_contract (uc->etp.template_contract)) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + return handle_using_templates_fixed (connection, + hc, + uc, + summary, + fulfillment_url, + fulfillment_message); + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + /* TODO: PAIVANA handle paivana-specific instantiation. */ + return handle_using_templates_paivana (connection, + hc, + uc, + summary, + fulfillment_url, + fulfillment_message); + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + return handle_using_templates_inventory (connection, + hc, + uc, + summary, + fulfillment_url, + fulfillment_message); + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + break; + } + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "template_contract"); +} diff --git a/src/backend/taler-merchant-httpd_private-patch-templates-ID.c b/src/backend/taler-merchant-httpd_private-patch-templates-ID.c @@ -87,6 +87,28 @@ determine_cause (struct MHD_Connection *connection, /** + * Validate template contract based on its type. + * + * @param template_contract JSON template contract + * @return true if valid + */ +static bool +validate_template_contract_by_type (const json_t *template_contract) +{ + switch (TMH_template_type_from_contract (template_contract)) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + return TMH_template_contract_valid (template_contract); + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + break; + } + return false; +} + + +/** * PATCH configuration of an existing instance, given its configuration. * * @param rh context of the handler @@ -133,7 +155,7 @@ TMH_private_patch_templates_ID (const struct TMH_RequestHandler *rh, : MHD_NO; } - if (! TMH_template_contract_valid (tp.template_contract)) + if (! validate_template_contract_by_type (tp.template_contract)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); diff --git a/src/backend/taler-merchant-httpd_private-post-templates.c b/src/backend/taler-merchant-httpd_private-post-templates.c @@ -58,6 +58,30 @@ templates_equal (const struct TALER_MERCHANTDB_TemplateDetails *t1, } +/** + * Validate template contract based on its type. + * + * @param template_contract JSON template contract + * @return true if valid + */ +static bool +validate_template_contract_by_type (const json_t *template_contract) +{ + /* TODO: I still think helper in main can be divided, + plus we might want to maintain some flexibility */ + switch (TMH_template_type_from_contract (template_contract)) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + return TMH_template_contract_valid (template_contract); + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + break; + } + return false; +} + + MHD_RESULT TMH_private_post_templates (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, @@ -101,7 +125,7 @@ TMH_private_post_templates (const struct TMH_RequestHandler *rh, : MHD_NO; } } - if (! TMH_template_contract_valid (tp.template_contract)) + if (! validate_template_contract_by_type (tp.template_contract)) { GNUNET_break_op (0); json_dumpf (tp.template_contract, diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am @@ -144,6 +144,7 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_insert_webhook.h pg_insert_webhook.c \ pg_delete_unit.h pg_delete_unit.c \ pg_lookup_units.h pg_lookup_units.c \ + pg_lookup_custom_units_by_names.h pg_lookup_custom_units_by_names.c \ pg_select_unit.h pg_select_unit.c \ pg_lookup_mfa_challenge.h pg_lookup_mfa_challenge.c \ pg_solve_mfa_challenge.h pg_solve_mfa_challenge.c \ @@ -171,6 +172,7 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_lookup_account.h pg_lookup_account.c \ pg_lookup_all_products.h pg_lookup_all_products.c \ pg_lookup_categories.h pg_lookup_categories.c \ + pg_lookup_categories_by_ids.h pg_lookup_categories_by_ids.c \ pg_lookup_contract_terms.h pg_lookup_contract_terms.c \ pg_lookup_contract_terms2.h pg_lookup_contract_terms2.c \ pg_lookup_contract_terms3.h pg_lookup_contract_terms3.c \ @@ -180,6 +182,8 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_lookup_expected_transfers.h pg_lookup_expected_transfers.c \ pg_lookup_instance_auth.h pg_lookup_instance_auth.c \ pg_lookup_instances.h pg_lookup_instances.c \ + pg_lookup_inventory_products.h pg_lookup_inventory_products.c \ + pg_lookup_inventory_products_filtered.h pg_lookup_inventory_products_filtered.c \ pg_lookup_login_tokens.h pg_lookup_login_tokens.c \ pg_lookup_order.h pg_lookup_order.c \ pg_lookup_order_by_fulfillment.h pg_lookup_order_by_fulfillment.c \ diff --git a/src/backenddb/merchantdb_helper.c b/src/backenddb/merchantdb_helper.c @@ -36,6 +36,7 @@ TALER_MERCHANTDB_product_details_free ( json_decref (pd->taxes); pd->taxes = NULL; GNUNET_free (pd->image); + GNUNET_free (pd->image_hash); json_decref (pd->address); pd->address = NULL; GNUNET_free (pd->price_array); diff --git a/src/backenddb/pg_insert_template.c b/src/backenddb/pg_insert_template.c @@ -47,6 +47,7 @@ TMH_PG_insert_template (void *cls, : TALER_PQ_query_param_json (td->editable_defaults), GNUNET_PQ_query_param_end }; + enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, @@ -63,7 +64,9 @@ TMH_PG_insert_template (void *cls, " $2, $3, $4, $5::TEXT::JSONB, $6::TEXT::JSONB" " FROM merchant_instances" " WHERE merchant_id=$1"); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_template", - params); + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "insert_template", + params); + GNUNET_PQ_cleanup_query_params_closures (params); + return qs; } diff --git a/src/backenddb/pg_lookup_all_products.c b/src/backenddb/pg_lookup_all_products.c @@ -146,6 +146,7 @@ lookup_products_cb (void *cls, plc->extract_failed = true; return; } + pd.image_hash = NULL; plc->cb (plc->cb_cls, product_serial, product_id, diff --git a/src/backenddb/pg_lookup_categories_by_ids.c b/src/backenddb/pg_lookup_categories_by_ids.c @@ -0,0 +1,145 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_by_ids.c + * @brief Lookup product categories by ID + * @author Bohdan Potuzhnyi + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_categories_by_ids.h" +#include "pg_helper.h" + + +/** + * Context used for TMH_PG_lookup_categories_by_ids(). + */ +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; +}; + + +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_by_ids (void *cls, + const char *instance_id, + const uint64_t *category_ids, + size_t num_category_ids, + 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_array_uint64 (num_category_ids, + category_ids, + pg->conn), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_categories_by_ids", + "SELECT" + " mc.category_serial" + ",mc.category_name" + ",mc.category_name_i18n::TEXT" + ",COALESCE(COUNT(mpc.product_serial),0)" + " AS product_count" + " FROM merchant_categories mc" + " LEFT JOIN merchant_product_categories mpc" + " USING (category_serial)" + " JOIN merchant_instances mi" + " USING (merchant_serial)" + " WHERE mi.merchant_id=$1" + " AND mc.category_serial = ANY ($2)" + " GROUP BY mc.category_serial" + " ORDER BY mc.category_serial;"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "lookup_categories_by_ids", + params, + &lookup_categories_cb, + &tlc); + if (tlc.extract_failed) + return GNUNET_DB_STATUS_HARD_ERROR; + return qs; +} diff --git a/src/backenddb/pg_lookup_categories_by_ids.h b/src/backenddb/pg_lookup_categories_by_ids.h @@ -0,0 +1,45 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_by_ids.h + * @brief Lookup product categories by ID + * @author Bohdan Potuzhnyi + */ +#ifndef PG_LOOKUP_CATEGORIES_BY_IDS_H +#define PG_LOOKUP_CATEGORIES_BY_IDS_H + +#include "taler_merchantdb_plugin.h" + +/** + * Lookup product categories by ID. + * + * @param cls closure + * @param instance_id instance to lookup categories for + * @param category_ids array of category IDs + * @param num_category_ids length of @a category_ids + * @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_by_ids (void *cls, + const char *instance_id, + const uint64_t *category_ids, + size_t num_category_ids, + TALER_MERCHANTDB_CategoriesCallback cb, + void *cb_cls); + +#endif diff --git a/src/backenddb/pg_lookup_custom_units_by_names.c b/src/backenddb/pg_lookup_custom_units_by_names.c @@ -0,0 +1,142 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_custom_units_by_names.c + * @brief Lookup custom measurement units by name + * @author Bohdan Potuzhnyi + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_custom_units_by_names.h" +#include "pg_helper.h" + + +/** + * Context used for TMH_PG_lookup_custom_units_by_names(). + */ +struct LookupUnitsContext +{ + TALER_MERCHANTDB_UnitsCallback cb; + void *cb_cls; + bool extract_failed; +}; + + +static void +lookup_units_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupUnitsContext *luc = cls; + + for (unsigned int i = 0; i<num_results; i++) + { + struct TALER_MERCHANTDB_UnitDetails ud = { 0 }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("unit_serial", + &ud.unit_serial), + GNUNET_PQ_result_spec_string ("unit", + &ud.unit), + GNUNET_PQ_result_spec_string ("unit_name_long", + &ud.unit_name_long), + GNUNET_PQ_result_spec_string ("unit_name_short", + &ud.unit_name_short), + TALER_PQ_result_spec_json ("unit_name_long_i18n", + &ud.unit_name_long_i18n), + TALER_PQ_result_spec_json ("unit_name_short_i18n", + &ud.unit_name_short_i18n), + GNUNET_PQ_result_spec_bool ("unit_allow_fraction", + &ud.unit_allow_fraction), + GNUNET_PQ_result_spec_uint32 ("unit_precision_level", + &ud.unit_precision_level), + GNUNET_PQ_result_spec_bool ("unit_active", + &ud.unit_active), + GNUNET_PQ_result_spec_bool ("unit_builtin", + &ud.unit_builtin), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + luc->extract_failed = true; + return; + } + luc->cb (luc->cb_cls, + ud.unit_serial, + &ud); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_custom_units_by_names (void *cls, + const char *instance_id, + const char *const *units, + size_t num_units, + TALER_MERCHANTDB_UnitsCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupUnitsContext luc = { + .cb = cb, + .cb_cls = cb_cls, + .extract_failed = false + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_array_ptrs_string (num_units, + (const char **) units, + pg->conn), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_custom_units_by_names", + "WITH mi AS (" + " SELECT merchant_serial FROM merchant_instances WHERE merchant_id=$1" + ")" + "SELECT cu.unit_serial" + " ,cu.unit" + " ,cu.unit_name_long" + " ,cu.unit_name_short" + " ,cu.unit_name_long_i18n" + " ,cu.unit_name_short_i18n" + " ,cu.unit_allow_fraction" + " ,cu.unit_precision_level" + " ,cu.unit_active" + " ,FALSE AS unit_builtin" + " FROM merchant_custom_units cu" + " JOIN mi ON cu.merchant_serial = mi.merchant_serial" + " WHERE cu.unit = ANY ($2)" + " ORDER BY cu.unit"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "lookup_custom_units_by_names", + params, + &lookup_units_cb, + &luc); + if (luc.extract_failed) + return GNUNET_DB_STATUS_HARD_ERROR; + return qs; +} diff --git a/src/backenddb/pg_lookup_custom_units_by_names.h b/src/backenddb/pg_lookup_custom_units_by_names.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_custom_units_by_names.h + * @brief Lookup custom measurement units by name + * @author Bohdan Potuzhnyi + */ +#ifndef PG_LOOKUP_CUSTOM_UNITS_BY_NAMES_H +#define PG_LOOKUP_CUSTOM_UNITS_BY_NAMES_H + +#include <taler/taler_util.h> +#include "taler_merchantdb_plugin.h" + +/** + * Lookup custom measurement units by name. + * + * @param cls closure + * @param instance_id instance to fetch units for + * @param units array of unit identifiers + * @param num_units length of @a units + * @param cb function to call with each unit + * @param cb_cls closure for @a cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_custom_units_by_names (void *cls, + const char *instance_id, + const char *const *units, + size_t num_units, + TALER_MERCHANTDB_UnitsCallback cb, + void *cb_cls); + +#endif diff --git a/src/backenddb/pg_lookup_inventory_products.c b/src/backenddb/pg_lookup_inventory_products.c @@ -0,0 +1,188 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_inventory_products.c + * @brief Implementation of the lookup_inventory_products function for Postgres + * @author Bohdan Potuzhnyi + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_inventory_products.h" +#include "pg_helper.h" + +/** + * Context used for TMH_PG_lookup_inventory_products(). + */ +struct LookupInventoryProductsContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_InventoryProductCallback 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 LookupInventoryProductsContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_inventory_products_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupInventoryProductsContext *plc = cls; + struct PostgresClosure *pg = plc->pg; + + for (unsigned int i = 0; i < num_results; i++) + { + char *product_id; + struct TALER_MERCHANTDB_InventoryProductDetails pd; + size_t num_categories; + uint64_t *categories; + bool no_image_hash; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("product_id", + &product_id), + GNUNET_PQ_result_spec_string ("product_name", + &pd.product_name), + 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_array_amount_with_currency (pg->conn, + "price_array", + &pd.price_array_length, + &pd.price_array), + TALER_PQ_result_spec_json ("taxes", + &pd.taxes), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("image_hash", + &pd.image_hash), + &no_image_hash), + GNUNET_PQ_result_spec_bool ("allow_fractional_quantity", + &pd.allow_fractional_quantity), + GNUNET_PQ_result_spec_uint32 ("fractional_precision_level", + &pd.fractional_precision_level), + 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_id, + &pd, + num_categories, + categories); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_inventory_products (void *cls, + const char *instance_id, + TALER_MERCHANTDB_InventoryProductCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupInventoryProductsContext plc = { + .cb = cb, + .cb_cls = cb_cls, + .pg = pg, + /* Can be overwritten by the lookup_inventory_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_inventory_products", + "SELECT" + " description" + ",description_i18n::TEXT" + ",product_name" + ",unit" + ",price" + ",price_array" + ",taxes::TEXT" + ",image_hash" + ",allow_fractional_quantity" + ",fractional_precision_level" + ",product_id" + ",t.category_array AS categories" + " FROM merchant_inventory minv" + " JOIN merchant_instances inst" + " USING (merchant_serial)" + ",LATERAL (" + " SELECT ARRAY (" + " SELECT mpc.category_serial" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = minv.product_serial" + " ) AS category_array" + " ) t" + " WHERE inst.merchant_id=$1"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "lookup_inventory_products", + params, + &lookup_inventory_products_cb, + &plc); + /* If there was an error inside lookup_inventory_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_inventory_products.h b/src/backenddb/pg_lookup_inventory_products.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_inventory_products.h + * @brief Lookup inventory product details for templates + * @author Bohdan Potuzhnyi + */ +#ifndef PG_LOOKUP_INVENTORY_PRODUCTS_H +#define PG_LOOKUP_INVENTORY_PRODUCTS_H + +#include "taler_merchantdb_plugin.h" + +/** + * Lookup inventory details for all products of an instance. + * + * @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 +TMH_PG_lookup_inventory_products (void *cls, + const char *instance_id, + TALER_MERCHANTDB_InventoryProductCallback cb, + void *cb_cls); + +#endif /* PG_LOOKUP_INVENTORY_PRODUCTS_H */ diff --git a/src/backenddb/pg_lookup_inventory_products_filtered.c b/src/backenddb/pg_lookup_inventory_products_filtered.c @@ -0,0 +1,210 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_inventory_products_filtered.c + * @brief Lookup inventory product details for templates (filtered) + * @author Bohdan Potuzhnyi + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_inventory_products_filtered.h" +#include "pg_helper.h" + + +/** + * Context used for TMH_PG_lookup_inventory_products_filtered(). + */ +struct LookupInventoryProductsContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_InventoryProductCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Postgres context for callbacks. + */ + struct PostgresClosure *pg; + + /** + * Did database result extraction fail? + */ + bool extract_failed; +}; + + +static void +lookup_inventory_products_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupInventoryProductsContext *plc = cls; + struct PostgresClosure *pg = plc->pg; + + for (unsigned int i = 0; i < num_results; i++) + { + char *product_id; + struct TALER_MERCHANTDB_InventoryProductDetails pd; + size_t num_categories; + uint64_t *categories; + bool no_image_hash; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("product_id", + &product_id), + GNUNET_PQ_result_spec_string ("product_name", + &pd.product_name), + 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_array_amount_with_currency (pg->conn, + "price_array", + &pd.price_array_length, + &pd.price_array), + TALER_PQ_result_spec_json ("taxes", + &pd.taxes), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("image_hash", + &pd.image_hash), + &no_image_hash), + GNUNET_PQ_result_spec_bool ("allow_fractional_quantity", + &pd.allow_fractional_quantity), + GNUNET_PQ_result_spec_uint32 ("fractional_precision_level", + &pd.fractional_precision_level), + 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_id, + &pd, + num_categories, + categories); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_inventory_products_filtered ( + void *cls, + const char *instance_id, + const char *const *product_ids, + size_t num_product_ids, + const uint64_t *categories, + size_t num_categories, + TALER_MERCHANTDB_InventoryProductCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupInventoryProductsContext plc = { + .cb = cb, + .cb_cls = cb_cls, + .pg = pg, + /* Can be overwritten by the lookup_inventory_products_cb */ + .extract_failed = false, + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + (0 == num_product_ids) + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_array_ptrs_string ( + num_product_ids, + (const char **) product_ids, + pg->conn), + (0 == num_categories) + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_array_uint64 (num_categories, + categories, + pg->conn), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_inventory_products_filtered", + "SELECT" + " description" + ",description_i18n::TEXT" + ",product_name" + ",unit" + ",price" + ",price_array" + ",taxes::TEXT" + ",image_hash" + ",allow_fractional_quantity" + ",fractional_precision_level" + ",product_id" + ",t.category_array AS categories" + " FROM merchant_inventory minv" + " JOIN merchant_instances inst" + " USING (merchant_serial)" + ",LATERAL (" + " SELECT ARRAY (" + " SELECT mpc.category_serial" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = minv.product_serial" + " ) AS category_array" + " ) t" + " WHERE inst.merchant_id=$1" + " AND (" + " (COALESCE (array_length ($2::TEXT[], 1), 0) > 0" + " AND minv.product_id = ANY ($2::TEXT[]))" + " OR" + " (COALESCE (array_length ($3::BIGINT[], 1), 0) > 0" + " AND EXISTS (" + " SELECT 1" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = minv.product_serial" + " AND mpc.category_serial = ANY ($3::BIGINT[])" + " ))" + " )"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "lookup_inventory_products_filtered", + params, + &lookup_inventory_products_cb, + &plc); + GNUNET_PQ_cleanup_query_params_closures (params); + /* If there was an error inside lookup_inventory_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_inventory_products_filtered.h b/src/backenddb/pg_lookup_inventory_products_filtered.h @@ -0,0 +1,50 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_inventory_products_filtered.h + * @brief Lookup inventory product details for templates (filtered) + * @author Bohdan Potuzhnyi + */ +#ifndef PG_LOOKUP_INVENTORY_PRODUCTS_FILTERED_H +#define PG_LOOKUP_INVENTORY_PRODUCTS_FILTERED_H + +#include "taler_merchantdb_plugin.h" + +/** + * Lookup inventory details for a subset of products. + * + * @param cls closure + * @param instance_id instance to lookup products for + * @param product_ids product IDs to include (can be NULL/empty) + * @param num_product_ids number of entries in @a product_ids + * @param categories category IDs to include (can be NULL/empty) + * @param num_categories number of entries in @a categories + * @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_inventory_products_filtered ( + void *cls, + const char *instance_id, + const char *const *product_ids, + size_t num_product_ids, + const uint64_t *categories, + size_t num_categories, + TALER_MERCHANTDB_InventoryProductCallback cb, + void *cb_cls); + +#endif /* PG_LOOKUP_INVENTORY_PRODUCTS_FILTERED_H */ diff --git a/src/backenddb/pg_lookup_product.c b/src/backenddb/pg_lookup_product.c @@ -173,6 +173,7 @@ TMH_PG_lookup_product (void *cls, pd->unit = my_unit; pd->taxes = my_taxes; pd->image = my_image; + pd->image_hash = NULL; pd->address = my_address; pd->price_array = my_price_array; pd->price_array_length = my_price_array_length; diff --git a/src/backenddb/pg_lookup_template.c b/src/backenddb/pg_lookup_template.c @@ -48,6 +48,7 @@ TMH_PG_lookup_template (void *cls, GNUNET_PQ_query_param_string (template_id), GNUNET_PQ_query_param_end }; + enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, @@ -70,10 +71,12 @@ TMH_PG_lookup_template (void *cls, GNUNET_PQ_result_spec_end }; - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_template", - params, - rs_null); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "lookup_template", + params, + rs_null); + GNUNET_PQ_cleanup_query_params_closures (params); + return qs; } else { @@ -96,9 +99,11 @@ TMH_PG_lookup_template (void *cls, memset (td, 0, sizeof (*td)); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_template", - params, - rs); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "lookup_template", + params, + rs); + GNUNET_PQ_cleanup_query_params_closures (params); + return qs; } } diff --git a/src/backenddb/pg_update_template.c b/src/backenddb/pg_update_template.c @@ -46,6 +46,7 @@ TMH_PG_update_template (void *cls, : TALER_PQ_query_param_json (td->editable_defaults), GNUNET_PQ_query_param_end }; + enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, @@ -70,7 +71,9 @@ TMH_PG_update_template (void *cls, " (SELECT merchant_serial" " FROM mid)" " AND template_id=$2"); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_template", - params); + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "update_template", + params); + GNUNET_PQ_cleanup_query_params_closures (params); + return qs; } diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -50,7 +50,9 @@ #include "pg_lookup_transfers.h" #include "pg_lookup_pending_deposits.h" #include "pg_lookup_categories.h" +#include "pg_lookup_categories_by_ids.h" #include "pg_lookup_units.h" +#include "pg_lookup_custom_units_by_names.h" #include "pg_select_category.h" #include "pg_update_category.h" #include "pg_insert_category.h" @@ -103,6 +105,8 @@ #include "pg_activate_account.h" #include "pg_lookup_products.h" #include "pg_lookup_all_products.h" +#include "pg_lookup_inventory_products.h" +#include "pg_lookup_inventory_products_filtered.h" #include "pg_lookup_product.h" #include "pg_lookup_product_image.h" #include "pg_lookup_statistics_amount_by_bucket2.h" @@ -489,6 +493,10 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_lookup_products; plugin->lookup_all_products = &TMH_PG_lookup_all_products; + plugin->lookup_inventory_products + = &TMH_PG_lookup_inventory_products; + plugin->lookup_inventory_products_filtered + = &TMH_PG_lookup_inventory_products_filtered; plugin->lookup_product = &TMH_PG_lookup_product; plugin->lookup_product_image_by_hash @@ -649,8 +657,12 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_update_pending_webhook; plugin->lookup_categories = &TMH_PG_lookup_categories; + plugin->lookup_categories_by_ids + = &TMH_PG_lookup_categories_by_ids; plugin->lookup_units = &TMH_PG_lookup_units; + plugin->lookup_custom_units_by_names + = &TMH_PG_lookup_custom_units_by_names; plugin->select_category_by_name = &TMH_PG_select_category_by_name; plugin->get_kyc_status diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -1055,7 +1055,7 @@ TALER_MERCHANT_instance_delete_cancel ( * @param arg request to cancel. */ #define TALER_MERCHANT_instance_purge_cancel(arg) \ - TALER_MERCHANT_instance_delete_cancel (arg) + TALER_MERCHANT_instance_delete_cancel (arg) /* *************** Accounts **************** */ @@ -2020,6 +2020,67 @@ TALER_MERCHANT_products_post4 ( TALER_MERCHANT_ProductsPostCallback cb, void *cb_cls); +/** + * Response from a POST /private/categories request. + */ +struct TALER_MERCHANT_CategoriesPostResponse +{ + /** + * HTTP response details. + */ + struct TALER_MERCHANT_HttpResponse hr; + + /** + * Category id on success. + */ + uint64_t category_id; +}; + +/** + * Callback for POST /private/categories. + * + * @param cls closure + * @param cpr response details + */ +typedef void +(*TALER_MERCHANT_CategoriesPostCallback)( + void *cls, + const struct TALER_MERCHANT_CategoriesPostResponse *cpr); + +/** + * Handle for a POST /private/categories operation. + */ +struct TALER_MERCHANT_CategoriesPostHandle; + +/** + * Make a POST /private/categories request to add a category. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend + * @param name category name + * @param name_i18n Map from IETF BCP 47 language tags to localized names + * @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_CategoriesPostHandle * +TALER_MERCHANT_categories_post ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *name, + const json_t *name_i18n, + TALER_MERCHANT_CategoriesPostCallback cb, + void *cb_cls); + +/** + * Cancel POST /private/categories operation. + * + * @param cph operation to cancel + */ +void +TALER_MERCHANT_categories_post_cancel ( + struct TALER_MERCHANT_CategoriesPostHandle *cph); + /** * Cancel POST /products operation. @@ -5951,6 +6012,27 @@ TALER_MERCHANT_using_templates_post ( TALER_MERCHANT_PostOrdersCallback cb, void *cb_cls); +/** + * Make a POST /templates/$ID request to instantiate a template + * with a raw JSON payload. Useful for inventory templates. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend + * @param template_id identifier of the template to use + * @param details JSON payload to send as request body + * @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_UsingTemplatesPostHandle * +TALER_MERCHANT_using_templates_post2 ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *template_id, + const json_t *details, + TALER_MERCHANT_PostOrdersCallback cb, + void *cb_cls); + /** * Cancel POST /using-templates operation. diff --git a/src/include/taler_merchant_testing_lib.h b/src/include/taler_merchant_testing_lib.h @@ -38,7 +38,7 @@ #define MERCHANT_FAIL() \ - do {GNUNET_break (0); return NULL; } while (0) + do {GNUNET_break (0); return NULL; } while (0) /** @@ -434,6 +434,86 @@ TALER_TESTING_cmd_merchant_post_products (const char *label, const char *price, unsigned int http_status); +/** + * Define a "POST /products" CMD with category IDs. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the + * POST /products request. + * @param product_id the ID of the product to create + * @param description name of the product + * @param price price of the product + * @param num_cats length of the @a cats array + * @param cats array of category IDs + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_products_with_categories ( + const char *label, + const char *merchant_url, + const char *product_id, + const char *description, + const char *unit, + const char *price, + unsigned int num_cats, + const uint64_t *cats, + bool unit_allow_fraction, + uint32_t unit_precision_level, + unsigned int http_status); + +/** + * Define a "POST /private/categories" CMD. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the request. + * @param name category name + * @param name_i18n category name translations + * @param expected_category_id expected category id (0 to ignore) + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_categories ( + const char *label, + const char *merchant_url, + const char *name, + json_t *name_i18n, + uint64_t expected_category_id, + unsigned int http_status); + +/** + * Define a "GET /templates/$ID" wallet CMD. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the request. + * @param template_id template ID to fetch + * @param expected_products_len expected number of products (0 to ignore) + * @param expected_product_id first expected product id (can be NULL) + * @param expected_unit expected unit for @a expected_product_id (can be NULL) + * @param expected_unit_allow_fraction expected fractional flag + * @param expected_unit_precision_level expected precision + * @param expected_product_id2 second expected product id (can be NULL) + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_wallet_get_template ( + const char *label, + const char *merchant_url, + const char *template_id, + size_t expected_products_len, + const char *expected_product_id, + const char *expected_unit, + bool expected_unit_allow_fraction, + uint32_t expected_unit_precision_level, + json_t *expected_unit_name_short_i18n, + const char *expected_product_id2, + const char *expected_product_id3, + uint64_t expected_category_id1, + uint64_t expected_category_id2, + unsigned int http_status); + /** * Define a "PATCH /products/$ID" CMD. @@ -1877,6 +1957,29 @@ TALER_TESTING_cmd_merchant_post_using_templates ( struct GNUNET_TIME_Timestamp pay_deadline, unsigned int http_status); +/** + * Define a "POST /using-templates" CMD with a raw JSON request body. + * + * @param label command label. + * @param template_ref label of command that created the template to use + * @param otp_ref label of command that created OTP device we use (or NULL for no OTP) + * @param merchant_url base URL of the merchant serving the + * POST /using-templates request. + * @param using_template_id template ID to use + * @param details raw JSON request body to send + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_using_templates2 ( + const char *label, + const char *template_ref, + const char *otp_ref, + const char *merchant_url, + const char *using_template_id, + const json_t *details, + unsigned int http_status); + /* ****** Token Families ******* */ @@ -2270,65 +2373,66 @@ TALER_TESTING_cmd_exec_donaukeyupdate (const char *label, */ // FIXME: rename: refund_entry->refund_detail #define TALER_MERCHANT_TESTING_SIMPLE_TRAITS(op) \ - op (claim_nonce, const struct GNUNET_CRYPTO_EddsaPublicKey) \ - op (pickup_id, const struct TALER_PickupIdentifierP) \ - op (instance_name, const char) \ - op (instance_id, const char) \ - op (address, const json_t) \ - op (product_description, const char) \ - op (product_image, const char) \ - op (product_stock, const int64_t) \ - op (product_unit_total_stock, const char) \ - op (product_unit_precision_level, const uint32_t) \ - op (product_unit_allow_fraction, const bool) \ - op (product_unit, const char) \ - op (product_id, const char) \ - op (unit_id, const char) \ - op (unit_name_long, const char) \ - op (unit_name_short, const char) \ - op (unit_allow_fraction, const bool) \ - op (unit_precision_level, const uint32_t) \ - op (unit_active, const bool) \ - op (unit_builtin, const bool) \ - op (unit_name_long_i18n, const json_t) \ - op (unit_name_short_i18n, const json_t) \ - op (reason, const char) \ - op (lock_uuid, const char) \ - op (auth_token, const char) \ - op (bearer_token, const char) \ - op (paths_length, const uint32_t) \ - op (payto_length, const uint32_t) \ - op (num_planchets, const uint32_t) \ - op (i18n_description, const json_t) \ - op (taxes, const json_t) \ - op (fee, const struct TALER_Amount) \ - op (use_stefan, const bool) \ - op (jurisdiction, const json_t) \ - op (wire_delay, const struct GNUNET_TIME_Relative) \ - op (pay_delay, const struct GNUNET_TIME_Relative) \ - op (refund_entry, const struct TALER_MERCHANT_RefundDetail) \ - op (order_terms, const json_t) \ - op (h_contract_terms, const struct TALER_PrivateContractHashP) \ - op (h_wire, const struct TALER_MerchantWireHashP) \ - op (proposal_reference, const char) \ - op (template_description, const char) \ - op (otp_device_description, const char) \ - op (otp_id, const char) \ - op (otp_key, const char) \ - op (otp_alg, const enum TALER_MerchantConfirmationAlgorithm) \ - op (template_id, const char) \ - op (template_contract, const json_t) \ - op (event_type, const char) \ - op (webhook_id, const char) \ - op (merchant_base_url, const char) \ - op (url, const char) \ - op (http_method, const char) \ - op (header_template, const char) \ - op (body_template, const char) \ - op (summary, const char) \ - op (token_family_slug, const char) \ - op (token_family_duration, const struct GNUNET_TIME_Relative) \ - op (token_family_kind, const char) + op (claim_nonce, const struct GNUNET_CRYPTO_EddsaPublicKey) \ + op (pickup_id, const struct TALER_PickupIdentifierP) \ + op (instance_name, const char) \ + op (instance_id, const char) \ + op (address, const json_t) \ + op (category_id, const uint64_t) \ + op (product_description, const char) \ + op (product_image, const char) \ + op (product_stock, const int64_t) \ + op (product_unit_total_stock, const char) \ + op (product_unit_precision_level, const uint32_t) \ + op (product_unit_allow_fraction, const bool) \ + op (product_unit, const char) \ + op (product_id, const char) \ + op (unit_id, const char) \ + op (unit_name_long, const char) \ + op (unit_name_short, const char) \ + op (unit_allow_fraction, const bool) \ + op (unit_precision_level, const uint32_t) \ + op (unit_active, const bool) \ + op (unit_builtin, const bool) \ + op (unit_name_long_i18n, const json_t) \ + op (unit_name_short_i18n, const json_t) \ + op (reason, const char) \ + op (lock_uuid, const char) \ + op (auth_token, const char) \ + op (bearer_token, const char) \ + op (paths_length, const uint32_t) \ + op (payto_length, const uint32_t) \ + op (num_planchets, const uint32_t) \ + op (i18n_description, const json_t) \ + op (taxes, const json_t) \ + op (fee, const struct TALER_Amount) \ + op (use_stefan, const bool) \ + op (jurisdiction, const json_t) \ + op (wire_delay, const struct GNUNET_TIME_Relative) \ + op (pay_delay, const struct GNUNET_TIME_Relative) \ + op (refund_entry, const struct TALER_MERCHANT_RefundDetail) \ + op (order_terms, const json_t) \ + op (h_contract_terms, const struct TALER_PrivateContractHashP) \ + op (h_wire, const struct TALER_MerchantWireHashP) \ + op (proposal_reference, const char) \ + op (template_description, const char) \ + op (otp_device_description, const char) \ + op (otp_id, const char) \ + op (otp_key, const char) \ + op (otp_alg, const enum TALER_MerchantConfirmationAlgorithm) \ + op (template_id, const char) \ + op (template_contract, const json_t) \ + op (event_type, const char) \ + op (webhook_id, const char) \ + op (merchant_base_url, const char) \ + op (url, const char) \ + op (http_method, const char) \ + op (header_template, const char) \ + op (body_template, const char) \ + op (summary, const char) \ + op (token_family_slug, const char) \ + op (token_family_duration, const struct GNUNET_TIME_Relative) \ + op (token_family_kind, const char) /** diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h @@ -24,6 +24,7 @@ #include <gnunet/gnunet_common.h> #include <gnunet/gnunet_util_lib.h> #include <gnunet/gnunet_json_lib.h> +#include <string.h> #include <stdint.h> #include <taler/taler_util.h> #include <jansson.h> @@ -206,6 +207,40 @@ enum TALER_MERCHANT_MFA_CriticalOperation }; +/** + * Template type discriminator. + */ +enum TALER_MERCHANT_TemplateType +{ + TALER_MERCHANT_TEMPLATE_TYPE_INVALID = 0, + TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER, + TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART, + TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA +}; + +/** + * Determine template type from string. + * + * @param template_type string value (NULL means fixed-order) + * @return template type (defaults to fixed order) + */ +static inline enum TALER_MERCHANT_TemplateType +TALER_MERCHANT_template_type_from_string (const char *template_type) +{ + if (NULL == template_type) + return TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER; + if (0 == strcmp (template_type, + "fixed-order")) + return TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER; + if (0 == strcmp (template_type, + "inventory-cart")) + return TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART; + if (0 == strcmp (template_type, + "paivana")) + return TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA; + return TALER_MERCHANT_TEMPLATE_TYPE_INVALID; +} + /** * Convert critical operation enumeration value to string diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -362,6 +362,11 @@ struct TALER_MERCHANTDB_ProductDetails char *image; /** + * Hash of the product image data, or NULL. + */ + char *image_hash; + + /** * List of taxes the merchant pays for this product. Never NULL, * but can be an empty array. */ @@ -446,6 +451,70 @@ struct TALER_MERCHANTDB_ProductDetails }; +/** + * Minimal product details for inventory templates. + */ +struct TALER_MERCHANTDB_InventoryProductDetails +{ + /** + * Name of the product. + */ + char *product_name; + + /** + * Description of the product. + */ + char *description; + + /** + * Internationalized description. + */ + json_t *description_i18n; + + /** + * Unit in which the product is sold. + */ + char *unit; + + /** + * Price per unit of the product. + */ + struct TALER_Amount price; + + /** + * Optional list of per-unit prices. When NULL or empty, @e price + * must be used as the canonical single price. + */ + struct TALER_Amount *price_array; + + /** + * Number of entries in @e price_array. + */ + size_t price_array_length; + + /** + * Hash of the product image data, or NULL. + */ + char *image_hash; + + /** + * Honor fractional stock if TRUE, else only integer stock. + */ + bool allow_fractional_quantity; + + /** + * Precision level (number of decimal places) to apply when + * fractional quantities are enabled. + */ + uint32_t fractional_precision_level; + + /** + * List of taxes the merchant pays for this product. Never NULL, + * but can be an empty array. + */ + json_t *taxes; +}; + /** * Details about an inventory measurement unit. @@ -525,6 +594,24 @@ typedef void size_t num_categories, const uint64_t *categories); +/** + * Typically called by `lookup_inventory_products`. + * + * @param cls a `json_t *` JSON array to build + * @param product_id ID of the product + * @param pd inventory product details + * @param num_categories length of @a categories array + * @param categories array of categories the + * product is in + */ +typedef void +(*TALER_MERCHANTDB_InventoryProductCallback)( + void *cls, + const char *product_id, + const struct TALER_MERCHANTDB_InventoryProductDetails *pd, + size_t num_categories, + const uint64_t *categories); + /** * Typically called by `lookup_login_tokens`. @@ -1800,7 +1887,7 @@ struct TALER_MERCHANTDB_Plugin * @param cls closure */ enum GNUNET_GenericReturnValue - (*connect)(void *cls); + (*connect)(void *cls); /** * Drop merchant tables. Used for testcases and to reset the DB. @@ -1809,7 +1896,7 @@ struct TALER_MERCHANTDB_Plugin * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure */ enum GNUNET_GenericReturnValue - (*drop_tables)(void *cls); + (*drop_tables)(void *cls); /** * Garbage collect database. Removes unnecessary data. @@ -1818,7 +1905,7 @@ struct TALER_MERCHANTDB_Plugin * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure */ enum GNUNET_GenericReturnValue - (*gc)(void *cls); + (*gc)(void *cls); /** * Initialize merchant tables @@ -1827,7 +1914,7 @@ struct TALER_MERCHANTDB_Plugin * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure */ enum GNUNET_GenericReturnValue - (*create_tables)(void *cls); + (*create_tables)(void *cls); /** * Register callback to be invoked on events of type @a es. @@ -1890,8 +1977,8 @@ struct TALER_MERCHANTDB_Plugin * @return #GNUNET_OK on success */ enum GNUNET_GenericReturnValue - (*start)(void *cls, - const char *name); + (*start)(void *cls, + const char *name); /** * Start a transaction with isolation level 'read committed'. @@ -1902,8 +1989,8 @@ struct TALER_MERCHANTDB_Plugin * @return #GNUNET_OK on success */ enum GNUNET_GenericReturnValue - (*start_read_committed)(void *cls, - const char *name); + (*start_read_committed)(void *cls, + const char *name); /** * Roll back the current transaction of a database connection. @@ -1920,7 +2007,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*commit)(void *cls); + (*commit)(void *cls); /** * Lookup all of the instances this backend has configured. @@ -1931,10 +2018,10 @@ struct TALER_MERCHANTDB_Plugin * @param cb_cls closure for @a cb */ enum GNUNET_DB_QueryStatus - (*lookup_instances)(void *cls, - bool active_only, - TALER_MERCHANTDB_InstanceCallback cb, - void *cb_cls); + (*lookup_instances)(void *cls, + bool active_only, + TALER_MERCHANTDB_InstanceCallback cb, + void *cb_cls); /** * Lookup one of the instances this backend has configured. @@ -1946,11 +2033,11 @@ struct TALER_MERCHANTDB_Plugin * @param cb_cls closure for @a cb */ enum GNUNET_DB_QueryStatus - (*lookup_instance)(void *cls, - const char *id, - bool active_only, - TALER_MERCHANTDB_InstanceCallback cb, - void *cb_cls); + (*lookup_instance)(void *cls, + const char *id, + bool active_only, + TALER_MERCHANTDB_InstanceCallback cb, + void *cb_cls); /** * Lookup authentication data of an instance. @@ -1960,9 +2047,9 @@ struct TALER_MERCHANTDB_Plugin * @param[out] ias where to store the auth data */ enum GNUNET_DB_QueryStatus - (*lookup_instance_auth)(void *cls, - const char *instance_id, - struct TALER_MERCHANTDB_InstanceAuthSettings *ias); + (*lookup_instance_auth)(void *cls, + const char *instance_id, + struct TALER_MERCHANTDB_InstanceAuthSettings *ias); /** @@ -1977,12 +2064,12 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_instance)(void *cls, - const struct TALER_MerchantPublicKeyP *merchant_pub, - const struct TALER_MerchantPrivateKeyP *merchant_priv, - const struct TALER_MERCHANTDB_InstanceSettings *is, - const struct TALER_MERCHANTDB_InstanceAuthSettings *ias, - bool validation_needed); + (*insert_instance)(void *cls, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_MerchantPrivateKeyP *merchant_priv, + const struct TALER_MERCHANTDB_InstanceSettings *is, + const struct TALER_MERCHANTDB_InstanceAuthSettings *ias, + bool validation_needed); /** @@ -1993,7 +2080,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_account)( + (*insert_account)( void *cls, const struct TALER_MERCHANTDB_AccountDetails *account_details); @@ -2011,7 +2098,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_login_token)( + (*insert_login_token)( void *cls, const char *id, const struct TALER_MERCHANTDB_LoginTokenP *token, @@ -2032,7 +2119,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_login_token)( + (*select_login_token)( void *cls, const char *id, const struct TALER_MERCHANTDB_LoginTokenP *token, @@ -2052,12 +2139,12 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_login_tokens)(void *cls, - const char *instance_id, - uint64_t offset, - int64_t limit, - TALER_MERCHANTDB_LoginTokensCallback cb, - void *cb_cls); + (*lookup_login_tokens)(void *cls, + const char *instance_id, + uint64_t offset, + int64_t limit, + TALER_MERCHANTDB_LoginTokensCallback cb, + void *cb_cls); /** * Delete login token from database. @@ -2068,7 +2155,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*delete_login_token)( + (*delete_login_token)( void *cls, const char *id, const struct TALER_MERCHANTDB_LoginTokenP *token); @@ -2082,7 +2169,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*delete_login_token_serial)( + (*delete_login_token_serial)( void *cls, const char *id, uint64_t serial); @@ -2099,7 +2186,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_account)( + (*update_account)( void *cls, const char *id, const struct TALER_MerchantWireHashP *h_wire, @@ -2117,7 +2204,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_accounts)( + (*select_accounts)( void *cls, const char *id, TALER_MERCHANTDB_AccountCallback cb, @@ -2134,7 +2221,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_account)( + (*select_account)( void *cls, const char *id, const struct TALER_MerchantWireHashP *h_wire, @@ -2151,7 +2238,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_account_by_uri)( + (*select_account_by_uri)( void *cls, const char *id, struct TALER_FullPayto payto_uri, @@ -2166,7 +2253,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*delete_instance_private_key)( + (*delete_instance_private_key)( void *cls, const char *merchant_id); @@ -2179,8 +2266,8 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*purge_instance)(void *cls, - const char *merchant_id); + (*purge_instance)(void *cls, + const char *merchant_id); /** * Update information about an instance into our database. @@ -2190,8 +2277,8 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_instance)(void *cls, - const struct TALER_MERCHANTDB_InstanceSettings *is); + (*update_instance)(void *cls, + const struct TALER_MERCHANTDB_InstanceSettings *is); /** * Update information about an instance's authentication settings @@ -2203,7 +2290,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_instance_auth)( + (*update_instance_auth)( void *cls, const char *merchant_id, const struct TALER_MERCHANTDB_InstanceAuthSettings *ias); @@ -2217,9 +2304,9 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*inactivate_account)(void *cls, - const char *merchant_id, - const struct TALER_MerchantWireHashP *h_wire); + (*inactivate_account)(void *cls, + const char *merchant_id, + const struct TALER_MerchantWireHashP *h_wire); /** @@ -2231,9 +2318,9 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*activate_account)(void *cls, - const char *merchant_id, - const struct TALER_MerchantWireHashP *h_wire); + (*activate_account)(void *cls, + const char *merchant_id, + const struct TALER_MerchantWireHashP *h_wire); /** @@ -2250,7 +2337,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*account_kyc_get_status)( + (*account_kyc_get_status)( void *cls, const char *merchant_id, const struct TALER_MerchantWireHashP *h_wire, @@ -2279,7 +2366,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*get_kyc_status)( + (*get_kyc_status)( void *cls, struct TALER_FullPayto merchant_account_uri, const char *instance_id, @@ -2308,7 +2395,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*get_kyc_limits)( + (*get_kyc_limits)( void *cls, struct TALER_FullPayto merchant_account_uri, const char *instance_id, @@ -2337,7 +2424,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*account_kyc_set_status)( + (*account_kyc_set_status)( void *cls, const char *merchant_id, const struct TALER_MerchantWireHashP *h_wire, @@ -2365,7 +2452,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*account_kyc_set_failed)( + (*account_kyc_set_failed)( void *cls, const char *merchant_id, const struct TALER_MerchantWireHashP *h_wire, @@ -2409,15 +2496,15 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_products)(void *cls, - const char *instance_id, - uint64_t offset, - int64_t limit, - const char *category_filter, - const char *name_filter, - const char *description_filter, - TALER_MERCHANTDB_ProductsCallback cb, - void *cb_cls); + (*lookup_products)(void *cls, + const char *instance_id, + uint64_t offset, + int64_t limit, + const char *category_filter, + const char *name_filter, + const char *description_filter, + TALER_MERCHANTDB_ProductsCallback cb, + void *cb_cls); /** @@ -2430,10 +2517,49 @@ struct TALER_MERCHANTDB_Plugin * @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_all_products)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_ProductCallback cb, + void *cb_cls); + + /** + * Lookup inventory details for all products of an instance. + * + * @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_inventory_products)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_InventoryProductCallback cb, + void *cb_cls); + + /** + * Lookup inventory details for a subset of products. + * + * @param cls closure + * @param instance_id instance to lookup products for + * @param product_ids product IDs to include (can be NULL/empty) + * @param num_product_ids number of entries in @a product_ids + * @param categories category IDs to include (can be NULL/empty) + * @param num_categories number of entries in @a categories + * @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_inventory_products_filtered)( + void *cls, + const char *instance_id, + const char *const *product_ids, + size_t num_product_ids, + const uint64_t *categories, + size_t num_categories, + TALER_MERCHANTDB_InventoryProductCallback cb, + void *cb_cls); /** * Lookup details about a particular product. @@ -2449,12 +2575,12 @@ struct TALER_MERCHANTDB_Plugin * @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, - size_t *num_categories, - uint64_t **categories); + (*lookup_product)(void *cls, + const char *instance_id, + const char *product_id, + struct TALER_MERCHANTDB_ProductDetails *pd, + size_t *num_categories, + uint64_t **categories); /** * Lookup product image by its hash. @@ -2466,10 +2592,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_product_image_by_hash)(void *cls, - const char *instance_id, - const char *image_hash, - char **image); + (*lookup_product_image_by_hash)(void *cls, + const char *instance_id, + const char *image_hash, + char **image); /** * Delete information about a product. Note that the transaction must @@ -2482,9 +2608,9 @@ struct TALER_MERCHANTDB_Plugin * if locks prevent deletion OR product unknown */ enum GNUNET_DB_QueryStatus - (*delete_product)(void *cls, - const char *instance_id, - const char *product_id); + (*delete_product)(void *cls, + const char *instance_id, + const char *product_id); /** * Insert details about a particular product. @@ -2504,18 +2630,15 @@ struct TALER_MERCHANTDB_Plugin * @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, - size_t num_cats, - const uint64_t *cats, - bool *no_instance, - bool *conflict, - ssize_t *no_cat, - bool *no_group, - bool *no_pot); - + (*insert_product)(void *cls, + const char *instance_id, + const char *product_id, + 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 @@ -2541,20 +2664,18 @@ struct TALER_MERCHANTDB_Plugin * @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, - 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, - bool *no_group, - bool *no_pot); + (*update_product)(void *cls, + const char *instance_id, + const char *product_id, + 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); /** @@ -2573,13 +2694,13 @@ struct TALER_MERCHANTDB_Plugin * product is unknown OR if there insufficient stocks remaining */ enum GNUNET_DB_QueryStatus - (*lock_product)(void *cls, - const char *instance_id, - const char *product_id, - const struct GNUNET_Uuid *uuid, - uint64_t quantity, - uint32_t quantity_frac, - struct GNUNET_TIME_Timestamp expiration_time); + (*lock_product)(void *cls, + const char *instance_id, + const char *product_id, + const struct GNUNET_Uuid *uuid, + uint64_t quantity, + uint32_t quantity_frac, + struct GNUNET_TIME_Timestamp expiration_time); /** @@ -2591,7 +2712,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*expire_locks)(void *cls); + (*expire_locks)(void *cls); /** @@ -2606,10 +2727,10 @@ struct TALER_MERCHANTDB_Plugin * if locks prevent deletion OR order unknown */ enum GNUNET_DB_QueryStatus - (*delete_order)(void *cls, - const char *instance_id, - const char *order_id, - bool force); + (*delete_order)(void *cls, + const char *instance_id, + const char *order_id, + bool force); /** @@ -2626,12 +2747,12 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_order)(void *cls, - const char *instance_id, - const char *order_id, - struct TALER_ClaimTokenP *claim_token, - struct TALER_MerchantPostDataHashP *h_post_data, - json_t **contract_terms); + (*lookup_order)(void *cls, + const char *instance_id, + const char *order_id, + struct TALER_ClaimTokenP *claim_token, + struct TALER_MerchantPostDataHashP *h_post_data, + json_t **contract_terms); /** @@ -2645,11 +2766,11 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_order_summary)(void *cls, - const char *instance_id, - const char *order_id, - struct GNUNET_TIME_Timestamp *timestamp, - uint64_t *order_serial); + (*lookup_order_summary)(void *cls, + const char *instance_id, + const char *order_id, + struct GNUNET_TIME_Timestamp *timestamp, + uint64_t *order_serial); /** @@ -2663,11 +2784,11 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_orders)(void *cls, - const char *instance_id, - const struct TALER_MERCHANTDB_OrderFilter *of, - TALER_MERCHANTDB_OrdersCallback cb, - void *cb_cls); + (*lookup_orders)(void *cls, + const char *instance_id, + const struct TALER_MERCHANTDB_OrderFilter *of, + TALER_MERCHANTDB_OrdersCallback cb, + void *cb_cls); /** @@ -2686,16 +2807,16 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*insert_order)(void *cls, - const char *instance_id, - const char *order_id, - const char *session_id, - const struct TALER_MerchantPostDataHashP *h_post_data, - struct GNUNET_TIME_Timestamp pay_deadline, - const struct TALER_ClaimTokenP *claim_token, - const json_t *contract_terms, - const char *pos_key, - enum TALER_MerchantConfirmationAlgorithm pos_algorithm); + (*insert_order)(void *cls, + const char *instance_id, + const char *order_id, + const char *session_id, + const struct TALER_MerchantPostDataHashP *h_post_data, + struct GNUNET_TIME_Timestamp pay_deadline, + const struct TALER_ClaimTokenP *claim_token, + const json_t *contract_terms, + const char *pos_key, + enum TALER_MerchantConfirmationAlgorithm pos_algorithm); /** @@ -2708,7 +2829,7 @@ struct TALER_MERCHANTDB_Plugin * @param blinded_sigs JSON array of blinded signatures */ enum GNUNET_DB_QueryStatus - (*insert_order_blinded_sigs)( + (*insert_order_blinded_sigs)( void *cls, const char *order_id, uint32_t i, @@ -2727,8 +2848,8 @@ struct TALER_MERCHANTDB_Plugin * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT indicates success */ enum GNUNET_DB_QueryStatus - (*unlock_inventory)(void *cls, - const struct GNUNET_Uuid *uuid); + (*unlock_inventory)(void *cls, + const struct GNUNET_Uuid *uuid); /** @@ -2746,12 +2867,12 @@ struct TALER_MERCHANTDB_Plugin * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT indicates success */ enum GNUNET_DB_QueryStatus - (*insert_order_lock)(void *cls, - const char *instance_id, - const char *order_id, - const char *product_id, - uint64_t quantity, - uint32_t quantity_frac); + (*insert_order_lock)(void *cls, + const char *instance_id, + const char *order_id, + const char *product_id, + uint64_t quantity, + uint32_t quantity_frac); /** @@ -2763,7 +2884,7 @@ struct TALER_MERCHANTDB_Plugin * @param cb_cls closure for @a cb */ enum GNUNET_DB_QueryStatus - (*select_order_blinded_sigs)( + (*select_order_blinded_sigs)( void *cls, const char *order_id, TALER_MERCHANTDB_BlindedSigCallback cb, @@ -2785,7 +2906,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_contract_terms2)( + (*lookup_contract_terms2)( void *cls, const char *instance_id, const char *order_id, @@ -2814,7 +2935,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_contract_terms3)( + (*lookup_contract_terms3)( void *cls, const char *instance_id, const char *order_id, @@ -2840,7 +2961,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_contract_terms)( + (*lookup_contract_terms)( void *cls, const char *instance_id, const char *order_id, @@ -2866,7 +2987,7 @@ struct TALER_MERCHANTDB_Plugin * is malformed */ enum GNUNET_DB_QueryStatus - (*insert_contract_terms)( + (*insert_contract_terms)( void *cls, const char *instance_id, const char *order_id, @@ -2890,10 +3011,10 @@ struct TALER_MERCHANTDB_Plugin * is malformed */ enum GNUNET_DB_QueryStatus - (*update_contract_terms)(void *cls, - const char *instance_id, - const char *order_id, - json_t *contract_terms); + (*update_contract_terms)(void *cls, + const char *instance_id, + const char *order_id, + json_t *contract_terms); /** @@ -2910,10 +3031,10 @@ struct TALER_MERCHANTDB_Plugin * if locks prevent deletion OR order unknown */ enum GNUNET_DB_QueryStatus - (*delete_contract_terms)(void *cls, - const char *instance_id, - const char *order_id, - struct GNUNET_TIME_Relative legal_expiration); + (*delete_contract_terms)(void *cls, + const char *instance_id, + const char *order_id, + struct GNUNET_TIME_Relative legal_expiration); /** @@ -2928,12 +3049,12 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_deposits)(void *cls, - const char *instance_id, - const struct TALER_PrivateContractHashP *h_contract_terms - , - TALER_MERCHANTDB_DepositsCallback cb, - void *cb_cls); + (*lookup_deposits)(void *cls, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms + , + TALER_MERCHANTDB_DepositsCallback cb, + void *cb_cls); /** @@ -2948,7 +3069,7 @@ struct TALER_MERCHANTDB_Plugin * @param master_sig signature of @a master_pub over the @a exchange_pub and the dates */ enum GNUNET_DB_QueryStatus - (*insert_exchange_signkey)( + (*insert_exchange_signkey)( void *cls, const struct TALER_MasterPublicKeyP *master_pub, const struct TALER_ExchangePublicKeyP *exchange_pub, @@ -2976,7 +3097,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*insert_deposit_confirmation)( + (*insert_deposit_confirmation)( void *cls, const char *instance_id, struct GNUNET_TIME_Timestamp deposit_timestamp, @@ -3008,7 +3129,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*insert_deposit)( + (*insert_deposit)( void *cls, uint32_t offset, uint64_t deposit_confirmation_serial_id, @@ -3031,11 +3152,11 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_refunds)(void *cls, - const char *instance_id, - const struct TALER_PrivateContractHashP *h_contract_terms, - TALER_MERCHANTDB_RefundCallback rc, - void *rc_cls); + (*lookup_refunds)(void *cls, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms, + TALER_MERCHANTDB_RefundCallback rc, + void *rc_cls); /** @@ -3048,10 +3169,10 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_spent_tokens_by_order)(void *cls, - uint64_t order_serial, - TALER_MERCHANTDB_UsedTokensCallback cb, - void *cb_cls); + (*lookup_spent_tokens_by_order)(void *cls, + uint64_t order_serial, + TALER_MERCHANTDB_UsedTokensCallback cb, + void *cb_cls); /** @@ -3066,7 +3187,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*mark_contract_paid)( + (*mark_contract_paid)( void *cls, const char *instance_id, const struct TALER_PrivateContractHashP *h_contract_terms, @@ -3087,7 +3208,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*update_contract_session)( + (*update_contract_session)( void *cls, const char *instance_id, const struct TALER_PrivateContractHashP *h_contract_terms, @@ -3112,12 +3233,12 @@ struct TALER_MERCHANTDB_Plugin * regardless of whether it actually increased the refund */ enum GNUNET_DB_QueryStatus - (*refund_coin)(void *cls, - const char *instance_id, - const struct TALER_PrivateContractHashP *h_contract_terms, - struct GNUNET_TIME_Timestamp refund_timestamp, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const char *reason); + (*refund_coin)(void *cls, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms, + struct GNUNET_TIME_Timestamp refund_timestamp, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *reason); /** @@ -3131,11 +3252,11 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_order_status)(void *cls, - const char *instance_id, - const char *order_id, - struct TALER_PrivateContractHashP *h_contract_terms, - bool *paid); + (*lookup_order_status)(void *cls, + const char *instance_id, + const char *order_id, + struct TALER_PrivateContractHashP *h_contract_terms, + bool *paid); /** * Retrieve contract terms given its @a order_serial @@ -3149,13 +3270,13 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_order_status_by_serial)(void *cls, - const char *instance_id, - uint64_t order_serial, - char **order_id, - struct TALER_PrivateContractHashP * - h_contract_terms, - bool *paid); + (*lookup_order_status_by_serial)(void *cls, + const char *instance_id, + uint64_t order_serial, + char **order_id, + struct TALER_PrivateContractHashP * + h_contract_terms, + bool *paid); /** @@ -3168,10 +3289,10 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_deposits_by_order)(void *cls, - uint64_t order_serial, - TALER_MERCHANTDB_DepositedCoinsCallback cb, - void *cb_cls); + (*lookup_deposits_by_order)(void *cls, + uint64_t order_serial, + TALER_MERCHANTDB_DepositedCoinsCallback cb, + void *cb_cls); /** @@ -3185,7 +3306,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_transfer_details_by_order)( + (*lookup_transfer_details_by_order)( void *cls, uint64_t order_serial, TALER_MERCHANTDB_OrderTransferDetailsCallback cb, @@ -3206,7 +3327,7 @@ struct TALER_MERCHANTDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*update_transfer_status)( + (*update_transfer_status)( void *cls, const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, @@ -3231,7 +3352,7 @@ struct TALER_MERCHANTDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*finalize_transfer_status)( + (*finalize_transfer_status)( void *cls, const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, @@ -3254,7 +3375,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*select_open_transfers)( + (*select_open_transfers)( void *cls, uint64_t limit, TALER_MERCHANTDB_OpenTransferCallback cb, @@ -3272,11 +3393,11 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*insert_deposit_to_transfer)(void *cls, - uint64_t deposit_serial, - const struct TALER_MerchantWireHashP *h_wire, - const char *exchange_url, - const struct TALER_EXCHANGE_DepositData *dd); + (*insert_deposit_to_transfer)(void *cls, + uint64_t deposit_serial, + const struct TALER_MerchantWireHashP *h_wire, + const char *exchange_url, + const struct TALER_EXCHANGE_DepositData *dd); /** @@ -3287,8 +3408,8 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*mark_order_wired)(void *cls, - uint64_t order_serial); + (*mark_order_wired)(void *cls, + uint64_t order_serial); /** @@ -3314,7 +3435,7 @@ struct TALER_MERCHANTDB_Plugin * what was already refunded (idempotency!) */ enum TALER_MERCHANTDB_RefundStatus - (*increase_refund)( + (*increase_refund)( void *cls, const char *instance_id, const char *order_id, @@ -3335,7 +3456,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_refunds_detailed)( + (*lookup_refunds_detailed)( void *cls, const char *instance_id, const struct TALER_PrivateContractHashP *h_contract_terms, @@ -3352,7 +3473,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*insert_refund_proof)( + (*insert_refund_proof)( void *cls, uint64_t refund_serial, const struct TALER_ExchangeSignatureP *exchange_sig, @@ -3371,7 +3492,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_spent_token)( + (*insert_spent_token)( void *cls, const struct TALER_PrivateContractHashP * h_contract_terms, @@ -3391,7 +3512,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_issued_token)( + (*insert_issued_token)( void *cls, const struct TALER_PrivateContractHashP *h_contract_terms, const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, @@ -3408,7 +3529,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_refund_proof)( + (*lookup_refund_proof)( void *cls, uint64_t refund_serial, struct TALER_ExchangeSignatureP *exchange_sig, @@ -3429,7 +3550,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_order_by_fulfillment)( + (*lookup_order_by_fulfillment)( void *cls, const char *instance_id, const char *fulfillment_url, @@ -3447,7 +3568,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*update_wirewatch_progress)( + (*update_wirewatch_progress)( void *cls, const char *instance, struct TALER_FullPayto payto_uri, @@ -3463,7 +3584,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*select_wirewatch_accounts)( + (*select_wirewatch_accounts)( void *cls, TALER_MERCHANTDB_WirewatchWorkCallback cb, void *cb_cls); @@ -3482,7 +3603,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*insert_transfer)( + (*insert_transfer)( void *cls, const char *instance_id, const char *exchange_url, @@ -3503,9 +3624,9 @@ struct TALER_MERCHANTDB_Plugin * if deletion is prohibited OR transfer is unknown */ enum GNUNET_DB_QueryStatus - (*delete_transfer)(void *cls, - const char *instance_id, - uint64_t transfer_serial_id); + (*delete_transfer)(void *cls, + const char *instance_id, + uint64_t transfer_serial_id); /** @@ -3519,9 +3640,9 @@ struct TALER_MERCHANTDB_Plugin * if the transfer record exists */ enum GNUNET_DB_QueryStatus - (*check_transfer_exists)(void *cls, - const char *instance_id, - uint64_t transfer_serial_id); + (*check_transfer_exists)(void *cls, + const char *instance_id, + uint64_t transfer_serial_id); /** @@ -3534,10 +3655,10 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_account)(void *cls, - const char *instance_id, - struct TALER_FullPayto payto_uri, - uint64_t *account_serial); + (*lookup_account)(void *cls, + const char *instance_id, + struct TALER_FullPayto payto_uri, + uint64_t *account_serial); /** @@ -3554,7 +3675,7 @@ struct TALER_MERCHANTDB_Plugin * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT on success */ enum GNUNET_DB_QueryStatus - (*insert_transfer_details)( + (*insert_transfer_details)( void *cls, const char *instance_id, const char *exchange_url, @@ -3578,7 +3699,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*lookup_wire_fee)( + (*lookup_wire_fee)( void *cls, const struct TALER_MasterPublicKeyP *master_pub, const char *wire_method, @@ -3602,7 +3723,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_deposits_by_contract_and_coin)( + (*lookup_deposits_by_contract_and_coin)( void *cls, const char *instance_id, const struct TALER_PrivateContractHashP *h_contract_terms, @@ -3622,7 +3743,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_transfer_summary)( + (*lookup_transfer_summary)( void *cls, const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, @@ -3641,7 +3762,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_transfer_details)( + (*lookup_transfer_details)( void *cls, const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, @@ -3666,7 +3787,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_transfers)( + (*lookup_transfers)( void *cls, const char *instance_id, struct TALER_FullPayto payto_uri, @@ -3697,7 +3818,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_expected_transfers)( + (*lookup_expected_transfers)( void *cls, const char *instance_id, struct TALER_FullPayto payto_uri, @@ -3725,7 +3846,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*store_wire_fee_by_exchange)( + (*store_wire_fee_by_exchange)( void *cls, const struct TALER_MasterPublicKeyP *master_pub, const struct GNUNET_HashCode *h_wire_method, @@ -3743,7 +3864,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*delete_exchange_accounts)( + (*delete_exchange_accounts)( void *cls, const struct TALER_MasterPublicKeyP *master_pub); @@ -3758,7 +3879,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*select_accounts_by_exchange)( + (*select_accounts_by_exchange)( void *cls, const struct TALER_MasterPublicKeyP *master_pub, TALER_MERCHANTDB_ExchangeAccountCallback cb, @@ -3778,7 +3899,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*insert_exchange_account)( + (*insert_exchange_account)( void *cls, const struct TALER_MasterPublicKeyP *master_pub, const struct TALER_FullPayto payto_uri, @@ -3798,10 +3919,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_templates)(void *cls, - const char *instance_id, - TALER_MERCHANTDB_TemplatesCallback cb, - void *cb_cls); + (*lookup_templates)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_TemplatesCallback cb, + void *cb_cls); /** @@ -3815,10 +3936,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_template)(void *cls, - const char *instance_id, - const char *template_id, - struct TALER_MERCHANTDB_TemplateDetails *td); + (*lookup_template)(void *cls, + const char *instance_id, + const char *template_id, + struct TALER_MERCHANTDB_TemplateDetails *td); /** * Delete information about a template. @@ -3830,9 +3951,9 @@ struct TALER_MERCHANTDB_Plugin * if template unknown. */ enum GNUNET_DB_QueryStatus - (*delete_template)(void *cls, - const char *instance_id, - const char *template_id); + (*delete_template)(void *cls, + const char *instance_id, + const char *template_id); /** @@ -3846,11 +3967,11 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_template)(void *cls, - const char *instance_id, - const char *template_id, - uint64_t otp_serial_id, - const struct TALER_MERCHANTDB_TemplateDetails *td); + (*insert_template)(void *cls, + const char *instance_id, + const char *template_id, + uint64_t otp_serial_id, + const struct TALER_MERCHANTDB_TemplateDetails *td); /** @@ -3865,10 +3986,10 @@ struct TALER_MERCHANTDB_Plugin * 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); + (*update_template)(void *cls, + const char *instance_id, + const char *template_id, + const struct TALER_MERCHANTDB_TemplateDetails *td); /** @@ -3881,9 +4002,9 @@ struct TALER_MERCHANTDB_Plugin * if template unknown. */ enum GNUNET_DB_QueryStatus - (*delete_otp)(void *cls, - const char *instance_id, - const char *otp_id); + (*delete_otp)(void *cls, + const char *instance_id, + const char *otp_id); /** * Insert details about a particular OTP device. @@ -3895,10 +4016,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_otp)(void *cls, - const char *instance_id, - const char *otp_id, - const struct TALER_MERCHANTDB_OtpDeviceDetails *td); + (*insert_otp)(void *cls, + const char *instance_id, + const char *otp_id, + const struct TALER_MERCHANTDB_OtpDeviceDetails *td); /** @@ -3913,10 +4034,10 @@ struct TALER_MERCHANTDB_Plugin * does not yet exist. */ enum GNUNET_DB_QueryStatus - (*update_otp)(void *cls, - const char *instance_id, - const char *otp_id, - const struct TALER_MERCHANTDB_OtpDeviceDetails *td); + (*update_otp)(void *cls, + const char *instance_id, + const char *otp_id, + const struct TALER_MERCHANTDB_OtpDeviceDetails *td); /** * Lookup all of the OTP devices the given instance has configured. @@ -3928,10 +4049,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_otp_devices)(void *cls, - const char *instance_id, - TALER_MERCHANTDB_OtpDeviceCallback cb, - void *cb_cls); + (*lookup_otp_devices)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_OtpDeviceCallback cb, + void *cb_cls); /** @@ -3945,10 +4066,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_otp)(void *cls, - const char *instance_id, - const char *otp_id, - struct TALER_MERCHANTDB_OtpDeviceDetails *td); + (*select_otp)(void *cls, + const char *instance_id, + const char *otp_id, + struct TALER_MERCHANTDB_OtpDeviceDetails *td); /** @@ -3960,10 +4081,10 @@ struct TALER_MERCHANTDB_Plugin * @param[out] serial set to the OTP device serial number * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_otp_serial)(void *cls, - const char *instance_id, - const char *otp_id, - uint64_t *serial); + (*select_otp_serial)(void *cls, + const char *instance_id, + const char *otp_id, + uint64_t *serial); /** * Delete information about a measurement unit. @@ -3977,12 +4098,12 @@ struct TALER_MERCHANTDB_Plugin * @return DB status code */ enum GNUNET_DB_QueryStatus - (*delete_unit)(void *cls, - const char *instance_id, - const char *unit_id, - bool *no_instance, - bool *no_unit, - bool *builtin_conflict); + (*delete_unit)(void *cls, + const char *instance_id, + const char *unit_id, + bool *no_instance, + bool *no_unit, + bool *builtin_conflict); /** * Insert a measurement unit definition. @@ -3996,12 +4117,12 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_unit)(void *cls, - const char *instance_id, - const struct TALER_MERCHANTDB_UnitDetails *ud, - bool *no_instance, - bool *conflict, - uint64_t *unit_serial); + (*insert_unit)(void *cls, + const char *instance_id, + const struct TALER_MERCHANTDB_UnitDetails *ud, + bool *no_instance, + bool *conflict, + uint64_t *unit_serial); /** * Update a measurement unit definition. @@ -4022,19 +4143,19 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_unit)(void *cls, - const char *instance_id, - const char *unit_id, - const char *unit_name_long, - const json_t *unit_name_long_i18n, - const char *unit_name_short, - const json_t *unit_name_short_i18n, - const bool *unit_allow_fraction, - const uint32_t *unit_precision_level, - const bool *unit_active, - bool *no_instance, - bool *no_unit, - bool *builtin_conflict); + (*update_unit)(void *cls, + const char *instance_id, + const char *unit_id, + const char *unit_name_long, + const json_t *unit_name_long_i18n, + const char *unit_name_short, + const json_t *unit_name_short_i18n, + const bool *unit_allow_fraction, + const uint32_t *unit_precision_level, + const bool *unit_active, + bool *no_instance, + bool *no_unit, + bool *builtin_conflict); /** * Lookup all measurement units of an instance. @@ -4046,12 +4167,30 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_units)(void *cls, - const char *instance_id, - TALER_MERCHANTDB_UnitsCallback cb, - void *cb_cls); + (*lookup_units)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_UnitsCallback cb, + void *cb_cls); /** + * Lookup custom measurement units by name. + * + * @param cls closure + * @param instance_id instance to fetch units for + * @param units array of unit identifiers + * @param num_units length of @a units + * @param cb function to call per unit + * @param cb_cls closure for @a cb + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*lookup_custom_units_by_names)(void *cls, + const char *instance_id, + const char *const *units, + size_t num_units, + TALER_MERCHANTDB_UnitsCallback cb, + void *cb_cls); + /** * Lookup a single measurement unit. * * @param cls closure @@ -4061,10 +4200,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_unit)(void *cls, - const char *instance_id, - const char *unit_id, - struct TALER_MERCHANTDB_UnitDetails *ud); + (*select_unit)(void *cls, + const char *instance_id, + const char *unit_id, + struct TALER_MERCHANTDB_UnitDetails *ud); /** @@ -4077,9 +4216,9 @@ struct TALER_MERCHANTDB_Plugin * if template unknown. */ enum GNUNET_DB_QueryStatus - (*delete_category)(void *cls, - const char *instance_id, - uint64_t category_id); + (*delete_category)(void *cls, + const char *instance_id, + uint64_t category_id); /** * Insert new product category. @@ -4092,11 +4231,11 @@ struct TALER_MERCHANTDB_Plugin * @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); + (*insert_category)(void *cls, + const char *instance_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t *category_id); /** @@ -4111,11 +4250,11 @@ struct TALER_MERCHANTDB_Plugin * does not yet exist. */ enum GNUNET_DB_QueryStatus - (*update_category)(void *cls, - const char *instance_id, - uint64_t category_id, - const char *category_name, - const json_t *category_name_i18n); + (*update_category)(void *cls, + const char *instance_id, + 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. @@ -4127,10 +4266,29 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_categories)(void *cls, - const char *instance_id, - TALER_MERCHANTDB_CategoriesCallback cb, - void *cb_cls); + (*lookup_categories)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_CategoriesCallback cb, + void *cb_cls); + + /** + * Lookup product categories by ID. + * + * @param cls closure + * @param instance_id instance to lookup categories for + * @param category_ids array of category IDs + * @param num_category_ids length of @a category_ids + * @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_by_ids)(void *cls, + const char *instance_id, + const uint64_t *category_ids, + size_t num_category_ids, + TALER_MERCHANTDB_CategoriesCallback cb, + void *cb_cls); /** @@ -4146,12 +4304,12 @@ struct TALER_MERCHANTDB_Plugin * @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, - size_t *num_products, - char **products); + (*select_category)(void *cls, + const char *instance_id, + uint64_t category_id, + struct TALER_MERCHANTDB_CategoryDetails *cd, + size_t *num_products, + char **products); /** @@ -4165,11 +4323,11 @@ struct TALER_MERCHANTDB_Plugin * @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); + (*select_category_by_name)(void *cls, + const char *instance_id, + const char *category_name, + json_t **name_i18n, + uint64_t *category_id); /** @@ -4182,10 +4340,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_webhooks)(void *cls, - const char *instance_id, - TALER_MERCHANTDB_WebhooksCallback cb, - void *cb_cls); + (*lookup_webhooks)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_WebhooksCallback cb, + void *cb_cls); /** @@ -4199,10 +4357,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_webhook)(void *cls, - const char *instance_id, - const char *webhook_id, - struct TALER_MERCHANTDB_WebhookDetails *wb); + (*lookup_webhook)(void *cls, + const char *instance_id, + const char *webhook_id, + struct TALER_MERCHANTDB_WebhookDetails *wb); /** * Delete information about a webhook. @@ -4214,9 +4372,9 @@ struct TALER_MERCHANTDB_Plugin * if webhook unknown. */ enum GNUNET_DB_QueryStatus - (*delete_webhook)(void *cls, - const char *instance_id, - const char *webhook_id); + (*delete_webhook)(void *cls, + const char *instance_id, + const char *webhook_id); /** @@ -4229,10 +4387,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_webhook)(void *cls, - const char *instance_id, - const char *webhook_id, - const struct TALER_MERCHANTDB_WebhookDetails *wb); + (*insert_webhook)(void *cls, + const char *instance_id, + const char *webhook_id, + const struct TALER_MERCHANTDB_WebhookDetails *wb); /** @@ -4247,10 +4405,10 @@ struct TALER_MERCHANTDB_Plugin * does not yet exist. */ enum GNUNET_DB_QueryStatus - (*update_webhook)(void *cls, - const char *instance_id, - const char *webhook_id, - const struct TALER_MERCHANTDB_WebhookDetails *wb); + (*update_webhook)(void *cls, + const char *instance_id, + const char *webhook_id, + const struct TALER_MERCHANTDB_WebhookDetails *wb); /** * Lookup webhook by event @@ -4263,11 +4421,11 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_webhook_by_event)(void *cls, - const char *instance_id, - const char *event_type, - TALER_MERCHANTDB_WebhookDetailCallback cb, - void *cb_cls); + (*lookup_webhook_by_event)(void *cls, + const char *instance_id, + const char *event_type, + TALER_MERCHANTDB_WebhookDetailCallback cb, + void *cb_cls); /** * Insert webhook in the pending webhook. @@ -4282,13 +4440,13 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_pending_webhook)(void *cls, - const char *instance_id, - uint64_t webhook_serial, - const char *url, - const char *http_method, - const char *header, - const char *body); + (*insert_pending_webhook)(void *cls, + const char *instance_id, + uint64_t webhook_serial, + const char *url, + const char *http_method, + const char *header, + const char *body); /** * Lookup the webhook that need to be send in priority. These webhooks are not successfully * send. @@ -4299,9 +4457,9 @@ struct TALER_MERCHANTDB_Plugin */ // WHERE next_attempt <= now ORDER BY next_attempt ASC enum GNUNET_DB_QueryStatus - (*lookup_pending_webhooks)(void *cls, - TALER_MERCHANTDB_PendingWebhooksCallback cb, - void *cb_cls); + (*lookup_pending_webhooks)(void *cls, + TALER_MERCHANTDB_PendingWebhooksCallback cb, + void *cb_cls); /** * Lookup future webhook in the pending webhook that need to be send. @@ -4313,9 +4471,9 @@ struct TALER_MERCHANTDB_Plugin */ // ORDER BY next_attempt ASC LIMIT 1 enum GNUNET_DB_QueryStatus - (*lookup_future_webhook)(void *cls, - TALER_MERCHANTDB_PendingWebhooksCallback cb, - void *cb_cls); + (*lookup_future_webhook)(void *cls, + TALER_MERCHANTDB_PendingWebhooksCallback cb, + void *cb_cls); /** * Lookup all the webhooks in the pending webhook. @@ -4330,12 +4488,12 @@ struct TALER_MERCHANTDB_Plugin */ // WHERE webhook_pending_serial > min_row ORDER BY webhook_pending_serial ASC LIMIT max_results enum GNUNET_DB_QueryStatus - (*lookup_all_webhooks)(void *cls, - const char *instance_id, - uint64_t min_row, - uint32_t max_results, - TALER_MERCHANTDB_PendingWebhooksCallback cb, - void *cb_cls); + (*lookup_all_webhooks)(void *cls, + const char *instance_id, + uint64_t min_row, + uint32_t max_results, + TALER_MERCHANTDB_PendingWebhooksCallback cb, + void *cb_cls); /** @@ -4347,9 +4505,9 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_pending_webhook)(void *cls, - uint64_t webhook_pending_serial, - struct GNUNET_TIME_Absolute next_attempt); + (*update_pending_webhook)(void *cls, + uint64_t webhook_pending_serial, + struct GNUNET_TIME_Absolute next_attempt); // maybe add: http status of failure? @@ -4362,8 +4520,8 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*delete_pending_webhook)(void *cls, - uint64_t webhook_pending_serial); + (*delete_pending_webhook)(void *cls, + uint64_t webhook_pending_serial); /** @@ -4376,10 +4534,10 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*select_exchange_keys)(void *cls, - const char *exchange_url, - struct GNUNET_TIME_Absolute *first_retry, - struct TALER_EXCHANGE_Keys **keys); + (*select_exchange_keys)(void *cls, + const char *exchange_url, + struct GNUNET_TIME_Absolute *first_retry, + struct TALER_EXCHANGE_Keys **keys); /** @@ -4391,9 +4549,9 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*insert_exchange_keys)(void *cls, - const struct TALER_EXCHANGE_Keys *keys, - struct GNUNET_TIME_Absolute first_retry); + (*insert_exchange_keys)(void *cls, + const struct TALER_EXCHANGE_Keys *keys, + struct GNUNET_TIME_Absolute first_retry); /** @@ -4406,10 +4564,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_token_families)(void *cls, - const char *instance_id, - TALER_MERCHANTDB_TokenFamiliesCallback cb, - void *cb_cls); + (*lookup_token_families)(void *cls, + const char *instance_id, + TALER_MERCHANTDB_TokenFamiliesCallback cb, + void *cb_cls); /** * Lookup details about a particular token family. @@ -4422,10 +4580,10 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_token_family)(void *cls, - const char *instance_id, - const char *token_family_slug, - struct TALER_MERCHANTDB_TokenFamilyDetails *details); + (*lookup_token_family)(void *cls, + const char *instance_id, + const char *token_family_slug, + struct TALER_MERCHANTDB_TokenFamilyDetails *details); /** * Delete information about a token family. @@ -4436,9 +4594,9 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*delete_token_family)(void *cls, - const char *instance_id, - const char *token_family_slug); + (*delete_token_family)(void *cls, + const char *instance_id, + const char *token_family_slug); /** * Update details about a particular token family. @@ -4451,7 +4609,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_token_family)( + (*update_token_family)( void *cls, const char *instance_id, const char *token_family_slug, @@ -4468,7 +4626,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_token_family)( + (*insert_token_family)( void *cls, const char *instance_id, const char *token_family_slug, @@ -4488,7 +4646,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_token_family_key)( + (*lookup_token_family_key)( void *cls, const char *instance_id, const char *token_family_slug, @@ -4510,7 +4668,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_token_family_keys)( + (*lookup_token_family_keys)( void *cls, const char *instance_id, const char *token_family_slug, @@ -4535,7 +4693,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_token_family_key)( + (*insert_token_family_key)( void *cls, const char *merchant_id, const char *token_family_slug, @@ -4557,7 +4715,7 @@ struct TALER_MERCHANTDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_pending_deposits)( + (*lookup_pending_deposits)( void *cls, const char *exchange_url, uint64_t limit, @@ -4580,7 +4738,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_deposit_confirmation_status)( + (*update_deposit_confirmation_status)( void *cls, uint64_t deposit_serial, bool retry_needed, @@ -4599,7 +4757,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_donau_instance_receipts_amount)( + (*update_donau_instance_receipts_amount)( void *cls, uint64_t *donau_instances_serial, const struct TALER_Amount *new_amount @@ -4626,7 +4784,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_mfa_challenge)( + (*lookup_mfa_challenge)( void *cls, uint64_t challenge_id, const struct TALER_MERCHANT_MFA_BodyHash *h_body, @@ -4656,7 +4814,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*solve_mfa_challenge)( + (*solve_mfa_challenge)( void *cls, uint64_t challenge_id, const struct TALER_MERCHANT_MFA_BodyHash *h_body, @@ -4680,7 +4838,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_mfa_challenge)( + (*update_mfa_challenge)( void *cls, uint64_t challenge_id, const char *code, @@ -4707,7 +4865,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*create_mfa_challenge)( + (*create_mfa_challenge)( void *cls, enum TALER_MERCHANT_MFA_CriticalOperation op, const struct TALER_MERCHANT_MFA_BodyHash *h_body, @@ -4730,7 +4888,7 @@ struct TALER_MERCHANTDB_Plugin * @param charity_id charity ID of the Donau instance */ enum GNUNET_DB_QueryStatus - (*insert_donau_instance)( + (*insert_donau_instance)( void *cls, const char *donau_url, const struct DONAU_Charity *charity, @@ -4747,7 +4905,7 @@ struct TALER_MERCHANTDB_Plugin * @param charity_id charity ID of the Donau instance */ enum GNUNET_DB_QueryStatus - (*check_donau_instance)( + (*check_donau_instance)( void *cls, const struct TALER_MerchantPublicKeyP *merchant_pub, const char *donau_url, @@ -4763,7 +4921,7 @@ struct TALER_MERCHANTDB_Plugin * @param[out] charity_id set to the charity ID of the Donau instance */ enum GNUNET_DB_QueryStatus - (*select_donau_instance_by_serial)( + (*select_donau_instance_by_serial)( void *cls, uint64_t serial, char **donau_url, @@ -4780,7 +4938,7 @@ struct TALER_MERCHANTDB_Plugin * @param cb_cls closure for @a cb */ enum GNUNET_DB_QueryStatus - (*select_donau_instances)( + (*select_donau_instances)( void *cls, const char *id, TALER_MERCHANTDB_DonauInstanceCallback cb, @@ -4796,7 +4954,7 @@ struct TALER_MERCHANTDB_Plugin * @param cb_cls closure for @a cb */ enum GNUNET_DB_QueryStatus - (*select_all_donau_instances)( + (*select_all_donau_instances)( void *cls, TALER_MERCHANTDB_DonauInstanceCallback cb, void *cb_cls); @@ -4811,7 +4969,7 @@ struct TALER_MERCHANTDB_Plugin * @param cb_cls closure for @a cb */ enum GNUNET_DB_QueryStatus - (*select_donau_instances_filtered)( + (*select_donau_instances_filtered)( void *cls, const char *currency, TALER_MERCHANTDB_DonauInstanceFilteredCallback cb, @@ -4825,7 +4983,7 @@ struct TALER_MERCHANTDB_Plugin * @param[out] keys set to the Donau keys on success */ enum GNUNET_DB_QueryStatus - (*lookup_donau_keys)( + (*lookup_donau_keys)( void *cls, const char *donau_url, struct GNUNET_TIME_Absolute *first_retry, @@ -4847,7 +5005,7 @@ struct TALER_MERCHANTDB_Plugin * Donau keys */ enum GNUNET_DB_QueryStatus - (*lookup_order_charity)( + (*lookup_order_charity)( void *cls, const char *instance_id, const char *donau_url, @@ -4865,7 +5023,7 @@ struct TALER_MERCHANTDB_Plugin * @param keys Donau keys to insert or update */ enum GNUNET_DB_QueryStatus - (*upsert_donau_keys)( + (*upsert_donau_keys)( void *cls, const struct DONAU_Keys *keys, struct GNUNET_TIME_Absolute first_retry); @@ -4879,7 +5037,7 @@ struct TALER_MERCHANTDB_Plugin * @param charity_id charity ID of the Donau instance */ enum GNUNET_DB_QueryStatus - (*update_donau_instance)( + (*update_donau_instance)( void *cls, const char *donau_url, const struct DONAU_Charity *charity, @@ -4894,7 +5052,7 @@ struct TALER_MERCHANTDB_Plugin * @param charity_id charity ID of the Donau instance to delete */ enum GNUNET_DB_QueryStatus - (*delete_donau_instance)( + (*delete_donau_instance)( void *cls, const char *id, uint64_t charity_id); @@ -4910,7 +5068,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_statistics_amount_by_bucket)( + (*lookup_statistics_amount_by_bucket)( void *cls, const char *instance_id, const char *slug, @@ -4931,7 +5089,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_statistics_amount_by_bucket2)( + (*lookup_statistics_amount_by_bucket2)( void *cls, const char *instance_id, const char *slug, @@ -4952,7 +5110,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_statistics_counter_by_bucket)( + (*lookup_statistics_counter_by_bucket)( void *cls, const char *instance_id, const char *slug, @@ -4972,7 +5130,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_statistics_counter_by_bucket2)( + (*lookup_statistics_counter_by_bucket2)( void *cls, const char *instance_id, const char *prefix, @@ -4993,7 +5151,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_statistics_amount_by_interval)( + (*lookup_statistics_amount_by_interval)( void *cls, const char *instance_id, const char *slug, @@ -5011,7 +5169,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_statistics_counter_by_interval)( + (*lookup_statistics_counter_by_interval)( void *cls, const char *instance_id, const char *slug, @@ -5032,12 +5190,12 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_money_pots)(void *cls, - const char *instance_id, - int64_t limit, - uint64_t offset, - TALER_MERCHANTDB_MoneyPotsCallback cb, - void *cb_cls); + (*select_money_pots)(void *cls, + const char *instance_id, + int64_t limit, + uint64_t offset, + TALER_MERCHANTDB_MoneyPotsCallback cb, + void *cb_cls); /** @@ -5056,11 +5214,11 @@ struct TALER_MERCHANTDB_Plugin * @a pot_missing was initialized to a missing pot */ enum GNUNET_DB_QueryStatus - (*check_money_pots)(void *cls, - const char *instance_id, - unsigned int pots_len, - uint64_t pots[static pots_len], - uint64_t *pot_missing); + (*check_money_pots)(void *cls, + const char *instance_id, + unsigned int pots_len, + uint64_t pots[static pots_len], + uint64_t *pot_missing); /** * Lookup details about a particular money pot. @@ -5078,13 +5236,13 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_money_pot)(void *cls, - const char *instance_id, - uint64_t money_pot_id, - char **name, - char **description, - size_t *pot_total_len, - struct TALER_Amount **pot_totals); + (*select_money_pot)(void *cls, + const char *instance_id, + uint64_t money_pot_id, + char **name, + char **description, + size_t *pot_total_len, + struct TALER_Amount **pot_totals); /** * Delete information about a money pot. @@ -5095,9 +5253,9 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*delete_money_pot)(void *cls, - const char *instance_id, - uint64_t money_pot_id); + (*delete_money_pot)(void *cls, + const char *instance_id, + uint64_t money_pot_id); /** * Update details about a particular money pot. @@ -5118,7 +5276,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_money_pot)( + (*update_money_pot)( void *cls, const char *instance_id, uint64_t money_pot_id, @@ -5145,7 +5303,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*increment_money_pots)( + (*increment_money_pots)( void *cls, const char *instance_id, size_t money_pots_len, @@ -5165,7 +5323,7 @@ struct TALER_MERCHANTDB_Plugin * on conflict (@a name already in use at @a instance_id). */ enum GNUNET_DB_QueryStatus - (*insert_money_pot)( + (*insert_money_pot)( void *cls, const char *instance_id, const char *name, @@ -5187,12 +5345,12 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_reports)(void *cls, - const char *instance_id, - int64_t limit, - uint64_t offset, - TALER_MERCHANTDB_ReportsCallback cb, - void *cb_cls); + (*select_reports)(void *cls, + const char *instance_id, + int64_t limit, + uint64_t offset, + TALER_MERCHANTDB_ReportsCallback cb, + void *cb_cls); /** @@ -5204,9 +5362,9 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*lookup_reports_pending)(void *cls, - TALER_MERCHANTDB_ReportsPendingCallback cb, - void *cb_cls); + (*lookup_reports_pending)(void *cls, + TALER_MERCHANTDB_ReportsPendingCallback cb, + void *cb_cls); /** * Lookup details about a particular report. @@ -5230,19 +5388,19 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_report)(void *cls, - const char *instance_id, - uint64_t report_id, - char **report_program_section, - char **report_description, - char **mime_type, - char **data_source, - char **target_address, - struct GNUNET_TIME_Relative *frequency, - struct GNUNET_TIME_Relative *frequency_shift, - struct GNUNET_TIME_Absolute *next_transmission, - enum TALER_ErrorCode *last_error_code, - char **last_error_detail); + (*select_report)(void *cls, + const char *instance_id, + uint64_t report_id, + char **report_program_section, + char **report_description, + char **mime_type, + char **data_source, + char **target_address, + struct GNUNET_TIME_Relative *frequency, + struct GNUNET_TIME_Relative *frequency_shift, + struct GNUNET_TIME_Absolute *next_transmission, + enum TALER_ErrorCode *last_error_code, + char **last_error_detail); /** * Check that a particular report is scheduled under the @@ -5259,12 +5417,12 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*check_report)(void *cls, - uint64_t report_id, - const struct TALER_MERCHANT_ReportToken *report_token, - const char *mime_type, - char **instance_id, - char **data_source); + (*check_report)(void *cls, + uint64_t report_id, + const struct TALER_MERCHANT_ReportToken *report_token, + const char *mime_type, + char **instance_id, + char **data_source); /** @@ -5276,9 +5434,9 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*delete_report)(void *cls, - const char *instance_id, - uint64_t report_id); + (*delete_report)(void *cls, + const char *instance_id, + uint64_t report_id); /** * Update transmission status about a particular report. @@ -5292,7 +5450,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_report_status)( + (*update_report_status)( void *cls, const char *instance_id, uint64_t report_id, @@ -5320,7 +5478,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_report)( + (*update_report)( void *cls, const char *instance_id, uint64_t report_id, @@ -5352,7 +5510,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*insert_report)( + (*insert_report)( void *cls, const char *instance_id, const char *report_program_section, @@ -5380,12 +5538,12 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*select_product_groups)(void *cls, - const char *instance_id, - int64_t limit, - uint64_t offset, - TALER_MERCHANTDB_ProductGroupsCallback cb, - void *cb_cls); + (*select_product_groups)(void *cls, + const char *instance_id, + int64_t limit, + uint64_t offset, + TALER_MERCHANTDB_ProductGroupsCallback cb, + void *cb_cls); /** * Delete information about a product group. @@ -5396,9 +5554,9 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*delete_product_group)(void *cls, - const char *instance_id, - uint64_t product_group_id); + (*delete_product_group)(void *cls, + const char *instance_id, + uint64_t product_group_id); /** * Update details about a particular product group. @@ -5412,7 +5570,7 @@ struct TALER_MERCHANTDB_Plugin * @return database result code */ enum GNUNET_DB_QueryStatus - (*update_product_group)( + (*update_product_group)( void *cls, const char *instance_id, uint64_t product_group_id, @@ -5433,14 +5591,13 @@ struct TALER_MERCHANTDB_Plugin * on conflict (@a name already in use at @a instance_id). */ enum GNUNET_DB_QueryStatus - (*insert_product_group)( + (*insert_product_group)( void *cls, const char *instance_id, const char *name, const char *description, uint64_t *product_group_id); - }; #endif diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -67,6 +67,7 @@ libtalermerchant_la_SOURCES = \ merchant_api_post_order_pay.c \ merchant_api_post_order_refund.c \ merchant_api_post_otp_devices.c \ + merchant_api_post_categories.c \ merchant_api_post_products.c \ merchant_api_post_units.c \ merchant_api_post_transfers.c \ diff --git a/src/lib/merchant_api_post_categories.c b/src/lib/merchant_api_post_categories.c @@ -0,0 +1,223 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 Foundation; either version 2.1, 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with TALER; see the file COPYING.LGPL. + If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant_api_post_categories.c + * @brief Implementation of POST /private/categories + * @author Bohdan Potuzhnyi + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> +#include <gnunet/gnunet_util_lib.h> +#include "taler_merchant_service.h" +#include "merchant_api_curl_defaults.h" +#include "merchant_api_common.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_curl_lib.h> + +/** + * Handle for a POST /private/categories operation. + */ +struct TALER_MERCHANT_CategoriesPostHandle +{ + /** + * Fully qualified request URL. + */ + char *url; + + /** + * CURL job handle. + */ + struct GNUNET_CURL_Job *job; + + /** + * Callback to invoke with the result. + */ + TALER_MERCHANT_CategoriesPostCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Helper keeping POST body and headers alive. + */ + struct TALER_CURL_PostContext post_ctx; +}; + +/** + * Called when the HTTP transfer finishes. + * + * @param cls operation handle + * @param response_code HTTP status (0 on network / parsing failures) + * @param response parsed JSON reply (NULL if unavailable) + */ +static void +handle_post_categories_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_MERCHANT_CategoriesPostHandle *cph = cls; + const json_t *json = response; + struct TALER_MERCHANT_CategoriesPostResponse cpr = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + cph->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "POST /private/categories completed with status %u\n", + (unsigned int) response_code); + switch (response_code) + { + case MHD_HTTP_OK: + { + const char *err_name; + unsigned int err_line; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ("category_id", + &cpr.category_id), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + &err_name, + &err_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid response for field %s\n", + err_name); + cpr.hr.http_status = 0; + cpr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + } + break; + } + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_UNAUTHORIZED: + case MHD_HTTP_FORBIDDEN: + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_CONFLICT: + case MHD_HTTP_INTERNAL_SERVER_ERROR: + cpr.hr.ec = TALER_JSON_get_error_code (json); + cpr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case 0: + cpr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + default: + TALER_MERCHANT_parse_error_details_ (json, + response_code, + &cpr.hr); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response %u/%d for POST /private/categories\n", + (unsigned int) response_code, + (int) cpr.hr.ec); + GNUNET_break_op (0); + break; + } + cph->cb (cph->cb_cls, + &cpr); + TALER_MERCHANT_categories_post_cancel (cph); +} + + +struct TALER_MERCHANT_CategoriesPostHandle * +TALER_MERCHANT_categories_post ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *name, + const json_t *name_i18n, + TALER_MERCHANT_CategoriesPostCallback cb, + void *cb_cls) +{ + struct TALER_MERCHANT_CategoriesPostHandle *cph; + json_t *req_obj; + + req_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("name", + name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("name_i18n", + (json_t *) name_i18n))); + cph = GNUNET_new (struct TALER_MERCHANT_CategoriesPostHandle); + cph->ctx = ctx; + cph->cb = cb; + cph->cb_cls = cb_cls; + cph->url = TALER_url_join (backend_url, + "private/categories", + NULL); + if (NULL == cph->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to build /private/categories URL\n"); + json_decref (req_obj); + GNUNET_free (cph); + return NULL; + } + { + CURL *eh; + + eh = TALER_MERCHANT_curl_easy_get_ (cph->url); + if (GNUNET_OK != + TALER_curl_easy_post (&cph->post_ctx, + eh, + req_obj)) + { + GNUNET_break (0); + curl_easy_cleanup (eh); + json_decref (req_obj); + GNUNET_free (cph->url); + GNUNET_free (cph); + return NULL; + } + json_decref (req_obj); + cph->job = GNUNET_CURL_job_add2 (ctx, + eh, + cph->post_ctx.headers, + &handle_post_categories_finished, + cph); + } + return cph; +} + + +void +TALER_MERCHANT_categories_post_cancel ( + struct TALER_MERCHANT_CategoriesPostHandle *cph) +{ + if (NULL != cph->job) + { + GNUNET_CURL_job_cancel (cph->job); + cph->job = NULL; + } + TALER_curl_easy_post_finished (&cph->post_ctx); + GNUNET_free (cph->url); + GNUNET_free (cph); +} + + +/* end of merchant_api_post_categories.c */ diff --git a/src/lib/merchant_api_post_templates.c b/src/lib/merchant_api_post_templates.c @@ -32,6 +32,178 @@ #include "merchant_api_common.h" #include <taler/taler_json_lib.h> #include <taler/taler_curl_lib.h> +#include <taler/taler_merchant_util.h> + + +/** + * Validate a fixed-order template contract. + * + * @param template_contract JSON template contract + * @param summary summary pointer + * @param amount amount storage + * @param minimum_age minimum age storage + * @param pay_duration pay duration storage + * @return true if valid + */ +static bool +validate_template_contract_fixed (const json_t *template_contract, + const char **summary, + struct TALER_Amount *amount, + uint32_t *minimum_age, + struct GNUNET_TIME_Relative *pay_duration) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("summary", + summary), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("amount", + amount), + NULL), + GNUNET_JSON_spec_uint32 ("minimum_age", + minimum_age), + GNUNET_JSON_spec_relative_time ("pay_duration", + pay_duration), + GNUNET_JSON_spec_end () + }; + const char *ename; + unsigned int eline; + + if (GNUNET_OK != + GNUNET_JSON_parse (template_contract, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid template_contract for field %s\n", + ename); + return false; + } + return true; +} + + +/** + * Validate an inventory-cart template contract. + * + * @param template_contract JSON template contract + * @param summary summary pointer + * @param pay_duration pay duration storage + * @return true if valid + */ +static bool +validate_template_contract_inventory (const json_t *template_contract, + const char **summary, + struct GNUNET_TIME_Relative *pay_duration) +{ + bool selected_all = false; + bool choose_one = false; + bool request_tip = false; + const json_t *selected_categories = NULL; + const json_t *selected_products = NULL; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("summary", + summary), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("request_tip", + &request_tip), + NULL), + GNUNET_JSON_spec_relative_time ("pay_duration", + pay_duration), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("selected_all", + &selected_all), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_categories", + &selected_categories), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_products", + &selected_products), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("choose_one", + &choose_one), + NULL), + GNUNET_JSON_spec_end () + }; + const char *ename; + unsigned int eline; + + (void) request_tip; + (void) selected_all; + (void) choose_one; + (void) selected_categories; + (void) selected_products; + if (GNUNET_OK != + GNUNET_JSON_parse (template_contract, + ispec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid template_contract for field %s\n", + ename); + return false; + } + return true; +} + + +/** + * Validate a paivana template contract. + * + * @param template_contract JSON template contract + * @param summary summary pointer + * @param amount amount storage + * @param minimum_age minimum age storage + * @param pay_duration pay duration storage + * @return true if valid + */ +static bool +validate_template_contract_paivana (const json_t *template_contract, + const char **summary, + struct TALER_Amount *amount, + uint32_t *minimum_age, + struct GNUNET_TIME_Relative *pay_duration) +{ + /* TODO: PAIVANA validate paivana-specific fields beyond presence. */ + const char *paivana_id; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_string ("paivana_id", + &paivana_id), + GNUNET_JSON_spec_end () + }; + const char *ename; + unsigned int eline; + + if (GNUNET_OK != + GNUNET_JSON_parse (template_contract, + pspec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid paivana template_contract for field %s\n", + ename); + return false; + } + + if (! validate_template_contract_fixed (template_contract, + summary, + amount, + minimum_age, + pay_duration)) + return false; + + (void) paivana_id; + return true; +} /** @@ -160,31 +332,25 @@ handle_post_templates_finished (void *cls, static bool test_template_contract_valid (const json_t *template_contract) { - const char *summary; - struct TALER_Amount amount = { .value = 0}; - uint32_t minimum_age = 0; - struct GNUNET_TIME_Relative pay_duration = { 0 }; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("summary", - &summary), - NULL), + const char *template_type = NULL; + enum TALER_MERCHANT_TemplateType template_type_enum; + struct GNUNET_JSON_Specification type_spec[] = { GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("amount", - &amount), + GNUNET_JSON_spec_string ("template_type", + &template_type), NULL), - GNUNET_JSON_spec_uint32 ("minimum_age", - &minimum_age), - GNUNET_JSON_spec_relative_time ("pay_duration", - &pay_duration), GNUNET_JSON_spec_end () }; + const char *summary; + struct TALER_Amount amount = { .value = 0}; + uint32_t minimum_age = 0; + struct GNUNET_TIME_Relative pay_duration = { 0 }; const char *ename; unsigned int eline; if (GNUNET_OK != GNUNET_JSON_parse (template_contract, - spec, + type_spec, &ename, &eline)) { @@ -193,7 +359,39 @@ test_template_contract_valid (const json_t *template_contract) ename); return false; } - return true; + + template_type_enum = + TALER_MERCHANT_template_type_from_string (template_type); + if (TALER_MERCHANT_TEMPLATE_TYPE_INVALID == template_type_enum) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid template_type '%s'\n", + template_type); + return false; + } + + switch (template_type_enum) + { + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + return validate_template_contract_inventory (template_contract, + &summary, + &pay_duration); + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + return validate_template_contract_fixed (template_contract, + &summary, + &amount, + &minimum_age, + &pay_duration); + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + return validate_template_contract_paivana (template_contract, + &summary, + &amount, + &minimum_age, + &pay_duration); + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + break; + } + return false; } diff --git a/src/lib/merchant_api_post_using_templates.c b/src/lib/merchant_api_post_using_templates.c @@ -96,26 +96,28 @@ handle_post_using_templates_finished (void *cls, } -struct TALER_MERCHANT_UsingTemplatesPostHandle * -TALER_MERCHANT_using_templates_post ( +/** + * Helper to submit a POST /templates/$ID request. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend + * @param template_id identifier of the template to use + * @param req_obj JSON request body (consumed) + * @param cb function to call with the backend's result + * @param cb_cls closure for @a cb + * @return the request handle; NULL upon error + */ +static struct TALER_MERCHANT_UsingTemplatesPostHandle * +using_templates_post_internal ( struct GNUNET_CURL_Context *ctx, const char *backend_url, const char *template_id, - const char *summary, - const struct TALER_Amount *amount, + json_t *req_obj, TALER_MERCHANT_PostOrdersCallback cb, void *cb_cls) { struct TALER_MERCHANT_UsingTemplatesPostHandle *utph; - json_t *req_obj; - req_obj = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("summary", - summary)), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("amount", - amount))); utph = GNUNET_new (struct TALER_MERCHANT_UsingTemplatesPostHandle); utph->ctx = ctx; utph->cb = cb; @@ -159,6 +161,58 @@ TALER_MERCHANT_using_templates_post ( } +struct TALER_MERCHANT_UsingTemplatesPostHandle * +TALER_MERCHANT_using_templates_post ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *template_id, + const char *summary, + const struct TALER_Amount *amount, + TALER_MERCHANT_PostOrdersCallback cb, + void *cb_cls) +{ + json_t *req_obj; + + req_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("summary", + summary)), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("amount", + amount))); + return using_templates_post_internal (ctx, + backend_url, + template_id, + req_obj, + cb, + cb_cls); +} + + +struct TALER_MERCHANT_UsingTemplatesPostHandle * +TALER_MERCHANT_using_templates_post2 ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *template_id, + const json_t *details, + TALER_MERCHANT_PostOrdersCallback cb, + void *cb_cls) +{ + if (NULL == details) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No request body provided for using templates.\n"); + return NULL; + } + return using_templates_post_internal (ctx, + backend_url, + template_id, + (json_t *) details, + cb, + cb_cls); +} + + void TALER_MERCHANT_using_templates_post_cancel ( struct TALER_MERCHANT_UsingTemplatesPostHandle *utph) diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am @@ -78,6 +78,7 @@ libtalermerchanttesting_la_SOURCES = \ testing_api_cmd_post_orders_paid.c \ testing_api_cmd_post_orders.c \ testing_api_cmd_post_otp_devices.c \ + testing_api_cmd_post_categories.c \ testing_api_cmd_post_products.c \ testing_api_cmd_post_transfers.c \ testing_api_cmd_post_templates.c \ @@ -88,6 +89,7 @@ libtalermerchanttesting_la_SOURCES = \ testing_api_cmd_refund_order.c \ testing_api_cmd_tme.c \ testing_api_cmd_wallet_get_order.c \ + testing_api_cmd_wallet_get_template.c \ testing_api_cmd_wallet_post_orders_refund.c \ testing_api_cmd_webhook.c \ testing_api_cmd_testserver.c \ diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -160,6 +160,101 @@ static const struct TALER_TESTING_ProductUnitExpectations }; /** + * Build an array of selected product IDs. + * + * @param first first product id + * @param second second product id or NULL + * @return JSON array of product ids + */ +static json_t * +make_selected_products (const char *first, + const char *second) +{ + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + if (NULL != first) + GNUNET_assert (0 == + json_array_append_new (arr, + json_string (first))); + if (NULL != second) + GNUNET_assert (0 == + json_array_append_new (arr, + json_string (second))); + return arr; +} + + +/** + * Build an array of selected category IDs. + * + * @param first first category id + * @param second second category id or 0 + * @return JSON array of category ids + */ +static json_t * +make_selected_categories (uint64_t first, + uint64_t second) +{ + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + if (0 != first) + GNUNET_assert (0 == + json_array_append_new (arr, + json_integer (first))); + if (0 != second) + GNUNET_assert (0 == + json_array_append_new (arr, + json_integer (second))); + return arr; +} + + +/** + * Build an inventory selection array. + * + * @param first_id first product id + * @param first_qty first quantity + * @param second_id second product id or NULL + * @param second_qty second quantity or NULL + * @return JSON array of inventory selections + */ +static json_t * +make_inventory_selection (const char *first_id, + const char *first_qty, + const char *second_id, + const char *second_qty) +{ + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + if (NULL != first_id) + GNUNET_assert (0 == + json_array_append_new ( + arr, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_id", + first_id), + GNUNET_JSON_pack_string ("quantity", + first_qty)))); + if (NULL != second_id) + GNUNET_assert (0 == + json_array_append_new ( + arr, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_id", + second_id), + GNUNET_JSON_pack_string ("quantity", + second_qty)))); + return arr; +} + + +/** * Execute the taler-merchant-webhook command with * our configuration file. * @@ -1799,6 +1894,348 @@ run (void *cls, "EUR:4.99", "EUR:4.99", NULL), + cmd_transfer_to_exchange ("create-reserve-20y", + "EUR:10.02"), + cmd_exec_wirewatch ("wirewatch-20y"), + TALER_TESTING_cmd_check_bank_admin_transfer ("check_bank_transfer-20y", + "EUR:10.02", + payer_payto, + exchange_payto, + "create-reserve-20y"), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-yk", + "create-reserve-20y", + "EUR:5", + 0, + MHD_HTTP_OK), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-ykb", + "create-reserve-20y", + "EUR:5", + 0, + MHD_HTTP_OK), + TALER_TESTING_cmd_status ("withdraw-status-20y", + "create-reserve-20y", + "EUR:0", + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_post_products ("post-products-inv-1", + merchant_url, + "inv-product-1", + "Inventory Product One", + "EUR:1", + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_products ("post-products-inv-2", + merchant_url, + "inv-product-2", + "Inventory Product Two", + "EUR:2", + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_templates2 ( + "post-templates-inv-one", + merchant_url, + "template-inv-one", + "inventory template one", + NULL, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("template_type", + "inventory-cart"), + GNUNET_JSON_pack_string ("summary", + "inventory template"), + GNUNET_JSON_pack_time_rel ("pay_duration", + GNUNET_TIME_UNIT_MINUTES), + GNUNET_JSON_pack_array_steal ( + "selected_products", + make_selected_products ("inv-product-1", + "inv-product-2")), + GNUNET_JSON_pack_bool ("choose_one", + true)), + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_using_templates2 ( + "using-templates-inv-one", + "post-templates-inv-one", + NULL, + merchant_url, + "inv-1", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("amount", + "EUR:1"), + GNUNET_JSON_pack_array_steal ( + "inventory_selection", + make_inventory_selection ("inv-product-1", + "1.0", + NULL, + NULL))), + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_post_templates2 ( + "post-templates-inv-multi", + merchant_url, + "template-inv-multi", + "inventory template multi", + NULL, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("template_type", + "inventory-cart"), + GNUNET_JSON_pack_string ("summary", + "inventory template multi"), + GNUNET_JSON_pack_time_rel ("pay_duration", + GNUNET_TIME_UNIT_MINUTES), + GNUNET_JSON_pack_array_steal ( + "selected_products", + make_selected_products ("inv-product-1", + "inv-product-2"))), + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_using_templates2 ( + "using-templates-inv-multi", + "post-templates-inv-multi", + NULL, + merchant_url, + "inv-2", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("amount", + "EUR:3"), + GNUNET_JSON_pack_array_steal ( + "inventory_selection", + make_inventory_selection ("inv-product-1", + "1.0", + "inv-product-2", + "1.0"))), + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_post_using_templates2 ( + "using-templates-inv-one-empty", + "post-templates-inv-one", + NULL, + merchant_url, + "inv-1-empty", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("amount", + "EUR:1"), + GNUNET_JSON_pack_array_steal ( + "inventory_selection", + json_array ())), + MHD_HTTP_CONFLICT), + TALER_TESTING_cmd_merchant_post_using_templates2 ( + "using-templates-inv-one-two", + "post-templates-inv-one", + NULL, + merchant_url, + "inv-1-two", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("amount", + "EUR:3"), + GNUNET_JSON_pack_array_steal ( + "inventory_selection", + make_inventory_selection ("inv-product-1", + "1.0", + "inv-product-2", + "1.0"))), + MHD_HTTP_CONFLICT), + TALER_TESTING_cmd_merchant_post_using_templates2 ( + "using-templates-inv-one-tip", + "post-templates-inv-one", + NULL, + merchant_url, + "inv-1-tip", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("amount", + "EUR:1"), + GNUNET_JSON_pack_string ("tip", + "EUR:1"), + GNUNET_JSON_pack_array_steal ( + "inventory_selection", + make_inventory_selection ("inv-product-1", + "1.0", + NULL, + NULL))), + MHD_HTTP_CONFLICT), + TALER_TESTING_cmd_merchant_post_using_templates2 ( + "using-templates-inv-one-unknown", + "post-templates-inv-one", + NULL, + merchant_url, + "inv-1-unknown", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("amount", + "EUR:1"), + GNUNET_JSON_pack_array_steal ( + "inventory_selection", + make_inventory_selection ("inv-product-404", + "1.0", + NULL, + NULL))), + MHD_HTTP_NOT_FOUND), + TALER_TESTING_cmd_merchant_post_templates2 ( + "post-templates-inv-only", + merchant_url, + "template-inv-only", + "inventory template only", + NULL, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("template_type", + "inventory-cart"), + GNUNET_JSON_pack_string ("summary", + "inventory template only"), + GNUNET_JSON_pack_time_rel ("pay_duration", + GNUNET_TIME_UNIT_MINUTES), + GNUNET_JSON_pack_array_steal ( + "selected_products", + make_selected_products ("inv-product-1", + NULL))), + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_using_templates2 ( + "using-templates-inv-only-wrong", + "post-templates-inv-only", + NULL, + merchant_url, + "inv-only-wrong", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("amount", + "EUR:2"), + GNUNET_JSON_pack_array_steal ( + "inventory_selection", + make_inventory_selection ("inv-product-2", + "1.0", + NULL, + NULL))), + MHD_HTTP_CONFLICT), + TALER_TESTING_cmd_merchant_post_using_templates2 ( + "using-templates-inv-multi-amount-mismatch", + "post-templates-inv-multi", + NULL, + merchant_url, + "inv-2-mismatch", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("amount", + "EUR:4"), + GNUNET_JSON_pack_array_steal ( + "inventory_selection", + make_inventory_selection ("inv-product-1", + "1.0", + "inv-product-2", + "1.0"))), + MHD_HTTP_CONFLICT), + TALER_TESTING_cmd_merchant_post_units ("post-unit-special", + merchant_url, + "special-unit", + "Special Unit", + "sunit", + true, + 1, + true, + json_pack ("{s:s}", + "en", + "Special Unit"), + json_pack ("{s:s}", + "en", + "sunit"), + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_categories ("post-category-special", + merchant_url, + "Category Special", + json_pack ("{s:s}", + "en", + "Category Special"), + 1, + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_post_categories ("post-category-special-2", + merchant_url, + "Category Special Two", + json_pack ("{s:s}", + "en", + "Category Special Two"), + 2, + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_post_products_with_categories ( + "post-products-inv-unit-1", + merchant_url, + "inv-unit-product-1", + "Inventory Unit Product One", + "special-unit", + "EUR:5", + 1, + (const uint64_t[]) { 1 }, + true, + 1, + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_products ( + "post-products-inv-unit-2", + merchant_url, + "inv-unit-product-2", + "Inventory Unit Product Two", + "EUR:1.2", + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_products_with_categories ( + "post-products-inv-unit-3", + merchant_url, + "inv-unit-product-3", + "Inventory Unit Product Three", + "test-unit", + "EUR:9", + 1, + (const uint64_t[]) { 2 }, + false, + 0, + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_templates2 ( + "post-templates-inv-cat-prod-unit", + merchant_url, + "template-inv-cat-prod-unit", + "inventory template category+product unit", + NULL, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("template_type", + "inventory-cart"), + GNUNET_JSON_pack_string ("summary", + "inventory template category+product unit"), + GNUNET_JSON_pack_time_rel ("pay_duration", + GNUNET_TIME_UNIT_MINUTES), + GNUNET_JSON_pack_array_steal ( + "selected_categories", + make_selected_categories (1, + 0)), + GNUNET_JSON_pack_array_steal ( + "selected_products", + make_selected_products ("inv-unit-product-2", + "inv-unit-product-3"))), + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_wallet_get_template ( + "wallet-get-template-inv-cat-prod-unit", + merchant_url, + "template-inv-cat-prod-unit", + 3, + "inv-unit-product-1", + "special-unit", + true, + 1, + json_pack ("{s:s}", + "en", + "sunit"), + "inv-unit-product-2", + "inv-unit-product-3", + 1, + 2, + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_post_using_templates2 ( + "using-templates-inv-cat-prod-unit", + "post-templates-inv-cat-prod-unit", + NULL, + merchant_url, + "inv-cat-prod-unit", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("amount", + "EUR:3.2"), + GNUNET_JSON_pack_array_steal ( + "inventory_selection", + make_inventory_selection ("inv-unit-product-1", + "0.4", + "inv-unit-product-2", + "1"))), + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_pay_order ("pay-inv-cat-prod-unit", + merchant_url, + MHD_HTTP_OK, + "using-templates-inv-cat-prod-unit", + "withdraw-coin-yk", + "EUR:3.2", + "EUR:3.2", + NULL), TALER_TESTING_cmd_end () diff --git a/src/testing/testing_api_cmd_post_categories.c b/src/testing/testing_api_cmd_post_categories.c @@ -0,0 +1,226 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 testing/testing_api_cmd_post_categories.c + * @brief command to test POST /private/categories + * @author Bohdan Potuzhnyi + */ +#include "platform.h" +#include <jansson.h> +#include <microhttpd.h> +#include <taler/taler_testing_lib.h> +#include "taler_merchant_service.h" +#include "taler_merchant_testing_lib.h" + +/** + * State of a "POST /private/categories" CMD. + */ +struct PostCategoriesState +{ + /** + * Handle for a "POST /private/categories" request. + */ + struct TALER_MERCHANT_CategoriesPostHandle *cph; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Base URL of the merchant serving the request. + */ + const char *merchant_url; + + /** + * Category name. + */ + const char *name; + + /** + * Category name translations. + */ + json_t *name_i18n; + + /** + * Expected HTTP response code. + */ + unsigned int http_status; + + /** + * Expected category id (0 to ignore). + */ + uint64_t expected_category_id; + + /** + * Category id returned by the backend. + */ + uint64_t category_id; +}; + +/** + * Callback for POST /private/categories. + * + * @param cls closure + * @param cpr response details + */ +static void +post_categories_cb (void *cls, + const struct TALER_MERCHANT_CategoriesPostResponse *cpr) +{ + struct PostCategoriesState *pcs = cls; + + pcs->cph = NULL; + if (pcs->http_status != cpr->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u (%d) to command %s\n", + cpr->hr.http_status, + (int) cpr->hr.ec, + TALER_TESTING_interpreter_get_current_label (pcs->is)); + TALER_TESTING_interpreter_fail (pcs->is); + return; + } + if (MHD_HTTP_OK == cpr->hr.http_status) + { + pcs->category_id = cpr->category_id; + if ( (0 != pcs->expected_category_id) && + (pcs->expected_category_id != pcs->category_id) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected category_id %lu (expected %lu)\n", + pcs->category_id, + pcs->expected_category_id); + TALER_TESTING_interpreter_fail (pcs->is); + return; + } + } + TALER_TESTING_interpreter_next (pcs->is); +} + + +/** + * Run the "POST /private/categories" CMD. + * + * @param cls closure. + * @param cmd command being run now. + * @param is interpreter state. + */ +static void +post_categories_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct PostCategoriesState *pcs = cls; + + pcs->is = is; + pcs->cph = TALER_MERCHANT_categories_post ( + TALER_TESTING_interpreter_get_context (is), + pcs->merchant_url, + pcs->name, + pcs->name_i18n, + &post_categories_cb, + pcs); + GNUNET_assert (NULL != pcs->cph); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param[out] ret result + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +post_categories_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct PostCategoriesState *pcs = cls; + struct TALER_TESTING_Trait traits[] = { + TALER_TESTING_make_trait_category_id (&pcs->category_id), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +/** + * Free the state of a "POST /private/categories" CMD, and possibly + * cancel a pending operation thereof. + * + * @param cls closure. + * @param cmd command being run. + */ +static void +post_categories_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct PostCategoriesState *pcs = cls; + + if (NULL != pcs->cph) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "POST /private/categories operation did not complete\n"); + TALER_MERCHANT_categories_post_cancel (pcs->cph); + } + json_decref (pcs->name_i18n); + GNUNET_free (pcs); +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_categories ( + const char *label, + const char *merchant_url, + const char *name, + json_t *name_i18n, + uint64_t expected_category_id, + unsigned int http_status) +{ + struct PostCategoriesState *pcs; + + GNUNET_assert ((NULL == name_i18n) || + json_is_object (name_i18n)); + pcs = GNUNET_new (struct PostCategoriesState); + pcs->merchant_url = merchant_url; + pcs->name = name; + pcs->name_i18n = name_i18n; /* ownership taken */ + pcs->http_status = http_status; + pcs->expected_category_id = expected_category_id; + { + struct TALER_TESTING_Command cmd = { + .cls = pcs, + .label = label, + .run = &post_categories_run, + .cleanup = &post_categories_cleanup, + .traits = &post_categories_traits + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_post_categories.c */ diff --git a/src/testing/testing_api_cmd_post_products.c b/src/testing/testing_api_cmd_post_products.c @@ -131,6 +131,16 @@ struct PostProductsState struct GNUNET_TIME_Timestamp next_restock; /** + * Categories array length. + */ + unsigned int num_cats; + + /** + * Categories array. + */ + uint64_t *cats; + + /** * Expected HTTP response code. */ unsigned int http_status; @@ -300,29 +310,57 @@ post_products_run (void *cls, pis->address, pis->next_restock, pis->minimum_age, - 0, - NULL, + pis->num_cats, + pis->cats, &post_products_cb, pis); } else { - pis->iph = TALER_MERCHANT_products_post2 ( - TALER_TESTING_interpreter_get_context (is), - pis->merchant_url, - pis->product_id, - pis->description, - pis->description_i18n, - pis->unit, - &pis->price, - pis->image, - pis->taxes, - pis->total_stock, - pis->address, - pis->next_restock, - pis->minimum_age, - &post_products_cb, - pis); + if (0 < pis->num_cats) + { + pis->iph = TALER_MERCHANT_products_post4 ( + TALER_TESTING_interpreter_get_context (is), + pis->merchant_url, + pis->product_id, + pis->description, + pis->description_i18n, + pis->unit, + &pis->price, + 1, + pis->image, + pis->taxes, + pis->total_stock, + 0, + false, + NULL, + pis->address, + pis->next_restock, + pis->minimum_age, + pis->num_cats, + pis->cats, + &post_products_cb, + pis); + } + else + { + pis->iph = TALER_MERCHANT_products_post2 ( + TALER_TESTING_interpreter_get_context (is), + pis->merchant_url, + pis->product_id, + pis->description, + pis->description_i18n, + pis->unit, + &pis->price, + pis->image, + pis->taxes, + pis->total_stock, + pis->address, + pis->next_restock, + pis->minimum_age, + &post_products_cb, + pis); + } } GNUNET_assert (NULL != pis->iph); } @@ -396,6 +434,7 @@ post_products_cleanup (void *cls, GNUNET_free (pis->image); json_decref (pis->taxes); json_decref (pis->address); + GNUNET_free (pis->cats); GNUNET_free (pis); } @@ -443,6 +482,8 @@ TALER_TESTING_cmd_merchant_post_products2 ( pis->minimum_age = minimum_age; pis->address = address; /* ownership taken */ pis->next_restock = next_restock; + pis->num_cats = 0; + pis->cats = NULL; post_products_update_unit_total_stock (pis); { struct TALER_TESTING_Command cmd = { @@ -532,4 +573,55 @@ TALER_TESTING_cmd_merchant_post_products ( } +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_products_with_categories ( + const char *label, + const char *merchant_url, + const char *product_id, + const char *description, + const char *unit, + const char *price, + unsigned int num_cats, + const uint64_t *cats, + bool unit_allow_fraction, + uint32_t unit_precision_level, + unsigned int http_status) +{ + struct TALER_TESTING_Command cmd; + + cmd = TALER_TESTING_cmd_merchant_post_products2 ( + label, + merchant_url, + product_id, + description, + json_pack ("{s:s}", "en", description), + unit, + price, + "", + json_array (), + 4, /* total stock */ + 0, /* minimum age */ + json_pack ("{s:s}", "street", "my street"), + GNUNET_TIME_UNIT_ZERO_TS, + http_status); + { + struct PostProductsState *pis = cmd.cls; + + pis->num_cats = num_cats; + if (0 < num_cats) + { + pis->cats = GNUNET_new_array (num_cats, + uint64_t); + for (unsigned int i = 0; i < num_cats; i++) + pis->cats[i] = cats[i]; + } + pis->unit_allow_fraction = unit_allow_fraction; + pis->unit_precision_level = unit_precision_level; + if (unit_allow_fraction) + pis->use_fractional = true; + } + return cmd; +} + + /* end of testing_api_cmd_post_products.c */ diff --git a/src/testing/testing_api_cmd_post_using_templates.c b/src/testing/testing_api_cmd_post_using_templates.c @@ -72,6 +72,11 @@ struct PostUsingTemplatesState struct TALER_Amount amount; /** + * Raw request body for inventory templates (if set). + */ + json_t *details; + + /** * Label of a command that created the template we should use. */ const char *template_ref; @@ -376,16 +381,29 @@ post_using_templates_run (void *cls, &tis->otp_alg)) TALER_TESTING_FAIL (is); } - tis->iph = TALER_MERCHANT_using_templates_post ( - TALER_TESTING_interpreter_get_context (is), - tis->merchant_url, - template_id, - tis->summary, - TALER_amount_is_valid (&tis->amount) - ? &tis->amount - : NULL, - &post_using_templates_cb, - tis); + if (NULL != tis->details) + { + tis->iph = TALER_MERCHANT_using_templates_post2 ( + TALER_TESTING_interpreter_get_context (is), + tis->merchant_url, + template_id, + tis->details, + &post_using_templates_cb, + tis); + } + else + { + tis->iph = TALER_MERCHANT_using_templates_post ( + TALER_TESTING_interpreter_get_context (is), + tis->merchant_url, + template_id, + tis->summary, + TALER_amount_is_valid (&tis->amount) + ? &tis->amount + : NULL, + &post_using_templates_cb, + tis); + } GNUNET_assert (NULL != tis->iph); } @@ -450,6 +468,7 @@ post_using_templates_cleanup (void *cls, } json_decref (tis->order_terms); json_decref (tis->contract_terms); + json_decref (tis->details); GNUNET_free (tis->order_id); GNUNET_free (tis); } @@ -604,4 +623,39 @@ TALER_TESTING_cmd_merchant_post_using_templates ( } +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_using_templates2 ( + const char *label, + const char *template_ref, + const char *otp_ref, + const char *merchant_url, + const char *using_template_id, + const json_t *details, + unsigned int http_status) +{ + struct PostUsingTemplatesState *tis; + + tis = GNUNET_new (struct PostUsingTemplatesState); + tis->template_ref = template_ref; + tis->otp_ref = otp_ref; + tis->merchant_url = merchant_url; + tis->using_template_id = using_template_id; + tis->http_status = http_status; + tis->with_claim = true; + if (NULL != details) + tis->details = json_incref ((json_t *) details); + { + struct TALER_TESTING_Command cmd = { + .cls = tis, + .label = label, + .run = &post_using_templates_run, + .cleanup = &post_using_templates_cleanup, + .traits = &post_using_templates_traits + }; + + return cmd; + } +} + + /* end of testing_api_cmd_post_using_templates.c */ diff --git a/src/testing/testing_api_cmd_wallet_get_template.c b/src/testing/testing_api_cmd_wallet_get_template.c @@ -0,0 +1,449 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 testing/testing_api_cmd_wallet_get_template.c + * @brief command to test GET /templates/$ID (wallet) + * @author Bohdan Potuzhnyi + */ +#include "platform.h" +#include <microhttpd.h> +#include <jansson.h> +#include <taler/taler_testing_lib.h> +#include "taler_merchant_service.h" +#include "taler_merchant_testing_lib.h" + +/** + * State of a "GET template" wallet CMD. + */ +struct WalletGetTemplateState +{ + /** + * Handle for a "GET template" request. + */ + struct TALER_MERCHANT_WalletTemplateGetHandle *igh; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Base URL of the merchant serving the request. + */ + const char *merchant_url; + + /** + * ID of the template to run GET for. + */ + const char *template_id; + + /** + * Expected product count (0 to ignore). + */ + size_t expected_products_len; + + /** + * Product id to verify unit info for (optional). + */ + const char *expected_product_id; + + /** + * Expected unit for @e expected_product_id. + */ + const char *expected_unit; + + /** + * Expected allow_fraction for @e expected_product_id. + */ + bool expected_unit_allow_fraction; + + /** + * Expected precision for @e expected_product_id. + */ + uint32_t expected_unit_precision_level; + + /** + * Expected unit name short i18n for @e expected_unit. + */ + json_t *expected_unit_name_short_i18n; + + /** + * Optional second product id expected to be present. + */ + const char *expected_product_id2; + + /** + * Optional third product id expected to be present. + */ + const char *expected_product_id3; + + /** + * Expected category id 1 (0 to ignore). + */ + uint64_t expected_category_id1; + + /** + * Expected category id 2 (0 to ignore). + */ + uint64_t expected_category_id2; + + /** + * Expected HTTP response code. + */ + unsigned int http_status; +}; + +static bool +product_id_matches (const json_t *product, + const char *expected_id) +{ + const json_t *id_val; + + if (NULL == expected_id) + return false; + id_val = json_object_get (product, + "product_id"); + if (! json_is_string (id_val)) + return false; + return (0 == strcmp (json_string_value (id_val), + expected_id)); +} + + +/** + * Callback for a wallet /get/templates/$ID operation. + * + * @param cls closure for this function + * @param tgr HTTP response details + */ +static void +wallet_get_template_cb (void *cls, + const struct + TALER_MERCHANT_WalletTemplateGetResponse *tgr) +{ + struct WalletGetTemplateState *wgs = cls; + + wgs->igh = NULL; + if (wgs->http_status != tgr->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u (%d) to command %s\n", + tgr->hr.http_status, + (int) tgr->hr.ec, + TALER_TESTING_interpreter_get_current_label (wgs->is)); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + if (MHD_HTTP_OK == tgr->hr.http_status) + { + const json_t *template_contract = tgr->details.ok.template_contract; + const json_t *inventory_payload; + const json_t *products; + + inventory_payload = json_object_get (template_contract, + "inventory_payload"); + if (! json_is_object (inventory_payload)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Missing inventory_payload in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + products = json_object_get (inventory_payload, + "products"); + if (! json_is_array (products)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Missing products in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + if ( (0 < wgs->expected_products_len) && + (json_array_size (products) != wgs->expected_products_len) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected products length\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + + { + bool found_unit = (NULL == wgs->expected_product_id); + bool found_extra = (NULL == wgs->expected_product_id2); + bool found_extra2 = (NULL == wgs->expected_product_id3); + + for (size_t i = 0; i < json_array_size (products); i++) + { + const json_t *product = json_array_get (products, + i); + + if (product_id_matches (product, + wgs->expected_product_id)) + { + const json_t *unit_val; + const json_t *allow_val; + const json_t *prec_val; + + unit_val = json_object_get (product, + "unit"); + allow_val = json_object_get (product, + "unit_allow_fraction"); + prec_val = json_object_get (product, + "unit_precision_level"); + if ( (NULL == unit_val) || + (! json_is_string (unit_val)) || + (0 != strcmp (json_string_value (unit_val), + wgs->expected_unit)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected unit in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + if ( (! json_is_boolean (allow_val)) || + (json_boolean_value (allow_val) != + wgs->expected_unit_allow_fraction) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected unit_allow_fraction in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + if ( (! json_is_integer (prec_val)) || + ((uint32_t) json_integer_value (prec_val) != + wgs->expected_unit_precision_level) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected unit_precision_level in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + found_unit = true; + } + if (product_id_matches (product, + wgs->expected_product_id2)) + found_extra = true; + if (product_id_matches (product, + wgs->expected_product_id3)) + found_extra2 = true; + } + if (! found_unit || ! found_extra || ! found_extra2) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Expected product ids missing in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + } + + if (NULL != wgs->expected_unit_name_short_i18n) + { + const json_t *units; + bool found_unit_i18n = false; + + units = json_object_get (inventory_payload, + "units"); + if (! json_is_array (units)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Missing units in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + for (size_t i = 0; i < json_array_size (units); i++) + { + const json_t *unit = json_array_get (units, + i); + const json_t *unit_id; + const json_t *unit_i18n; + + unit_id = json_object_get (unit, + "unit"); + if (! json_is_string (unit_id)) + continue; + if (0 != strcmp (json_string_value (unit_id), + wgs->expected_unit)) + continue; + unit_i18n = json_object_get (unit, + "unit_name_short_i18n"); + if ( (NULL == unit_i18n) || + (! json_is_object (unit_i18n)) || + (1 != json_equal (unit_i18n, + wgs->expected_unit_name_short_i18n)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected unit_name_short_i18n in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + found_unit_i18n = true; + break; + } + if (! found_unit_i18n) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Expected unit entry missing in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + } + + if ( (0 != wgs->expected_category_id1) || + (0 != wgs->expected_category_id2) ) + { + const json_t *categories; + bool found_cat1 = (0 == wgs->expected_category_id1); + bool found_cat2 = (0 == wgs->expected_category_id2); + + categories = json_object_get (inventory_payload, + "categories"); + if (! json_is_array (categories)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Missing categories in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + for (size_t i = 0; i < json_array_size (categories); i++) + { + const json_t *category = json_array_get (categories, + i); + const json_t *cid; + + cid = json_object_get (category, + "category_id"); + if (! json_is_integer (cid)) + continue; + if ( (0 != wgs->expected_category_id1) && + ((uint64_t) json_integer_value (cid) + == wgs->expected_category_id1) ) + found_cat1 = true; + if ( (0 != wgs->expected_category_id2) && + ((uint64_t) json_integer_value (cid) + == wgs->expected_category_id2) ) + found_cat2 = true; + } + if (! found_cat1 || ! found_cat2) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Expected category ids missing in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + } + } + TALER_TESTING_interpreter_next (wgs->is); +} + + +/** + * Run the "GET /templates/$ID" wallet CMD. + * + * @param cls closure. + * @param cmd command being run now. + * @param is interpreter state. + */ +static void +wallet_get_template_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct WalletGetTemplateState *wgs = cls; + + wgs->is = is; + wgs->igh = TALER_MERCHANT_wallet_template_get ( + TALER_TESTING_interpreter_get_context (is), + wgs->merchant_url, + wgs->template_id, + &wallet_get_template_cb, + wgs); + GNUNET_assert (NULL != wgs->igh); +} + + +/** + * Free the state of a "GET template" CMD, and possibly + * cancel a pending operation thereof. + * + * @param cls closure. + * @param cmd command being run. + */ +static void +wallet_get_template_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct WalletGetTemplateState *wgs = cls; + + if (NULL != wgs->igh) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "GET /templates/$ID operation did not complete\n"); + TALER_MERCHANT_wallet_template_get_cancel (wgs->igh); + } + json_decref (wgs->expected_unit_name_short_i18n); + GNUNET_free (wgs); +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_wallet_get_template ( + const char *label, + const char *merchant_url, + const char *template_id, + size_t expected_products_len, + const char *expected_product_id, + const char *expected_unit, + bool expected_unit_allow_fraction, + uint32_t expected_unit_precision_level, + json_t *expected_unit_name_short_i18n, + const char *expected_product_id2, + const char *expected_product_id3, + uint64_t expected_category_id1, + uint64_t expected_category_id2, + unsigned int http_status) +{ + struct WalletGetTemplateState *wgs; + + wgs = GNUNET_new (struct WalletGetTemplateState); + wgs->merchant_url = merchant_url; + wgs->template_id = template_id; + wgs->expected_products_len = expected_products_len; + wgs->expected_product_id = expected_product_id; + wgs->expected_unit = expected_unit; + wgs->expected_unit_allow_fraction = expected_unit_allow_fraction; + wgs->expected_unit_precision_level = expected_unit_precision_level; + if (NULL != expected_unit_name_short_i18n) + wgs->expected_unit_name_short_i18n = + json_incref (expected_unit_name_short_i18n); + wgs->expected_product_id2 = expected_product_id2; + wgs->expected_product_id3 = expected_product_id3; + wgs->expected_category_id1 = expected_category_id1; + wgs->expected_category_id2 = expected_category_id2; + wgs->http_status = http_status; + { + struct TALER_TESTING_Command cmd = { + .cls = wgs, + .label = label, + .run = &wallet_get_template_run, + .cleanup = &wallet_get_template_cleanup + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_wallet_get_template.c */