commit 3a86481b5fa7935cf01e48ae4d638e5bac630527 parent a21ccce30297919d9ffb8581d2391d871db7cd10 Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com> Date: Wed, 7 Jan 2026 22:58:41 +0100 some updates to templates Diffstat:
13 files changed, 843 insertions(+), 222 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c @@ -466,6 +466,25 @@ TMH_products_array_valid (const json_t *products) } +enum GNUNET_GenericReturnValue +TMH_validate_unit_price_array (const struct TALER_Amount *prices, + size_t prices_len) +{ + /* Check for duplicate currencies */ + for (size_t i = 0; i < prices_len; i++) + { + for (size_t j = i + 1; j < prices_len; j++) + { + if (GNUNET_OK == + TALER_amount_cmp_currency (&prices[i], + &prices[j])) + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + enum TALER_MERCHANT_TemplateType TMH_template_type_from_contract (const json_t *template_contract) { diff --git a/src/backend/taler-merchant-httpd_helper.h b/src/backend/taler-merchant-httpd_helper.h @@ -79,6 +79,17 @@ TMH_parse_fractional_string (const char *value, uint32_t *fractional_part); /** + * Check that no two prices use the same currency. + * + * @param prices price list to check + * @param prices_len length of @a prices + * @return #GNUNET_OK if unique, #GNUNET_SYSERR otherwise + */ +enum GNUNET_GenericReturnValue +TMH_validate_unit_price_array (const struct TALER_Amount *prices, + size_t prices_len); + +/** * 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. @@ -408,13 +419,13 @@ TMH_make_order_status_url (struct MHD_Connection *con, * @param hr a `TALER_EXCHANGE_HttpResponse` */ #define TMH_pack_exchange_reply(hr) \ - GNUNET_JSON_pack_uint64 ("exchange_code", (hr)->ec), \ - GNUNET_JSON_pack_uint64 ("exchange_http_status", (hr)->http_status), \ - GNUNET_JSON_pack_uint64 ("exchange_ec", (hr)->ec), /* LEGACY */ \ - GNUNET_JSON_pack_uint64 ("exchange_hc", (hr)->http_status), /* LEGACY */ \ - GNUNET_JSON_pack_allow_null ( \ - GNUNET_JSON_pack_object_incref ("exchange_reply", (json_t *) (hr)-> \ - reply)) + GNUNET_JSON_pack_uint64 ("exchange_code", (hr)->ec), \ + GNUNET_JSON_pack_uint64 ("exchange_http_status", (hr)->http_status), \ + GNUNET_JSON_pack_uint64 ("exchange_ec", (hr)->ec), /* LEGACY */ \ + GNUNET_JSON_pack_uint64 ("exchange_hc", (hr)->http_status), /* LEGACY */ \ + GNUNET_JSON_pack_allow_null ( \ + GNUNET_JSON_pack_object_incref ("exchange_reply", (json_t *) (hr)-> \ + reply)) /** diff --git a/src/backend/taler-merchant-httpd_post-using-templates.c b/src/backend/taler-merchant-httpd_post-using-templates.c @@ -27,8 +27,8 @@ #include "taler-merchant-httpd_post-using-templates.h" #include "taler-merchant-httpd_private-post-orders.h" #include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_exchanges.h" #include <taler/taler_json_lib.h> -#include <taler/taler_merchant_util.h> /** @@ -189,6 +189,269 @@ compute_line_total (const struct TALER_Amount *unit_price, /** + * Context for inventory template processing. + */ +struct InventoryTemplateContext +{ + /** + * Selected products from inventory_selection. + */ + struct InventoryTemplateItem + { + /** + * Product ID as referenced in inventory. + */ + char *product_id; + + /** + * Unit quantity string as provided by the client. + */ + char *unit_quantity; + + /** + * Parsed integer quantity. + */ + uint64_t quantity_value; + + /** + * Parsed fractional quantity. + */ + uint32_t quantity_frac; + + /** + * Product details from the DB (includes price array). + */ + struct TALER_MERCHANTDB_ProductDetails pd; + } *items; + + /** + * Length of @e items. + */ + unsigned int items_len; + + /** + * Selected categories from the template contract. + */ + const json_t *selected_categories; + + /** + * Selected products from the template contract. + */ + const json_t *selected_products; + + /** + * Whether all products are selectable. + */ + bool selected_all; + + /** + * Amount provided by the client. + */ + struct TALER_Amount amount; + + /** + * Optional tip amount. + */ + struct TALER_Amount tip; + + /** + * Total amount for the requested currency (includes tip when present). + */ + struct TALER_Amount total; + + /** + * Currency totals shared across selected products. + * Without amount.currency + */ + struct InventoryTemplateCurrency + { + /** + * Currency string. + */ + char *currency; + + /** + * Total amount for this currency (without tips). + */ + struct TALER_Amount total; + } *currencies; + + /** + * Length of @e currencies. + */ + unsigned int currencies_len; +}; + + +/** + * Clean up a `struct InventoryTemplateContext *`. + * + * @param cls a `struct InventoryTemplateContext *` + */ +static void +cleanup_inventory_template_context (void *cls) +{ + struct InventoryTemplateContext *ctx = cls; + + if (NULL == ctx) + return; + for (unsigned int i = 0; i < ctx->items_len; i++) + { + struct InventoryTemplateItem *item = &ctx->items[i]; + + GNUNET_free (item->product_id); + GNUNET_free (item->unit_quantity); + TALER_MERCHANTDB_product_details_free (&item->pd); + } + GNUNET_free (ctx->items); + for (unsigned int i = 0; i < ctx->currencies_len; i++) + GNUNET_free (ctx->currencies[i].currency); + GNUNET_free (ctx->currencies); + memset (ctx, + 0, + sizeof (*ctx)); +} + + +/** + * Compute totals for all currencies shared across selected products. + * + * @param[in,out] ctx inventory template context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +compute_totals_per_currency (struct InventoryTemplateContext *ctx) +{ + struct TALER_Amount line_total; + + if (0 == ctx->items_len) + return GNUNET_SYSERR; + + for (size_t i = 0; i < ctx->items[0].pd.price_array_length; i++) + { + const char *currency = ctx->items[0].pd.price_array[i].currency; + + if (0 == strcmp (currency, + ctx->amount.currency)) + continue; + + if (! TMH_test_exchange_configured_for_currency (currency)) + continue; + + GNUNET_array_append (ctx->currencies, + ctx->currencies_len, + (struct InventoryTemplateCurrency) { + .currency = GNUNET_strdup (currency) + }); + if (GNUNET_OK != + TALER_amount_set_zero (currency, + &ctx->currencies[ctx->currencies_len - 1].total)) + return GNUNET_SYSERR; + } + + if (0 == ctx->currencies_len) + return GNUNET_OK; // If there is only main currency, all good we just return + + /* Just loop through items, for each item check that all currencies from item[0] + is in this item, if not remove the currency, in the process pre-calculate + total per currency except the requested currency */ + for (unsigned int i = 1; i < ctx->items_len; i++) + { + const struct InventoryTemplateItem *item = &ctx->items[i]; + for (unsigned int c = 0; c < ctx->currencies_len;) + { + const struct TALER_Amount *unit_price = NULL; + + for (size_t j = 0; j < item->pd.price_array_length; j++) + { + if (0 == strcmp (item->pd.price_array[j].currency, + ctx->currencies[c].currency)) + { + unit_price = &item->pd.price_array[j]; + break; + } + } + if (NULL == unit_price) + { + GNUNET_free (ctx->currencies[c].currency); + ctx->currencies[c] = ctx->currencies[ctx->currencies_len - 1]; + ctx->currencies_len--; + continue; + } + if (GNUNET_OK != + compute_line_total (unit_price, + item->quantity_value, + item->quantity_frac, + &line_total)) + return GNUNET_SYSERR; + if (0 > + TALER_amount_add (&ctx->currencies[c].total, + &ctx->currencies[c].total, + &line_total)) + return GNUNET_SYSERR; + c++; + } + } + + return (0 == ctx->currencies_len) + ? GNUNET_SYSERR + : GNUNET_OK; +} + + +/** + * Compute total for a specific currency. + * + * @param[in] ctx inventory template context + * @param[in] currency currency to total + * @param[out] total computed total + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +compute_inventory_total (const struct InventoryTemplateContext *ctx, + const char *currency, + struct TALER_Amount *total) +{ + struct TALER_Amount line_total; + + if (GNUNET_OK != + TALER_amount_set_zero (currency, + total)) + return GNUNET_SYSERR; + for (unsigned int i = 0; i < ctx->items_len; i++) + { + const struct InventoryTemplateItem *item = &ctx->items[i]; + const struct TALER_Amount *unit_price = NULL; + + for (size_t j = 0; j < item->pd.price_array_length; j++) + { + if (0 == strcmp (item->pd.price_array[j].currency, + currency)) + { + unit_price = &item->pd.price_array[j]; + break; + } + } + if (NULL == unit_price) + return GNUNET_SYSERR; + if (GNUNET_OK != + compute_line_total (unit_price, + item->quantity_value, + item->quantity_frac, + &line_total)) + return GNUNET_SYSERR; + if (0 > + TALER_amount_add (total, + total, + &line_total)) + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + +/** * Handle POST /templates/$ID for inventory templates. * * @param connection connection to reply on @@ -209,29 +472,22 @@ handle_using_templates_inventory (struct MHD_Connection *connection, { struct TMH_MerchantInstance *mi = hc->instance; 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 InventoryTemplateContext itc; 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), + TALER_JSON_spec_amount_any ("amount", + &itc.amount), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_amount_any ("tip", - &tip), + &itc.tip), &no_tip), GNUNET_JSON_spec_array_const ("inventory_selection", &inventory_selection), @@ -240,8 +496,10 @@ handle_using_templates_inventory (struct MHD_Connection *connection, enum GNUNET_GenericReturnValue res; json_t *inventory_products; json_t *tip_products = NULL; - struct TALER_Amount total; - bool total_init = false; + + memset (&itc, + 0, + sizeof (itc)); res = TALER_MHD_parse_json_data (connection, hc->request_body, @@ -254,58 +512,51 @@ handle_using_templates_inventory (struct MHD_Connection *connection, : MHD_NO; } - if (no_amount) { - 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"); - } + struct GNUNET_JSON_Specification tcspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("summary", + &tsummary), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_categories", + &itc.selected_categories), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_products", + &itc.selected_products), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("choose_one", + &choose_one), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("request_tip", + &request_tip), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("selected_all", + &itc.selected_all), + NULL), + GNUNET_JSON_spec_end () + }; + const char *err_name; + unsigned int err_line; - { - 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); + res = GNUNET_JSON_parse (uc->etp.template_contract, + tcspec, + &err_name, + &err_line); + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + err_name); + } } if ( (NULL != summary) && @@ -340,6 +591,40 @@ handle_using_templates_inventory (struct MHD_Connection *connection, "tip"); } + if (! TMH_test_exchange_configured_for_currency (itc.amount.currency)) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + "Currency for amount is not supported by backend"); + } + + if (! no_tip) + { + if (GNUNET_YES != + TALER_amount_cmp_currency (&itc.amount, + &itc.tip)) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + "Mismatch of currencies between the amount and tip"); + } + } + + if (0 == json_array_size (inventory_selection)) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "inventory_selection"); + } if (choose_one && (1 != json_array_size (inventory_selection))) { @@ -351,8 +636,6 @@ handle_using_templates_inventory (struct MHD_Connection *connection, "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; @@ -370,7 +653,6 @@ handle_using_templates_inventory (struct MHD_Connection *connection, }; 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; @@ -384,7 +666,7 @@ handle_using_templates_inventory (struct MHD_Connection *connection, { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); - json_decref (inventory_products); + cleanup_inventory_template_context (&itc); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, @@ -421,21 +703,21 @@ handle_using_templates_inventory (struct MHD_Connection *connection, GNUNET_assert (0); } GNUNET_JSON_parse_free (spec); - json_decref (inventory_products); + cleanup_inventory_template_context (&itc); return TALER_MHD_reply_with_error (connection, http_status, ec, product_id); } - if (! selected_all) + if (! itc.selected_all) { bool allowed = false; - if (product_id_selected (selected_products, + if (product_id_selected (itc.selected_products, product_id)) allowed = true; - else if (categories_selected (selected_categories, + else if (categories_selected (itc.selected_categories, num_categories, categories)) allowed = true; @@ -443,14 +725,14 @@ handle_using_templates_inventory (struct MHD_Connection *connection, if (! allowed) { GNUNET_JSON_parse_free (spec); - json_decref (inventory_products); GNUNET_free (categories); TALER_MERCHANTDB_product_details_free (&pd); + cleanup_inventory_template_context (&itc); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inventory_selection"); + "inventory_selection(selected product is not available for this template)"); } } @@ -467,9 +749,9 @@ handle_using_templates_inventory (struct MHD_Connection *connection, &eparam)) { GNUNET_JSON_parse_free (spec); - json_decref (inventory_products); GNUNET_free (categories); TALER_MERCHANTDB_product_details_free (&pd); + cleanup_inventory_template_context (&itc); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, @@ -477,87 +759,96 @@ handle_using_templates_inventory (struct MHD_Connection *connection, eparam); } - // FIXME: this was pd.price, which no longer exists. - // Bad hack, will not really work, need to properly - // support an *array* of prices here! - if (GNUNET_OK != - compute_line_total (&pd.price_array[0], - quantity_value, - quantity_frac, - &line_total)) + if (0 == pd.price_array_length) { + GNUNET_break (0); GNUNET_JSON_parse_free (spec); - json_decref (inventory_products); GNUNET_free (categories); TALER_MERCHANTDB_product_details_free (&pd); + cleanup_inventory_template_context (&itc); return TALER_MHD_reply_with_error ( connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "quantity"); + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "price_array"); } - 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); - } + struct InventoryTemplateItem item; + + memset (&item, + 0, + sizeof (item)); + item.product_id = GNUNET_strdup (product_id); + item.unit_quantity = GNUNET_strdup (quantity); + item.quantity_value = quantity_value; + item.quantity_frac = quantity_frac; + item.pd = pd; + GNUNET_array_append (itc.items, + itc.items_len, + item); + memset (&pd, + 0, + sizeof (pd)); + GNUNET_free (categories); + categories = NULL; } + } - 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 (GNUNET_OK != + compute_totals_per_currency (&itc)) + { + GNUNET_JSON_parse_free (spec); + cleanup_inventory_template_context (&itc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "calculation of currency totals failed"); } - if (! total_init) + if (GNUNET_OK != + compute_inventory_total (&itc, + itc.amount.currency, + &itc.total)) { GNUNET_JSON_parse_free (spec); - json_decref (inventory_products); + cleanup_inventory_template_context (&itc); return TALER_MHD_reply_with_error ( connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "inventory_selection"); + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "total amount calculation failed"); } if (! no_tip) { - if (0 > - TALER_amount_add (&total, - &total, - &tip)) + // TODO: HMMMMM... If we have tip, we can't really go for another choice... + // or we will have to indicate it somehow in the wallet, when user changes the choice... + if (GNUNET_OK != + TALER_amount_cmp_currency (&itc.tip, + &itc.total)) { GNUNET_JSON_parse_free (spec); - json_decref (inventory_products); + cleanup_inventory_template_context (&itc); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - tip.currency); + itc.tip.currency); + } + if (0 > + TALER_amount_add (&itc.total, + &itc.total, + &itc.tip)) + { + GNUNET_JSON_parse_free (spec); + cleanup_inventory_template_context (&itc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "tip"); } tip_products = json_array (); GNUNET_assert (0 == @@ -569,15 +860,15 @@ handle_using_templates_inventory (struct MHD_Connection *connection, GNUNET_JSON_pack_uint64 ("quantity", 1), TALER_JSON_pack_amount ("price", - &tip)))); + &itc.tip)))); } - if (0 != TALER_amount_cmp (&amount, - &total)) + if (0 != TALER_amount_cmp (&itc.amount, + &itc.total)) { GNUNET_JSON_parse_free (spec); - json_decref (inventory_products); json_decref (tip_products); + cleanup_inventory_template_context (&itc); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, @@ -585,8 +876,46 @@ handle_using_templates_inventory (struct MHD_Connection *connection, NULL); } + inventory_products = json_array (); + GNUNET_assert (NULL != inventory_products); + for (size_t i = 0; i < itc.items_len; i++) + { + const struct InventoryTemplateItem *item = &itc.items[i]; + + GNUNET_assert (0 == + json_array_append_new ( + inventory_products, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_id", + item->product_id), + GNUNET_JSON_pack_string ("unit_quantity", + item->unit_quantity)))); + } + { json_t *body; + json_t *choices; + + choices = json_array (); + GNUNET_assert (NULL != choices); + GNUNET_assert (0 == + json_array_append_new (choices, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &itc.total)))); + + for (size_t i = 0; i < itc.currencies_len; i++) + { + if (0 == strcmp (itc.currencies[i].currency, + itc.amount.currency)) + continue; + GNUNET_assert (0 == + json_array_append_new ( + choices, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &itc.currencies[i].total)))); + } body = GNUNET_JSON_PACK ( GNUNET_JSON_pack_allow_null ( @@ -597,8 +926,10 @@ handle_using_templates_inventory (struct MHD_Connection *connection, GNUNET_JSON_pack_object_steal ( "order", GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - &total), + GNUNET_JSON_pack_uint64 ("version", + 1), + GNUNET_JSON_pack_array_steal ("choices", + choices), GNUNET_JSON_pack_string ("summary", no_summary ? tsummary @@ -615,6 +946,7 @@ handle_using_templates_inventory (struct MHD_Connection *connection, GNUNET_assert (NULL != body); uc->ihc.request_body = body; } + cleanup_inventory_template_context (&itc); GNUNET_JSON_parse_free (spec); return TMH_private_post_orders ( NULL, /* not even used */ @@ -767,8 +1099,8 @@ handle_using_templates_fixed (struct MHD_Connection *connection, const struct TALER_Amount *total_amount; total_amount = no_amount ? &tamount : &amount; - if (0 != TALER_amount_cmp_currency (&tip, - total_amount)) + if (GNUNET_OK != TALER_amount_cmp_currency (&tip, + total_amount)) { GNUNET_JSON_parse_free (spec); json_decref (tip_products); @@ -843,7 +1175,6 @@ handle_using_templates_fixed (struct MHD_Connection *connection, GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_array_steal ("products", tip_products))))); - GNUNET_assert (NULL != body); uc->ihc.request_body = body; } } diff --git a/src/backend/taler-merchant-httpd_private-patch-products-ID.c b/src/backend/taler-merchant-httpd_private-patch-products-ID.c @@ -179,6 +179,16 @@ TMH_private_patch_products_ID ( goto cleanup; } } + if (GNUNET_OK != + TMH_validate_unit_price_array (pd.price_array, + pd.price_array_length)) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_price"); + goto cleanup; + } } else { diff --git a/src/backend/taler-merchant-httpd_private-patch-templates-ID.c b/src/backend/taler-merchant-httpd_private-patch-templates-ID.c @@ -95,6 +95,8 @@ determine_cause (struct MHD_Connection *connection, static bool validate_template_contract_by_type (const json_t *template_contract) { + /* For some special different post/patch checks + otherwise change/check the TMH_template */ switch (TMH_template_type_from_contract (template_contract)) { case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: diff --git a/src/backend/taler-merchant-httpd_private-post-products.c b/src/backend/taler-merchant-httpd_private-post-products.c @@ -169,6 +169,16 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, goto cleanup; } } + if (GNUNET_OK != + TMH_validate_unit_price_array (pd.price_array, + pd.price_array_length)) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_price"); + goto cleanup; + } } else { diff --git a/src/backend/taler-merchant-httpd_private-post-templates.c b/src/backend/taler-merchant-httpd_private-post-templates.c @@ -67,8 +67,8 @@ templates_equal (const struct TALER_MERCHANTDB_TemplateDetails *t1, 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 */ + /* For some special different post/patch checks + otherwise change/check the TMH_template */ switch (TMH_template_type_from_contract (template_contract)) { case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: diff --git a/src/include/taler_merchant_testing_lib.h b/src/include/taler_merchant_testing_lib.h @@ -413,6 +413,31 @@ TALER_TESTING_cmd_merchant_post_products3 ( struct GNUNET_TIME_Timestamp next_restock, unsigned int http_status); +/** + * Define a "POST /products" CMD with explicit unit_price array. + * + * @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 unit unit in which the product is measured + * @param unit_prices array of unit prices as strings + * @param unit_prices_len length of @a unit_prices + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_products_with_unit_prices ( + const char *label, + const char *merchant_url, + const char *product_id, + const char *description, + const char *unit, + const char *const *unit_prices, + size_t unit_prices_len, + unsigned int http_status); + /** * Define a "POST /products" CMD, simple version @@ -578,6 +603,31 @@ TALER_TESTING_cmd_merchant_patch_product2 ( struct GNUNET_TIME_Timestamp next_restock, unsigned int http_status); +/** + * Define a "PATCH /products/$ID" CMD with explicit unit_price array. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the + * PATCH /product request. + * @param product_id the ID of the product to query + * @param description description of the product + * @param unit unit in which the product is measured + * @param unit_prices array of unit prices as strings + * @param unit_prices_len length of @a unit_prices + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_patch_product_with_unit_prices ( + const char *label, + const char *merchant_url, + const char *product_id, + const char *description, + const char *unit, + const char *const *unit_prices, + size_t unit_prices_len, + unsigned int http_status); + /** * Define a "GET /products" CMD. diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h @@ -24,7 +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> diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -49,7 +49,7 @@ * commands should NOT wait for this timeout! */ #define POLL_ORDER_TIMEOUT \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 60) + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 60) /** * The 'poll-orders-conclude-1x' and other 'conclude' @@ -57,7 +57,7 @@ * here we use a short value! */ #define POLL_ORDER_SHORT_TIMEOUT \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 2) + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 2) /** * Configuration file we use. One (big) configuration is used @@ -288,8 +288,8 @@ cmd_exec_wirewatch (const char *label) * @param label label to use for the command. */ #define CMD_EXEC_AGGREGATOR(label) \ - TALER_TESTING_cmd_exec_aggregator (label "-aggregator", config_file), \ - TALER_TESTING_cmd_exec_transfer (label "-transfer", config_file) + TALER_TESTING_cmd_exec_aggregator (label "-aggregator", config_file), \ + TALER_TESTING_cmd_exec_transfer (label "-transfer", config_file) /** @@ -726,6 +726,15 @@ run (void *cls, "a product", "EUR:1", MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_products_with_unit_prices ( + "post-products-dup-currency", + merchant_url, + "product-dup-currency", + "duplicate currency product", + "test-unit", + (const char *[]) { "EUR:1", "EUR:2" }, + 2, + MHD_HTTP_BAD_REQUEST), TALER_TESTING_cmd_merchant_post_products2 ("post-products-p4", merchant_url, "product-4age", @@ -765,6 +774,15 @@ run (void *cls, "product-frac", MHD_HTTP_OK, "post-products-frac"), + TALER_TESTING_cmd_merchant_patch_product_with_unit_prices ( + "patch-products-dup-currency", + merchant_url, + "product-3", + "a product", + "can", + (const char *[]) { "EUR:1", "EUR:2" }, + 2, + MHD_HTTP_BAD_REQUEST), TALER_TESTING_cmd_merchant_patch_product ("patch-products-p3", merchant_url, "product-3", @@ -1916,18 +1934,24 @@ run (void *cls, "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_products_with_unit_prices ( + "post-products-inv-1", + merchant_url, + "inv-product-1", + "Inventory Product One", + "test-unit", + (const char *[]) { "EUR:1", "KUDOS:1" }, + 2, + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_products_with_unit_prices ( + "post-products-inv-2", + merchant_url, + "inv-product-2", + "Inventory Product Two", + "test-unit", + (const char *[]) { "EUR:2", "KUDOS:2" }, + 2, + MHD_HTTP_NO_CONTENT), TALER_TESTING_cmd_merchant_post_templates2 ( "post-templates-inv-one", merchant_url, @@ -1948,6 +1972,21 @@ run (void *cls, GNUNET_JSON_pack_bool ("choose_one", true)), MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_wallet_get_template ( + "wallet-get-template-inv-one", + merchant_url, + "template-inv-one", + 0, + NULL, + NULL, + false, + 0, + NULL, + NULL, + NULL, + 0, + 0, + MHD_HTTP_OK), TALER_TESTING_cmd_merchant_post_using_templates2 ( "using-templates-inv-one", "post-templates-inv-one", @@ -2010,7 +2049,7 @@ run (void *cls, GNUNET_JSON_pack_array_steal ( "inventory_selection", json_array ())), - MHD_HTTP_CONFLICT), + MHD_HTTP_BAD_REQUEST), TALER_TESTING_cmd_merchant_post_using_templates2 ( "using-templates-inv-one-two", "post-templates-inv-one", @@ -2228,14 +2267,17 @@ run (void *cls, "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_merchant_pay_order_choices ( + "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, + 0, + NULL), TALER_TESTING_cmd_end () diff --git a/src/testing/testing_api_cmd_patch_product.c b/src/testing/testing_api_cmd_patch_product.c @@ -76,6 +76,26 @@ struct PatchProductState struct TALER_Amount price; /** + * Array of unit prices (may point to @e price for single-entry usage). + */ + struct TALER_Amount *unit_prices; + + /** + * Number of entries in @e unit_prices. + */ + size_t unit_prices_len; + + /** + * True if @e unit_prices must be freed. + */ + bool owns_unit_prices; + + /** + * True if we should send an explicit unit_price array. + */ + bool use_unit_price_array; + + /** * base64-encoded product image */ char *image; @@ -286,8 +306,8 @@ patch_product_run (void *cls, pis->description, pis->description_i18n, pis->unit, - &pis->price, - 1, + pis->unit_prices, + pis->unit_prices_len, pis->image, pis->taxes, pis->total_stock, @@ -389,6 +409,8 @@ patch_product_cleanup (void *cls, "PATCH /products/$ID operation did not complete\n"); TALER_MERCHANT_product_patch_cancel (pis->iph); } + if (pis->owns_unit_prices) + GNUNET_free (pis->unit_prices); json_decref (pis->description_i18n); GNUNET_free (pis->image); json_decref (pis->taxes); @@ -429,6 +451,10 @@ TALER_TESTING_cmd_merchant_patch_product ( GNUNET_assert (GNUNET_OK == TALER_string_to_amount (price, &pis->price)); + pis->unit_prices = &pis->price; + pis->unit_prices_len = 1; + pis->owns_unit_prices = false; + pis->use_unit_price_array = false; pis->image = GNUNET_strdup (image); pis->taxes = taxes; /* ownership taken */ pis->total_stock = total_stock; @@ -500,4 +526,62 @@ TALER_TESTING_cmd_merchant_patch_product2 ( } +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_patch_product_with_unit_prices ( + const char *label, + const char *merchant_url, + const char *product_id, + const char *description, + const char *unit, + const char *const *unit_prices, + size_t unit_prices_len, + unsigned int http_status) +{ + struct PatchProductState *pis; + + GNUNET_assert (0 < unit_prices_len); + GNUNET_assert (NULL != unit_prices); + pis = GNUNET_new (struct PatchProductState); + pis->merchant_url = merchant_url; + pis->product_id = product_id; + pis->http_status = http_status; + pis->description = description; + pis->description_i18n = json_pack ("{s:s}", "en", description); + pis->unit = unit; + pis->unit_precision_level = default_precision_from_unit (unit); + pis->unit_prices = GNUNET_new_array (unit_prices_len, + struct TALER_Amount); + pis->unit_prices_len = unit_prices_len; + pis->owns_unit_prices = true; + pis->use_unit_price_array = true; + for (size_t i = 0; i < unit_prices_len; i++) + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (unit_prices[i], + &pis->unit_prices[i])); + pis->price = pis->unit_prices[0]; + pis->image = GNUNET_strdup (""); + pis->taxes = json_array (); + pis->total_stock = 5; + pis->total_stock_frac = 0; + pis->unit_allow_fraction = true; + pis->unit_precision_level = 1; + pis->use_fractional = true; + pis->total_lost = 0; + pis->address = json_object (); + pis->next_restock = GNUNET_TIME_UNIT_FOREVER_TS; + patch_product_update_unit_total_stock (pis); + { + struct TALER_TESTING_Command cmd = { + .cls = pis, + .label = label, + .run = &patch_product_run, + .cleanup = &patch_product_cleanup, + .traits = &patch_product_traits + }; + + return cmd; + } +} + + /* end of testing_api_cmd_patch_product.c */ diff --git a/src/testing/testing_api_cmd_post_products.c b/src/testing/testing_api_cmd_post_products.c @@ -76,6 +76,26 @@ struct PostProductsState struct TALER_Amount price; /** + * Array of unit prices (may point to @e price for single-entry usage). + */ + struct TALER_Amount *unit_prices; + + /** + * Number of entries in @e unit_prices. + */ + size_t unit_prices_len; + + /** + * True if @e unit_prices must be freed. + */ + bool owns_unit_prices; + + /** + * True if we should send an explicit unit_price array. + */ + bool use_unit_price_array; + + /** * base64-encoded product image */ char *image; @@ -288,7 +308,9 @@ post_products_run (void *cls, struct PostProductsState *pis = cls; pis->is = is; - if (pis->use_fractional) + if (pis->use_fractional || + pis->use_unit_price_array || + (0 < pis->num_cats)) { pis->iph = TALER_MERCHANT_products_post4 ( TALER_TESTING_interpreter_get_context (is), @@ -297,8 +319,8 @@ post_products_run (void *cls, pis->description, pis->description_i18n, pis->unit, - &pis->price, - 1, + pis->unit_prices, + pis->unit_prices_len, pis->image, pis->taxes, pis->total_stock, @@ -317,50 +339,22 @@ post_products_run (void *cls, } else { - 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); - } + 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); } @@ -430,6 +424,8 @@ post_products_cleanup (void *cls, "POST /products operation did not complete\n"); TALER_MERCHANT_products_post_cancel (pis->iph); } + if (pis->owns_unit_prices) + GNUNET_free (pis->unit_prices); json_decref (pis->description_i18n); GNUNET_free (pis->image); json_decref (pis->taxes); @@ -473,6 +469,10 @@ TALER_TESTING_cmd_merchant_post_products2 ( GNUNET_assert (GNUNET_OK == TALER_string_to_amount (price, &pis->price)); + pis->unit_prices = &pis->price; + pis->unit_prices_len = 1; + pis->owns_unit_prices = false; + pis->use_unit_price_array = false; pis->image = GNUNET_strdup (image); pis->taxes = taxes; /* ownership taken */ pis->total_stock = total_stock; @@ -547,6 +547,65 @@ TALER_TESTING_cmd_merchant_post_products3 ( struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_products_with_unit_prices ( + const char *label, + const char *merchant_url, + const char *product_id, + const char *description, + const char *unit, + const char *const *unit_prices, + size_t unit_prices_len, + unsigned int http_status) +{ + struct PostProductsState *pis; + + GNUNET_assert (0 < unit_prices_len); + GNUNET_assert (NULL != unit_prices); + pis = GNUNET_new (struct PostProductsState); + pis->merchant_url = merchant_url; + pis->product_id = product_id; + pis->http_status = http_status; + pis->description = description; + pis->description_i18n = json_pack ("{s:s}", "en", description); + pis->unit = unit; + pis->unit_precision_level = default_precision_from_unit (unit); + pis->unit_prices = GNUNET_new_array (unit_prices_len, + struct TALER_Amount); + pis->unit_prices_len = unit_prices_len; + pis->owns_unit_prices = true; + pis->use_unit_price_array = true; + for (size_t i = 0; i < unit_prices_len; i++) + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (unit_prices[i], + &pis->unit_prices[i])); + pis->price = pis->unit_prices[0]; + pis->image = GNUNET_strdup (""); + pis->taxes = json_array (); + pis->total_stock = 4; + pis->total_stock_frac = 0; + pis->unit_allow_fraction = false; + pis->use_fractional = false; + pis->minimum_age = 0; + pis->address = json_pack ("{s:s}", "street", "my street"); + pis->next_restock = GNUNET_TIME_UNIT_ZERO_TS; + pis->num_cats = 0; + pis->cats = NULL; + post_products_update_unit_total_stock (pis); + { + struct TALER_TESTING_Command cmd = { + .cls = pis, + .label = label, + .run = &post_products_run, + .cleanup = &post_products_cleanup, + .traits = &post_products_traits + }; + + return cmd; + } +} + + +struct TALER_TESTING_Command TALER_TESTING_cmd_merchant_post_products ( const char *label, const char *merchant_url, diff --git a/src/testing/testing_api_cmd_post_using_templates.c b/src/testing/testing_api_cmd_post_using_templates.c @@ -381,6 +381,9 @@ post_using_templates_run (void *cls, &tis->otp_alg)) TALER_TESTING_FAIL (is); } + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &tis->nonce, + sizeof (struct GNUNET_CRYPTO_EddsaPublicKey)); if (NULL != tis->details) { tis->iph = TALER_MERCHANT_using_templates_post2 (