merchant

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

commit 1680844affef7b5ce63effb5f6cbf274c835dfae
parent 9f6610110475dfc0e6ef9bf35d0f3ca71b7f4dbf
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Thu, 29 Jan 2026 18:05:24 +0900

rename for consistency

Diffstat:
Msrc/backend/Makefile.am | 4++--
Msrc/backend/taler-merchant-httpd_dispatcher.c | 2+-
Asrc/backend/taler-merchant-httpd_post-templates-ID.c | 1782+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-templates-ID.h | 40++++++++++++++++++++++++++++++++++++++++
Dsrc/backend/taler-merchant-httpd_post-using-templates.c | 1782-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-using-templates.h | 40----------------------------------------
6 files changed, 1825 insertions(+), 1825 deletions(-)

diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -237,8 +237,8 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_post-orders-ID-paid.h \ taler-merchant-httpd_post-orders-ID-refund.c \ taler-merchant-httpd_post-orders-ID-refund.h \ - taler-merchant-httpd_post-using-templates.c \ - taler-merchant-httpd_post-using-templates.h \ + taler-merchant-httpd_post-templates-ID.c \ + taler-merchant-httpd_post-templates-ID.h \ taler-merchant-httpd_post-reports-ID.c \ taler-merchant-httpd_post-reports-ID.h \ taler-merchant-httpd_private-get-statistics-amount-SLUG.c \ diff --git a/src/backend/taler-merchant-httpd_dispatcher.c b/src/backend/taler-merchant-httpd_dispatcher.c @@ -98,7 +98,7 @@ #include "taler-merchant-httpd_post-orders-ID-claim.h" #include "taler-merchant-httpd_post-orders-ID-paid.h" #include "taler-merchant-httpd_post-orders-ID-pay.h" -#include "taler-merchant-httpd_post-using-templates.h" +#include "taler-merchant-httpd_post-templates-ID.h" #include "taler-merchant-httpd_post-orders-ID-refund.h" #include "taler-merchant-httpd_spa.h" #include "taler-merchant-httpd_statics.h" diff --git a/src/backend/taler-merchant-httpd_post-templates-ID.c b/src/backend/taler-merchant-httpd_post-templates-ID.c @@ -0,0 +1,1782 @@ +/* + This file is part of TALER + (C) 2022-2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ + +/** + * @file taler-merchant-httpd_post-using-templates.c + * @brief implementing POST /using-templates request handling + * @author Priscilla HUANG + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_post-templates-ID.h" +#include "taler-merchant-httpd_private-post-orders.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler_merchant_util.h" +#include <taler/taler_json_lib.h> +#include <regex.h> + + +/** + * Item selected from inventory_selection. + */ +struct InventoryTemplateItemContext +{ + /** + * Product ID as referenced in inventory. + */ + const char *product_id; + + /** + * Unit quantity string as provided by the client. + */ + const 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; + + /** + * Categories referenced by the product. + */ + uint64_t *categories; + + /** + * Length of @e categories. + */ + size_t num_categories; +}; + + +/** + * Our context. + */ +enum UsePhase +{ + /** + * Parse request payload into context fields. + */ + USE_PHASE_PARSE_REQUEST, + + /** + * Fetch template details from the database. + */ + USE_PHASE_LOOKUP_TEMPLATE, + + /** + * Parse template. + */ + USE_PHASE_PARSE_TEMPLATE, + + /** + * Load additional details (like products and + * categories) needed for verification and + * price computation. + */ + USE_PHASE_DB_FETCH, + + /** + * Validate request and template compatibility. + */ + USE_PHASE_VERIFY, + + /** + * Compute price of the order. + */ + USE_PHASE_COMPUTE_PRICE, + + /** + * Handle tip. + */ + USE_PHASE_CHECK_TIP, + + /** + * Check if client-supplied total amount matches + * our calculation (if we did any). + */ + USE_PHASE_CHECK_TOTAL, + + /** + * Construct the internal order request body. + */ + USE_PHASE_CREATE_ORDER, + + /** + * Submit the order to the shared order handler. + */ + USE_PHASE_SUBMIT_ORDER, + + /** + * Finished successfully with MHD_YES. + */ + USE_PHASE_FINISHED_MHD_YES, + + /** + * Finished with MHD_NO. + */ + USE_PHASE_FINISHED_MHD_NO +}; + +struct UseContext +{ + /** + * Context for our handler. + */ + struct TMH_HandlerContext *hc; + + /** + * Internal handler context we are passing into the + * POST /private/orders handler. + */ + struct TMH_HandlerContext ihc; + + /** + * Phase we are currently in. + */ + enum UsePhase phase; + + /** + * Template type from the contract. + */ + enum TALER_MERCHANT_TemplateType template_type; + + /** + * Information set in the #USE_PHASE_PARSE_REQUEST phase. + */ + struct + { + /** + * Summary override from request, if any. + */ + const char *summary; + + /** + * Amount provided by the client. + */ + struct TALER_Amount amount; + + /** + * Tip provided by the client. + */ + struct TALER_Amount tip; + + /** + * True if @e amount was not provided. + */ + bool no_amount; + + /** + * True if @e tip was not provided. + */ + bool no_tip; + + /** + * Parsed fields for inventory templates. + */ + struct + { + /** + * Selected products from inventory_selection. + */ + struct InventoryTemplateItemContext *items; + + /** + * Length of @e items. + */ + unsigned int items_len; + + } inventory; + + /** + * Request details if this is a paivana instantiation. + */ + struct + { + + /** + * Target website for the request. + */ + const char *website; + + /** + * Unique client identifier, consisting of + * current time, "-", and the hash of a nonce, + * the website and the current time. + */ + const char *paivana_id; + + } paivana; + + } parse_request; + + /** + * Information set in the #USE_PHASE_LOOKUP_TEMPLATE phase. + */ + struct + { + + /** + * Our template details from the DB. + */ + struct TALER_MERCHANTDB_TemplateDetails etp; + + } lookup_template; + + /** + * Information set in the #USE_PHASE_PARSE_TEMPLATE phase. + */ + struct TALER_MERCHANT_TemplateContract template_contract; + + /** + * Information set in the #USE_PHASE_COMPUTE_PRICE phase. + */ + struct + { + + /** + * Per-currency totals across selected products (without tips). + */ + struct TALER_Amount *totals; + + /** + * Length of @e totals. + */ + unsigned int totals_len; + + /** + * Array of payment choices, used with Paviana. + */ + json_t *choices; + + } compute_price; + +}; + + +/** + * Clean up inventory items. + * + * @param items_len length of @a items + * @param[in] items item array to free + */ +static void +cleanup_inventory_items (unsigned int items_len, + struct InventoryTemplateItemContext items[static + items_len]) +{ + for (unsigned int i = 0; i < items_len; i++) + { + struct InventoryTemplateItemContext *item = &items[i]; + + TALER_MERCHANTDB_product_details_free (&item->pd); + GNUNET_free (item->categories); + } + GNUNET_free (items); +} + + +/** + * Clean up a `struct UseContext *` + * + * @param[in] cls a `struct UseContext *` + */ +static void +cleanup_use_context (void *cls) +{ + struct UseContext *uc = cls; + + TALER_MERCHANTDB_template_details_free (&uc->lookup_template.etp); + if (NULL != + uc->parse_request.inventory.items) + cleanup_inventory_items (uc->parse_request.inventory.items_len, + uc->parse_request.inventory.items); + GNUNET_free (uc->compute_price.totals); + uc->compute_price.totals_len = 0; + json_decref (uc->compute_price.choices); + if (NULL != uc->ihc.cc) + uc->ihc.cc (uc->ihc.ctx); + GNUNET_free (uc->ihc.infix); + json_decref (uc->ihc.request_body); + GNUNET_free (uc); +} + + +/** + * Finalize a template use request. + * + * @param[in,out] uc use context + * @param ret handler return value + */ +static void +use_finalize (struct UseContext *uc, + MHD_RESULT ret) +{ + uc->phase = (MHD_YES == ret) + ? USE_PHASE_FINISHED_MHD_YES + : USE_PHASE_FINISHED_MHD_NO; +} + + +/** + * Finalize after JSON parsing result. + * + * @param[in,out] uc use context + * @param res parse result + */ +static void +use_finalize_parse (struct UseContext *uc, + enum GNUNET_GenericReturnValue res) +{ + GNUNET_assert (GNUNET_OK != res); + use_finalize (uc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); +} + + +/** + * Reply with error and finalize the request. + * + * @param[in,out] uc use context + * @param http_status HTTP status code + * @param ec error code + * @param detail error detail + */ +static void +use_reply_with_error (struct UseContext *uc, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *detail) +{ + MHD_RESULT mret; + + mret = TALER_MHD_reply_with_error (uc->hc->connection, + http_status, + ec, + detail); + use_finalize (uc, + mret); +} + + +/* ***************** USE_PHASE_PARSE_REQUEST **************** */ + +/** + * Parse request data for inventory templates. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_using_templates_inventory_request ( + struct UseContext *uc) +{ + const json_t *inventory_selection; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("inventory_selection", + &inventory_selection), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + GNUNET_assert (NULL == uc->ihc.request_body); + res = TALER_MHD_parse_json_data (uc->hc->connection, + uc->hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + use_finalize_parse (uc, + res); + return GNUNET_SYSERR; + } + + if ( (! uc->parse_request.no_amount) && + (! TMH_test_exchange_configured_for_currency ( + uc->parse_request.amount.currency)) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + "Currency is not supported by backend"); + return GNUNET_SYSERR; + } + + for (size_t i = 0; i < json_array_size (inventory_selection); i++) + { + struct InventoryTemplateItemContext item = { 0 }; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("product_id", + &item.product_id), + GNUNET_JSON_spec_string ("quantity", + &item.unit_quantity), + GNUNET_JSON_spec_end () + }; + const char *err_name; + unsigned int err_line; + + res = GNUNET_JSON_parse (json_array_get (inventory_selection, + i), + ispec, + &err_name, + &err_line); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_selection"); + return GNUNET_SYSERR; + } + + GNUNET_array_append (uc->parse_request.inventory.items, + uc->parse_request.inventory.items_len, + item); + } + return GNUNET_OK; +} + + +/** + * Parse request data for paivana templates. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_using_templates_paivana_request ( + struct UseContext *uc) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("website", + &uc->parse_request.paivana.website), + GNUNET_JSON_spec_string ("paivana_id", + &uc->parse_request.paivana.paivana_id), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + struct GNUNET_HashCode sh; + unsigned long long tv; + const char *dash; + + GNUNET_assert (NULL == uc->ihc.request_body); + res = TALER_MHD_parse_json_data (uc->hc->connection, + uc->hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + use_finalize_parse (uc, + res); + return GNUNET_SYSERR; + } + if (1 != + sscanf (uc->parse_request.paivana.paivana_id, + "%llu-", + &tv)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "paivana_id"); + return GNUNET_SYSERR; + } + dash = strchr (uc->parse_request.paivana.paivana_id, + '-'); + GNUNET_assert (NULL != dash); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (dash + 1, + strlen (dash + 1), + &sh, + sizeof (sh))) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "paivana_id"); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Main function for the #USE_PHASE_PARSE_REQUEST. + * + * @param[in,out] uc context to update + */ +static void +handle_phase_parse_request ( + struct UseContext *uc) +{ + const char *template_type = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("template_type", + &template_type), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("tip", + &uc->parse_request.tip), + &uc->parse_request.no_tip), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("summary", + &uc->parse_request.summary), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("amount", + &uc->parse_request.amount), + &uc->parse_request.no_amount), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (uc->hc->connection, + uc->hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + use_finalize_parse (uc, + res); + return; + } + if (NULL == template_type) + template_type = "fixed-order"; + uc->template_type + = TALER_MERCHANT_template_type_from_string ( + template_type); + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + /* nothig left to do */ + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + res = parse_using_templates_paivana_request (uc); + if (GNUNET_OK == res) + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + res = parse_using_templates_inventory_request (uc); + if (GNUNET_OK == res) + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + break; + } + GNUNET_break (0); + use_reply_with_error ( + uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "template_type"); +} + + +/* ***************** USE_PHASE_LOOKUP_TEMPLATE **************** */ + +/** + * Main function for the #USE_PHASE_LOOKUP_TEMPLATE. + * + * @param[in,out] uc context to update + */ +static void +handle_phase_lookup_template ( + struct UseContext *uc) +{ + struct TMH_MerchantInstance *mi = uc->hc->instance; + const char *template_id = uc->hc->infix; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_template (TMH_db->cls, + mi->settings.id, + template_id, + &uc->lookup_template.etp); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + /* Clean up and fail hard */ + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_template"); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* this should be impossible (single select) */ + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_template"); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* template not found! */ + use_reply_with_error (uc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, + template_id); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* all good */ + break; + } + if (uc->template_type != + TALER_MERCHANT_template_type_from_contract ( + uc->lookup_template.etp.template_contract)) + { + GNUNET_break_op (0); + use_reply_with_error ( + uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_WRONG_TYPE, + "template_contract has different type"); + return; + } + uc->phase++; +} + + +/* ***************** USE_PHASE_PARSE_TEMPLATE **************** */ + + +/** + * Parse template. + * + * @param[in,out] uc use context + */ +static void +handle_phase_template_contract (struct UseContext *uc) +{ + const char *err_name; + enum GNUNET_GenericReturnValue res; + + res = TALER_MERCHANT_template_contract_parse ( + uc->lookup_template.etp.template_contract, + &uc->template_contract, + &err_name); + if (GNUNET_OK != res) + { + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + err_name); + return; + } + uc->phase++; +} + + +/* ***************** USE_PHASE_DB_FETCH **************** */ + +/** + * Fetch DB data for inventory templates. + * + * @param[in,out] uc use context + */ +static void +handle_phase_db_fetch (struct UseContext *uc) +{ + struct TMH_MerchantInstance *mi = uc->hc->instance; + + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + GNUNET_assert (0); + } + + for (unsigned int i = 0; + i < uc->parse_request.inventory.items_len; + i++) + { + struct InventoryTemplateItemContext *item = + &uc->parse_request.inventory.items[i]; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_product (TMH_db->cls, + mi->settings.id, + item->product_id, + &item->pd, + &item->num_categories, + &item->categories); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_product"); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_product"); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + use_reply_with_error (uc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + item->product_id); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + uc->phase++; +} + + +/* *************** Helpers for USE_PHASE_VERIFY ***************** */ + +/** + * Check if the given product ID appears in the array of allowed_products. + * + * @param allowed_products JSON array of product IDs allowed by the template, may be NULL + * @param product_id product ID to check + * @return true if the product ID is in the list + */ +static bool +product_id_allowed (const json_t *allowed_products, + const char *product_id) +{ + const json_t *entry; + size_t idx; + + if (NULL == allowed_products) + return false; + json_array_foreach ((json_t *) allowed_products, idx, entry) + { + if (! json_is_string (entry)) + { + GNUNET_break (0); + 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 allowed_categories JSON array of categories allowed by the template, may be NULL + * @param num_categories length of @a categories + * @param categories list of categories of the selected product + * @return true if any category of the product is in the list of allowed categories matches + */ +static bool +category_allowed (const json_t *allowed_categories, + size_t num_categories, + const uint64_t categories[num_categories]) +{ + const json_t *entry; + size_t idx; + + if (NULL == allowed_categories) + return false; + json_array_foreach ((json_t *) allowed_categories, + idx, + entry) + { + uint64_t selected_id; + + if (! json_is_integer (entry)) + { + GNUNET_break (0); + continue; + } + if (0 > json_integer_value (entry)) + { + GNUNET_break (0); + 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; +} + + +/** + * Verify request data for inventory templates. + * Checks that the selected products are allowed + * for this template. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +verify_using_templates_inventory (struct UseContext *uc) +{ + if (uc->template_contract.details.inventory.choose_one && + (1 != uc->parse_request.inventory.items_len)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_selection"); + return GNUNET_SYSERR; + } + if (uc->template_contract.details.inventory.selected_all) + return GNUNET_OK; + for (unsigned int i = 0; + i < uc->parse_request.inventory.items_len; + i++) + { + struct InventoryTemplateItemContext *item = + &uc->parse_request.inventory.items[i]; + const char *eparam = NULL; + + if (GNUNET_OK != + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_QUANTITY, + item->pd.allow_fractional_quantity, + true, + 0, + false, + item->unit_quantity, + &item->quantity_value, + &item->quantity_frac, + &eparam)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + eparam); + return GNUNET_SYSERR; + } + + if (0 == item->pd.price_array_length) + { + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "price_array"); + return GNUNET_SYSERR; + } + } + + for (unsigned int i = 0; + i < uc->parse_request.inventory.items_len; + i++) + { + struct InventoryTemplateItemContext *item = + &uc->parse_request.inventory.items[i]; + + if (product_id_allowed (uc->template_contract.details.inventory. + selected_products, + item->product_id)) + continue; + if (category_allowed ( + uc->template_contract.details.inventory.selected_categories, + item->num_categories, + item->categories)) + continue; + GNUNET_break_op (0); + use_reply_with_error ( + uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_WRONG_PRODUCT, + item->product_id); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Verify request data for fixed-order templates. + * As here we cannot compute the total amount, either + * the template or the client request must provide it. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +verify_using_templates_fixed ( + struct UseContext *uc) +{ + if ( (! uc->parse_request.no_amount) && + (! uc->template_contract.no_amount) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + NULL); + return GNUNET_SYSERR; + } + if (uc->parse_request.no_amount && + uc->template_contract.no_amount) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT, + NULL); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Verify request data for paivana templates. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +verify_using_templates_paivana ( + struct UseContext *uc) +{ + if (NULL != uc->template_contract.details.paivana.website_regex) + { + regex_t ex; + bool allowed = false; + + if (0 != regcomp (&ex, + uc->template_contract.details.paivana.website_regex, + REG_NOSUB | REG_EXTENDED)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 == + regexec (&ex, + uc->parse_request.paivana.website, + 0, NULL, + 0)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Website `%s' allowed by template\n", + uc->parse_request.paivana.website); + allowed = true; + } + regfree (&ex); + if (! allowed) + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Verify that the client request is structurally acceptable for the specified + * template. Does NOT check the total amount being reasonable. + * + * @param[in,out] uc use context + */ +static void +handle_phase_verify ( + struct UseContext *uc) +{ + enum GNUNET_GenericReturnValue res = GNUNET_SYSERR; + + if ( (NULL != uc->parse_request.summary) && + (NULL != uc->template_contract.summary) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT, + NULL); + return; + } + if ( (NULL == uc->parse_request.summary) && + (NULL == uc->template_contract.summary) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY, + NULL); + return; + } + if ( (! uc->parse_request.no_amount) && + (NULL != uc->template_contract.currency) && + (0 != strcasecmp (uc->template_contract.currency, + uc->parse_request.amount.currency)) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + uc->template_contract.currency); + return; + } + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + res = verify_using_templates_fixed (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + res = verify_using_templates_paivana (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + res = verify_using_templates_inventory (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + GNUNET_assert (0); + } + if (GNUNET_OK == res) + uc->phase++; +} + + +/* ***************** USE_PHASE_COMPUTE_PRICE **************** */ + + +/** + * 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..TALER_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; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (unit_price->currency, + line_total)); + if ( (0 != quantity) && + (0 > + TALER_amount_multiply (line_total, + unit_price, + (uint32_t) quantity)) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 == quantity_frac) + return GNUNET_OK; + if (0 > + TALER_amount_multiply (&tmp, + unit_price, + quantity_frac)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + TALER_amount_divide (&tmp, + &tmp, + TALER_MERCHANT_UNIT_FRAC_BASE); + if (0 > + TALER_amount_add (line_total, + line_total, + &tmp)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Find the price of the given @a item in the specified + * @a currency. + * + * @param currency currency to search price in + * @param item item to check prices of + * @return NULL if a suitable price was not found + */ +static const struct TALER_Amount * +find_item_price_in_currency ( + const char *currency, + const struct InventoryTemplateItemContext *item) +{ + for (size_t j = 0; j < item->pd.price_array_length; j++) + { + if (0 == strcasecmp (item->pd.price_array[j].currency, + currency)) + return &item->pd.price_array[j]; + } + return NULL; +} + + +/** + * Compute totals for all currencies shared across selected products. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success (including no price due to no items) + * #GNUNET_NO if we could not find a price in any accepted currency + * for all selected products + * #GNUNET_SYSERR on arithmetic issues (internal error) + */ +static enum GNUNET_GenericReturnValue +compute_totals_per_currency (struct UseContext *uc) +{ + const struct InventoryTemplateItemContext *items + = uc->parse_request.inventory.items; + unsigned int items_len = uc->parse_request.inventory.items_len; + + if (0 == items_len) + return GNUNET_OK; + for (size_t i = 0; i < items[0].pd.price_array_length; i++) + { + const struct TALER_Amount *price + = &items[0].pd.price_array[i]; + struct TALER_Amount zero; + + if (! TMH_test_exchange_configured_for_currency (price->currency)) + continue; + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (price->currency, + &zero)); + GNUNET_array_append (uc->compute_price.totals, + uc->compute_price.totals_len, + zero); + } + if (0 == uc->compute_price.totals_len) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No currency supported by our configuration in which we have prices for first selected product!\n"); + return GNUNET_NO; + } + /* Loop through items, ensure each currency exists and sum totals. */ + for (unsigned int i = 0; i < items_len; i++) + { + const struct InventoryTemplateItemContext *item = &items[i]; + unsigned int c = 0; + + while (c < uc->compute_price.totals_len) + { + struct TALER_Amount *total = &uc->compute_price.totals[c]; + const struct TALER_Amount *unit_price; + struct TALER_Amount line_total; + + unit_price = find_item_price_in_currency (total->currency, + item); + if (NULL == unit_price) + { + /* Drop the currency: we have no price in one of + the selected products */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Product `%s' has no price in %s: dropping currency\n", + item->product_id, + total->currency); + *total = uc->compute_price.totals[--uc->compute_price.totals_len]; + continue; + } + if (GNUNET_OK != + compute_line_total (unit_price, + item->quantity_value, + item->quantity_frac, + &line_total)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_add (total, + total, + &line_total)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + c++; + } + } + if (0 == uc->compute_price.totals_len) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No currency available in which we have prices for all selected products!\n"); + GNUNET_free (uc->compute_price.totals); + } + return (0 == uc->compute_price.totals_len) + ? GNUNET_NO + : GNUNET_OK; +} + + +/** + * Compute total for only the given @a currency. + * + * @param items_len length of @a items + * @param items inventory items + * @param currency currency to total + * @param[out] total computed total + * @return #GNUNET_OK on success + * #GNUNET_NO if we could not find a price in any accepted currency + * for all selected products + * #GNUNET_SYSERR on arithmetic issues (internal error) + */ +static enum GNUNET_GenericReturnValue +compute_inventory_total (unsigned int items_len, + const struct InventoryTemplateItemContext *items, + const char *currency, + struct TALER_Amount *total) +{ + GNUNET_assert (NULL != currency); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (currency, + total)); + for (unsigned int i = 0; i < items_len; i++) + { + const struct InventoryTemplateItemContext *item = &items[i]; + const struct TALER_Amount *unit_price; + struct TALER_Amount line_total; + + unit_price = find_item_price_in_currency (currency, + item); + if (NULL == unit_price) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "compute_inventory_total: no price in %s for product `%s'\n", + currency, + item->product_id); + return GNUNET_NO; + } + if (GNUNET_OK != + compute_line_total (unit_price, + item->quantity_value, + item->quantity_frac, + &line_total)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "compute_inventory_total: line total failed for %s in %s\n", + item->product_id, + currency); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_add (total, + total, + &line_total)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Compute total price. + * + * @param[in,out] uc use context + */ +static void +handle_phase_compute_price (struct UseContext *uc) +{ + const char *primary_currency; + enum GNUNET_GenericReturnValue ret; + + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + uc->compute_price.totals + = GNUNET_new (struct TALER_Amount); + uc->compute_price.totals_len + = 1; + if (uc->parse_request.no_amount) + { + GNUNET_assert (! uc->template_contract.no_amount); + *uc->compute_price.totals + = uc->template_contract.amount; + } + else + { + GNUNET_assert (uc->template_contract.no_amount); + *uc->compute_price.totals + = uc->parse_request.amount; + } + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + /* handled below */ + break; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + { + const struct TALER_MERCHANT_TemplateContractPaivana *tcp + = &uc->template_contract.details.paivana; + json_t *choices; + + choices = json_array (); + GNUNET_assert (NULL != choices); + for (size_t i = 0; i < tcp->choices_len; i++) + { + /* Make deep copy, we're going to MODIFY it! */ + struct TALER_MERCHANT_ContractChoice choice + = tcp->choices[i]; + + choice.no_tip = uc->parse_request.no_tip; + if (! uc->parse_request.no_tip) + { + if (GNUNET_YES != + TALER_amount_cmp_currency (&choice.amount, + &uc->parse_request.tip)) + continue; /* tip does not match choice currency */ + choice.tip = uc->parse_request.tip; + if (0 > + TALER_amount_add (&choice.amount, + &choice.amount, + &uc->parse_request.tip)) + { + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "tip"); + return; + } + } + GNUNET_assert (0 == + json_array_append_new ( + choices, + TALER_MERCHANT_json_from_contract_choice (&choice, + true))); + } + if (0 == json_array_size (choices)) + { + GNUNET_break_op (0); + json_decref (choices); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY, + "tip"); + return; + } + uc->compute_price.choices = choices; + } + /* Note: we already did the tip and pricing + fully here, so we skip these phases. */ + uc->phase = USE_PHASE_CREATE_ORDER; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + GNUNET_assert (0); + } + primary_currency = uc->template_contract.currency; + if (! uc->parse_request.no_amount) + primary_currency = uc->parse_request.amount.currency; + if (! uc->parse_request.no_tip) + primary_currency = uc->parse_request.tip.currency; + if (NULL == primary_currency) + { + ret = compute_totals_per_currency (uc); + } + else + { + uc->compute_price.totals + = GNUNET_new (struct TALER_Amount); + uc->compute_price.totals_len + = 1; + ret = compute_inventory_total ( + uc->parse_request.inventory.items_len, + uc->parse_request.inventory.items, + primary_currency, + uc->compute_price.totals); + } + if (GNUNET_SYSERR == ret) + { + use_reply_with_error ( + uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "calculation of currency totals failed"); + return; + } + if (GNUNET_NO == ret) + { + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY, + NULL); + return; + } + + uc->phase++; +} + + +/* ***************** USE_PHASE_CHECK_TIP **************** */ + + +/** + * Check that tip specified is reasonable and add to total. + * + * @param[in,out] uc use context + */ +static void +handle_phase_check_tip (struct UseContext *uc) +{ + struct TALER_Amount *total_amount; + + if (uc->parse_request.no_tip) + { + uc->phase++; + return; + } + if (0 == uc->compute_price.totals_len) + { + if (! TMH_test_exchange_configured_for_currency ( + uc->parse_request.tip.currency)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + "Tip currency is not supported by backend"); + return; + } + uc->compute_price.totals + = GNUNET_new (struct TALER_Amount); + uc->compute_price.totals_len + = 1; + *uc->compute_price.totals + = uc->parse_request.tip; + uc->phase++; + return; + } + GNUNET_assert (1 == uc->compute_price.totals_len); + total_amount = &uc->compute_price.totals[0]; + if (GNUNET_YES != + TALER_amount_cmp_currency (&uc->parse_request.tip, + total_amount)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + uc->parse_request.tip.currency); + return; + } + if (0 > + TALER_amount_add (total_amount, + total_amount, + &uc->parse_request.tip)) + { + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "tip"); + return; + } + uc->phase++; +} + + +/* ***************** USE_PHASE_CHECK_TOTAL **************** */ + +/** + * Check that if the client specified a total, + * it matches our own calculation. + * + * @param[in,out] uc use context + */ +static void +handle_phase_check_total (struct UseContext *uc) +{ + GNUNET_assert (1 <= uc->compute_price.totals_len); + if (! uc->parse_request.no_amount) + { + GNUNET_assert (1 == uc->compute_price.totals_len); + GNUNET_assert (GNUNET_YES == + TALER_amount_cmp_currency (&uc->parse_request.amount, + &uc->compute_price.totals[0])); + if (0 != + TALER_amount_cmp (&uc->parse_request.amount, + &uc->compute_price.totals[0])) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + TALER_amount2s (&uc->compute_price.totals[0])); + return; + } + } + uc->phase++; +} + + +/* ***************** USE_PHASE_CREATE_ORDER **************** */ + + +/** + * Create order request for inventory templates. + * + * @param[in,out] uc use context + */ +static void +create_using_templates_inventory (struct UseContext *uc) +{ + json_t *inventory_products; + json_t *choices; + + inventory_products = json_array (); + GNUNET_assert (NULL != inventory_products); + for (unsigned int i = 0; + i < uc->parse_request.inventory.items_len; + i++) + { + const struct InventoryTemplateItemContext *item = + &uc->parse_request.inventory.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)))); + } + choices = json_array (); + GNUNET_assert (NULL != choices); + for (unsigned int i = 0; + i < uc->compute_price.totals_len; + i++) + { + GNUNET_assert (0 == + json_array_append_new ( + choices, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &uc->compute_price.totals[i]), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("tip", + uc->parse_request.no_tip + ? NULL + : &uc->parse_request.tip)) + ))); + } + + uc->ihc.request_body + = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("otp_id", + uc->lookup_template.etp.otp_id)), + GNUNET_JSON_pack_array_steal ("inventory_products", + inventory_products), + GNUNET_JSON_pack_object_steal ( + "order", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("version", + 1), + GNUNET_JSON_pack_array_steal ("choices", + choices), + GNUNET_JSON_pack_string ("summary", + NULL == uc->parse_request.summary + ? uc->template_contract.summary + : uc->parse_request.summary)))); +} + + +/** + * Create order request for fixed-order templates. + * + * @param[in,out] uc use context + */ +static void +create_using_templates_fixed (struct UseContext *uc) +{ + uc->ihc.request_body + = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("otp_id", + uc->lookup_template.etp.otp_id)), + GNUNET_JSON_pack_object_steal ( + "order", + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ( + "amount", + &uc->compute_price.totals[0]), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("tip", + uc->parse_request.no_tip + ? NULL + : &uc->parse_request.tip)), + GNUNET_JSON_pack_string ( + "summary", + NULL == uc->parse_request.summary + ? uc->template_contract.summary + : uc->parse_request.summary)))); +} + + +/** + * Create order request for paivana templates. + * + * @param[in,out] uc use context + */ +static void +create_using_templates_paivana (struct UseContext *uc) +{ + uc->ihc.request_body + = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ( + "session_id", + uc->parse_request.paivana.paivana_id), + GNUNET_JSON_pack_object_steal ( + "order", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("version", + 1), + GNUNET_JSON_pack_array_incref ("choices", + uc->compute_price.choices), + GNUNET_JSON_pack_string ( + "summary", + NULL == uc->parse_request.summary + ? uc->template_contract.summary + : uc->parse_request.summary), + GNUNET_JSON_pack_string ("fulfillment_url", + uc->parse_request.paivana.website)))); +} + + +static void +handle_phase_create_order (struct UseContext *uc) +{ + GNUNET_assert (NULL == uc->ihc.request_body); + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + create_using_templates_fixed (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + create_using_templates_paivana (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + create_using_templates_inventory (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + GNUNET_assert (0); + } + uc->phase++; +} + + +/* ***************** Main handler **************** */ + +MHD_RESULT +TMH_post_using_templates_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct UseContext *uc = hc->ctx; + + (void) rh; + if (NULL == uc) + { + uc = GNUNET_new (struct UseContext); + uc->hc = hc; + hc->ctx = uc; + hc->cc = &cleanup_use_context; + uc->ihc.instance = hc->instance; + uc->phase = USE_PHASE_PARSE_REQUEST; + uc->template_type = TALER_MERCHANT_TEMPLATE_TYPE_INVALID; + } + + while (1) + { + switch (uc->phase) + { + case USE_PHASE_PARSE_REQUEST: + handle_phase_parse_request (uc); + break; + case USE_PHASE_LOOKUP_TEMPLATE: + handle_phase_lookup_template (uc); + break; + case USE_PHASE_PARSE_TEMPLATE: + handle_phase_template_contract (uc); + break; + case USE_PHASE_DB_FETCH: + handle_phase_db_fetch (uc); + break; + case USE_PHASE_VERIFY: + handle_phase_verify (uc); + break; + case USE_PHASE_COMPUTE_PRICE: + handle_phase_compute_price (uc); + break; + case USE_PHASE_CHECK_TIP: + handle_phase_check_tip (uc); + break; + case USE_PHASE_CHECK_TOTAL: + handle_phase_check_total (uc); + break; + case USE_PHASE_CREATE_ORDER: + handle_phase_create_order (uc); + break; + case USE_PHASE_SUBMIT_ORDER: + return TMH_private_post_orders ( + NULL, /* not even used */ + connection, + &uc->ihc); + case USE_PHASE_FINISHED_MHD_YES: + return MHD_YES; + case USE_PHASE_FINISHED_MHD_NO: + return MHD_NO; + } + } +} diff --git a/src/backend/taler-merchant-httpd_post-templates-ID.h b/src/backend/taler-merchant-httpd_post-templates-ID.h @@ -0,0 +1,40 @@ +/* + This file is part of TALER + (C) 2022 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 taler-merchant-httpd_post-using-templates.h + * @brief headers for POST /using-templates handler + * @author Priscilla Huang + */ +#ifndef TALER_MERCHANT_HTTPD_POST_TEMPLATES_ID_H +#define TALER_MERCHANT_HTTPD_POST_TEMPLATES_ID_H + +#include "taler-merchant-httpd.h" + +/** + * Generate a template that customer can use it. Returns an MHD_RESULT. + * + * @param rh details about this request handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_post-using-templates.c b/src/backend/taler-merchant-httpd_post-using-templates.c @@ -1,1782 +0,0 @@ -/* - This file is part of TALER - (C) 2022-2026 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation; either version 3, - or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public - License along with TALER; see the file COPYING. If not, - see <http://www.gnu.org/licenses/> -*/ - -/** - * @file taler-merchant-httpd_post-using-templates.c - * @brief implementing POST /using-templates request handling - * @author Priscilla HUANG - * @author Christian Grothoff - */ -#include "platform.h" -#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_merchant_util.h" -#include <taler/taler_json_lib.h> -#include <regex.h> - - -/** - * Item selected from inventory_selection. - */ -struct InventoryTemplateItemContext -{ - /** - * Product ID as referenced in inventory. - */ - const char *product_id; - - /** - * Unit quantity string as provided by the client. - */ - const 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; - - /** - * Categories referenced by the product. - */ - uint64_t *categories; - - /** - * Length of @e categories. - */ - size_t num_categories; -}; - - -/** - * Our context. - */ -enum UsePhase -{ - /** - * Parse request payload into context fields. - */ - USE_PHASE_PARSE_REQUEST, - - /** - * Fetch template details from the database. - */ - USE_PHASE_LOOKUP_TEMPLATE, - - /** - * Parse template. - */ - USE_PHASE_PARSE_TEMPLATE, - - /** - * Load additional details (like products and - * categories) needed for verification and - * price computation. - */ - USE_PHASE_DB_FETCH, - - /** - * Validate request and template compatibility. - */ - USE_PHASE_VERIFY, - - /** - * Compute price of the order. - */ - USE_PHASE_COMPUTE_PRICE, - - /** - * Handle tip. - */ - USE_PHASE_CHECK_TIP, - - /** - * Check if client-supplied total amount matches - * our calculation (if we did any). - */ - USE_PHASE_CHECK_TOTAL, - - /** - * Construct the internal order request body. - */ - USE_PHASE_CREATE_ORDER, - - /** - * Submit the order to the shared order handler. - */ - USE_PHASE_SUBMIT_ORDER, - - /** - * Finished successfully with MHD_YES. - */ - USE_PHASE_FINISHED_MHD_YES, - - /** - * Finished with MHD_NO. - */ - USE_PHASE_FINISHED_MHD_NO -}; - -struct UseContext -{ - /** - * Context for our handler. - */ - struct TMH_HandlerContext *hc; - - /** - * Internal handler context we are passing into the - * POST /private/orders handler. - */ - struct TMH_HandlerContext ihc; - - /** - * Phase we are currently in. - */ - enum UsePhase phase; - - /** - * Template type from the contract. - */ - enum TALER_MERCHANT_TemplateType template_type; - - /** - * Information set in the #USE_PHASE_PARSE_REQUEST phase. - */ - struct - { - /** - * Summary override from request, if any. - */ - const char *summary; - - /** - * Amount provided by the client. - */ - struct TALER_Amount amount; - - /** - * Tip provided by the client. - */ - struct TALER_Amount tip; - - /** - * True if @e amount was not provided. - */ - bool no_amount; - - /** - * True if @e tip was not provided. - */ - bool no_tip; - - /** - * Parsed fields for inventory templates. - */ - struct - { - /** - * Selected products from inventory_selection. - */ - struct InventoryTemplateItemContext *items; - - /** - * Length of @e items. - */ - unsigned int items_len; - - } inventory; - - /** - * Request details if this is a paivana instantiation. - */ - struct - { - - /** - * Target website for the request. - */ - const char *website; - - /** - * Unique client identifier, consisting of - * current time, "-", and the hash of a nonce, - * the website and the current time. - */ - const char *paivana_id; - - } paivana; - - } parse_request; - - /** - * Information set in the #USE_PHASE_LOOKUP_TEMPLATE phase. - */ - struct - { - - /** - * Our template details from the DB. - */ - struct TALER_MERCHANTDB_TemplateDetails etp; - - } lookup_template; - - /** - * Information set in the #USE_PHASE_PARSE_TEMPLATE phase. - */ - struct TALER_MERCHANT_TemplateContract template_contract; - - /** - * Information set in the #USE_PHASE_COMPUTE_PRICE phase. - */ - struct - { - - /** - * Per-currency totals across selected products (without tips). - */ - struct TALER_Amount *totals; - - /** - * Length of @e totals. - */ - unsigned int totals_len; - - /** - * Array of payment choices, used with Paviana. - */ - json_t *choices; - - } compute_price; - -}; - - -/** - * Clean up inventory items. - * - * @param items_len length of @a items - * @param[in] items item array to free - */ -static void -cleanup_inventory_items (unsigned int items_len, - struct InventoryTemplateItemContext items[static - items_len]) -{ - for (unsigned int i = 0; i < items_len; i++) - { - struct InventoryTemplateItemContext *item = &items[i]; - - TALER_MERCHANTDB_product_details_free (&item->pd); - GNUNET_free (item->categories); - } - GNUNET_free (items); -} - - -/** - * Clean up a `struct UseContext *` - * - * @param[in] cls a `struct UseContext *` - */ -static void -cleanup_use_context (void *cls) -{ - struct UseContext *uc = cls; - - TALER_MERCHANTDB_template_details_free (&uc->lookup_template.etp); - if (NULL != - uc->parse_request.inventory.items) - cleanup_inventory_items (uc->parse_request.inventory.items_len, - uc->parse_request.inventory.items); - GNUNET_free (uc->compute_price.totals); - uc->compute_price.totals_len = 0; - json_decref (uc->compute_price.choices); - if (NULL != uc->ihc.cc) - uc->ihc.cc (uc->ihc.ctx); - GNUNET_free (uc->ihc.infix); - json_decref (uc->ihc.request_body); - GNUNET_free (uc); -} - - -/** - * Finalize a template use request. - * - * @param[in,out] uc use context - * @param ret handler return value - */ -static void -use_finalize (struct UseContext *uc, - MHD_RESULT ret) -{ - uc->phase = (MHD_YES == ret) - ? USE_PHASE_FINISHED_MHD_YES - : USE_PHASE_FINISHED_MHD_NO; -} - - -/** - * Finalize after JSON parsing result. - * - * @param[in,out] uc use context - * @param res parse result - */ -static void -use_finalize_parse (struct UseContext *uc, - enum GNUNET_GenericReturnValue res) -{ - GNUNET_assert (GNUNET_OK != res); - use_finalize (uc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); -} - - -/** - * Reply with error and finalize the request. - * - * @param[in,out] uc use context - * @param http_status HTTP status code - * @param ec error code - * @param detail error detail - */ -static void -use_reply_with_error (struct UseContext *uc, - unsigned int http_status, - enum TALER_ErrorCode ec, - const char *detail) -{ - MHD_RESULT mret; - - mret = TALER_MHD_reply_with_error (uc->hc->connection, - http_status, - ec, - detail); - use_finalize (uc, - mret); -} - - -/* ***************** USE_PHASE_PARSE_REQUEST **************** */ - -/** - * Parse request data for inventory templates. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -parse_using_templates_inventory_request ( - struct UseContext *uc) -{ - const json_t *inventory_selection; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("inventory_selection", - &inventory_selection), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - GNUNET_assert (NULL == uc->ihc.request_body); - res = TALER_MHD_parse_json_data (uc->hc->connection, - uc->hc->request_body, - spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - use_finalize_parse (uc, - res); - return GNUNET_SYSERR; - } - - if ( (! uc->parse_request.no_amount) && - (! TMH_test_exchange_configured_for_currency ( - uc->parse_request.amount.currency)) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - "Currency is not supported by backend"); - return GNUNET_SYSERR; - } - - for (size_t i = 0; i < json_array_size (inventory_selection); i++) - { - struct InventoryTemplateItemContext item = { 0 }; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("product_id", - &item.product_id), - GNUNET_JSON_spec_string ("quantity", - &item.unit_quantity), - GNUNET_JSON_spec_end () - }; - const char *err_name; - unsigned int err_line; - - res = GNUNET_JSON_parse (json_array_get (inventory_selection, - i), - ispec, - &err_name, - &err_line); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inventory_selection"); - return GNUNET_SYSERR; - } - - GNUNET_array_append (uc->parse_request.inventory.items, - uc->parse_request.inventory.items_len, - item); - } - return GNUNET_OK; -} - - -/** - * Parse request data for paivana templates. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -parse_using_templates_paivana_request ( - struct UseContext *uc) -{ - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("website", - &uc->parse_request.paivana.website), - GNUNET_JSON_spec_string ("paivana_id", - &uc->parse_request.paivana.paivana_id), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - struct GNUNET_HashCode sh; - unsigned long long tv; - const char *dash; - - GNUNET_assert (NULL == uc->ihc.request_body); - res = TALER_MHD_parse_json_data (uc->hc->connection, - uc->hc->request_body, - spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - use_finalize_parse (uc, - res); - return GNUNET_SYSERR; - } - if (1 != - sscanf (uc->parse_request.paivana.paivana_id, - "%llu-", - &tv)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "paivana_id"); - return GNUNET_SYSERR; - } - dash = strchr (uc->parse_request.paivana.paivana_id, - '-'); - GNUNET_assert (NULL != dash); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (dash + 1, - strlen (dash + 1), - &sh, - sizeof (sh))) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "paivana_id"); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Main function for the #USE_PHASE_PARSE_REQUEST. - * - * @param[in,out] uc context to update - */ -static void -handle_phase_parse_request ( - struct UseContext *uc) -{ - const char *template_type = NULL; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("template_type", - &template_type), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("tip", - &uc->parse_request.tip), - &uc->parse_request.no_tip), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("summary", - &uc->parse_request.summary), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("amount", - &uc->parse_request.amount), - &uc->parse_request.no_amount), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (uc->hc->connection, - uc->hc->request_body, - spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - use_finalize_parse (uc, - res); - return; - } - if (NULL == template_type) - template_type = "fixed-order"; - uc->template_type - = TALER_MERCHANT_template_type_from_string ( - template_type); - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - /* nothig left to do */ - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - res = parse_using_templates_paivana_request (uc); - if (GNUNET_OK == res) - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - res = parse_using_templates_inventory_request (uc); - if (GNUNET_OK == res) - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - break; - } - GNUNET_break (0); - use_reply_with_error ( - uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "template_type"); -} - - -/* ***************** USE_PHASE_LOOKUP_TEMPLATE **************** */ - -/** - * Main function for the #USE_PHASE_LOOKUP_TEMPLATE. - * - * @param[in,out] uc context to update - */ -static void -handle_phase_lookup_template ( - struct UseContext *uc) -{ - struct TMH_MerchantInstance *mi = uc->hc->instance; - const char *template_id = uc->hc->infix; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_template (TMH_db->cls, - mi->settings.id, - template_id, - &uc->lookup_template.etp); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_template"); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - /* this should be impossible (single select) */ - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_template"); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* template not found! */ - use_reply_with_error (uc, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, - template_id); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* all good */ - break; - } - if (uc->template_type != - TALER_MERCHANT_template_type_from_contract ( - uc->lookup_template.etp.template_contract)) - { - GNUNET_break_op (0); - use_reply_with_error ( - uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_WRONG_TYPE, - "template_contract has different type"); - return; - } - uc->phase++; -} - - -/* ***************** USE_PHASE_PARSE_TEMPLATE **************** */ - - -/** - * Parse template. - * - * @param[in,out] uc use context - */ -static void -handle_phase_template_contract (struct UseContext *uc) -{ - const char *err_name; - enum GNUNET_GenericReturnValue res; - - res = TALER_MERCHANT_template_contract_parse ( - uc->lookup_template.etp.template_contract, - &uc->template_contract, - &err_name); - if (GNUNET_OK != res) - { - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - err_name); - return; - } - uc->phase++; -} - - -/* ***************** USE_PHASE_DB_FETCH **************** */ - -/** - * Fetch DB data for inventory templates. - * - * @param[in,out] uc use context - */ -static void -handle_phase_db_fetch (struct UseContext *uc) -{ - struct TMH_MerchantInstance *mi = uc->hc->instance; - - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - GNUNET_assert (0); - } - - for (unsigned int i = 0; - i < uc->parse_request.inventory.items_len; - i++) - { - struct InventoryTemplateItemContext *item = - &uc->parse_request.inventory.items[i]; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_product (TMH_db->cls, - mi->settings.id, - item->product_id, - &item->pd, - &item->num_categories, - &item->categories); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_product"); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_product"); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - use_reply_with_error (uc, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - item->product_id); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } - uc->phase++; -} - - -/* *************** Helpers for USE_PHASE_VERIFY ***************** */ - -/** - * Check if the given product ID appears in the array of allowed_products. - * - * @param allowed_products JSON array of product IDs allowed by the template, may be NULL - * @param product_id product ID to check - * @return true if the product ID is in the list - */ -static bool -product_id_allowed (const json_t *allowed_products, - const char *product_id) -{ - const json_t *entry; - size_t idx; - - if (NULL == allowed_products) - return false; - json_array_foreach ((json_t *) allowed_products, idx, entry) - { - if (! json_is_string (entry)) - { - GNUNET_break (0); - 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 allowed_categories JSON array of categories allowed by the template, may be NULL - * @param num_categories length of @a categories - * @param categories list of categories of the selected product - * @return true if any category of the product is in the list of allowed categories matches - */ -static bool -category_allowed (const json_t *allowed_categories, - size_t num_categories, - const uint64_t categories[num_categories]) -{ - const json_t *entry; - size_t idx; - - if (NULL == allowed_categories) - return false; - json_array_foreach ((json_t *) allowed_categories, - idx, - entry) - { - uint64_t selected_id; - - if (! json_is_integer (entry)) - { - GNUNET_break (0); - continue; - } - if (0 > json_integer_value (entry)) - { - GNUNET_break (0); - 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; -} - - -/** - * Verify request data for inventory templates. - * Checks that the selected products are allowed - * for this template. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -verify_using_templates_inventory (struct UseContext *uc) -{ - if (uc->template_contract.details.inventory.choose_one && - (1 != uc->parse_request.inventory.items_len)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inventory_selection"); - return GNUNET_SYSERR; - } - if (uc->template_contract.details.inventory.selected_all) - return GNUNET_OK; - for (unsigned int i = 0; - i < uc->parse_request.inventory.items_len; - i++) - { - struct InventoryTemplateItemContext *item = - &uc->parse_request.inventory.items[i]; - const char *eparam = NULL; - - if (GNUNET_OK != - TALER_MERCHANT_vk_process_quantity_inputs ( - TALER_MERCHANT_VK_QUANTITY, - item->pd.allow_fractional_quantity, - true, - 0, - false, - item->unit_quantity, - &item->quantity_value, - &item->quantity_frac, - &eparam)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - eparam); - return GNUNET_SYSERR; - } - - if (0 == item->pd.price_array_length) - { - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "price_array"); - return GNUNET_SYSERR; - } - } - - for (unsigned int i = 0; - i < uc->parse_request.inventory.items_len; - i++) - { - struct InventoryTemplateItemContext *item = - &uc->parse_request.inventory.items[i]; - - if (product_id_allowed (uc->template_contract.details.inventory. - selected_products, - item->product_id)) - continue; - if (category_allowed ( - uc->template_contract.details.inventory.selected_categories, - item->num_categories, - item->categories)) - continue; - GNUNET_break_op (0); - use_reply_with_error ( - uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_WRONG_PRODUCT, - item->product_id); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Verify request data for fixed-order templates. - * As here we cannot compute the total amount, either - * the template or the client request must provide it. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -verify_using_templates_fixed ( - struct UseContext *uc) -{ - if ( (! uc->parse_request.no_amount) && - (! uc->template_contract.no_amount) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, - NULL); - return GNUNET_SYSERR; - } - if (uc->parse_request.no_amount && - uc->template_contract.no_amount) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT, - NULL); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Verify request data for paivana templates. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -verify_using_templates_paivana ( - struct UseContext *uc) -{ - if (NULL != uc->template_contract.details.paivana.website_regex) - { - regex_t ex; - bool allowed = false; - - if (0 != regcomp (&ex, - uc->template_contract.details.paivana.website_regex, - REG_NOSUB | REG_EXTENDED)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 == - regexec (&ex, - uc->parse_request.paivana.website, - 0, NULL, - 0)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Website `%s' allowed by template\n", - uc->parse_request.paivana.website); - allowed = true; - } - regfree (&ex); - if (! allowed) - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Verify that the client request is structurally acceptable for the specified - * template. Does NOT check the total amount being reasonable. - * - * @param[in,out] uc use context - */ -static void -handle_phase_verify ( - struct UseContext *uc) -{ - enum GNUNET_GenericReturnValue res = GNUNET_SYSERR; - - if ( (NULL != uc->parse_request.summary) && - (NULL != uc->template_contract.summary) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT, - NULL); - return; - } - if ( (NULL == uc->parse_request.summary) && - (NULL == uc->template_contract.summary) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY, - NULL); - return; - } - if ( (! uc->parse_request.no_amount) && - (NULL != uc->template_contract.currency) && - (0 != strcasecmp (uc->template_contract.currency, - uc->parse_request.amount.currency)) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - uc->template_contract.currency); - return; - } - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - res = verify_using_templates_fixed (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - res = verify_using_templates_paivana (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - res = verify_using_templates_inventory (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - GNUNET_assert (0); - } - if (GNUNET_OK == res) - uc->phase++; -} - - -/* ***************** USE_PHASE_COMPUTE_PRICE **************** */ - - -/** - * 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..TALER_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; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (unit_price->currency, - line_total)); - if ( (0 != quantity) && - (0 > - TALER_amount_multiply (line_total, - unit_price, - (uint32_t) quantity)) ) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - if (0 == quantity_frac) - return GNUNET_OK; - if (0 > - TALER_amount_multiply (&tmp, - unit_price, - quantity_frac)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - TALER_amount_divide (&tmp, - &tmp, - TALER_MERCHANT_UNIT_FRAC_BASE); - if (0 > - TALER_amount_add (line_total, - line_total, - &tmp)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Find the price of the given @a item in the specified - * @a currency. - * - * @param currency currency to search price in - * @param item item to check prices of - * @return NULL if a suitable price was not found - */ -static const struct TALER_Amount * -find_item_price_in_currency ( - const char *currency, - const struct InventoryTemplateItemContext *item) -{ - for (size_t j = 0; j < item->pd.price_array_length; j++) - { - if (0 == strcasecmp (item->pd.price_array[j].currency, - currency)) - return &item->pd.price_array[j]; - } - return NULL; -} - - -/** - * Compute totals for all currencies shared across selected products. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success (including no price due to no items) - * #GNUNET_NO if we could not find a price in any accepted currency - * for all selected products - * #GNUNET_SYSERR on arithmetic issues (internal error) - */ -static enum GNUNET_GenericReturnValue -compute_totals_per_currency (struct UseContext *uc) -{ - const struct InventoryTemplateItemContext *items - = uc->parse_request.inventory.items; - unsigned int items_len = uc->parse_request.inventory.items_len; - - if (0 == items_len) - return GNUNET_OK; - for (size_t i = 0; i < items[0].pd.price_array_length; i++) - { - const struct TALER_Amount *price - = &items[0].pd.price_array[i]; - struct TALER_Amount zero; - - if (! TMH_test_exchange_configured_for_currency (price->currency)) - continue; - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (price->currency, - &zero)); - GNUNET_array_append (uc->compute_price.totals, - uc->compute_price.totals_len, - zero); - } - if (0 == uc->compute_price.totals_len) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No currency supported by our configuration in which we have prices for first selected product!\n"); - return GNUNET_NO; - } - /* Loop through items, ensure each currency exists and sum totals. */ - for (unsigned int i = 0; i < items_len; i++) - { - const struct InventoryTemplateItemContext *item = &items[i]; - unsigned int c = 0; - - while (c < uc->compute_price.totals_len) - { - struct TALER_Amount *total = &uc->compute_price.totals[c]; - const struct TALER_Amount *unit_price; - struct TALER_Amount line_total; - - unit_price = find_item_price_in_currency (total->currency, - item); - if (NULL == unit_price) - { - /* Drop the currency: we have no price in one of - the selected products */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Product `%s' has no price in %s: dropping currency\n", - item->product_id, - total->currency); - *total = uc->compute_price.totals[--uc->compute_price.totals_len]; - continue; - } - if (GNUNET_OK != - compute_line_total (unit_price, - item->quantity_value, - item->quantity_frac, - &line_total)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - if (0 > - TALER_amount_add (total, - total, - &line_total)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - c++; - } - } - if (0 == uc->compute_price.totals_len) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No currency available in which we have prices for all selected products!\n"); - GNUNET_free (uc->compute_price.totals); - } - return (0 == uc->compute_price.totals_len) - ? GNUNET_NO - : GNUNET_OK; -} - - -/** - * Compute total for only the given @a currency. - * - * @param items_len length of @a items - * @param items inventory items - * @param currency currency to total - * @param[out] total computed total - * @return #GNUNET_OK on success - * #GNUNET_NO if we could not find a price in any accepted currency - * for all selected products - * #GNUNET_SYSERR on arithmetic issues (internal error) - */ -static enum GNUNET_GenericReturnValue -compute_inventory_total (unsigned int items_len, - const struct InventoryTemplateItemContext *items, - const char *currency, - struct TALER_Amount *total) -{ - GNUNET_assert (NULL != currency); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (currency, - total)); - for (unsigned int i = 0; i < items_len; i++) - { - const struct InventoryTemplateItemContext *item = &items[i]; - const struct TALER_Amount *unit_price; - struct TALER_Amount line_total; - - unit_price = find_item_price_in_currency (currency, - item); - if (NULL == unit_price) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "compute_inventory_total: no price in %s for product `%s'\n", - currency, - item->product_id); - return GNUNET_NO; - } - if (GNUNET_OK != - compute_line_total (unit_price, - item->quantity_value, - item->quantity_frac, - &line_total)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "compute_inventory_total: line total failed for %s in %s\n", - item->product_id, - currency); - return GNUNET_SYSERR; - } - if (0 > - TALER_amount_add (total, - total, - &line_total)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - } - return GNUNET_OK; -} - - -/** - * Compute total price. - * - * @param[in,out] uc use context - */ -static void -handle_phase_compute_price (struct UseContext *uc) -{ - const char *primary_currency; - enum GNUNET_GenericReturnValue ret; - - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - uc->compute_price.totals - = GNUNET_new (struct TALER_Amount); - uc->compute_price.totals_len - = 1; - if (uc->parse_request.no_amount) - { - GNUNET_assert (! uc->template_contract.no_amount); - *uc->compute_price.totals - = uc->template_contract.amount; - } - else - { - GNUNET_assert (uc->template_contract.no_amount); - *uc->compute_price.totals - = uc->parse_request.amount; - } - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - /* handled below */ - break; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - { - const struct TALER_MERCHANT_TemplateContractPaivana *tcp - = &uc->template_contract.details.paivana; - json_t *choices; - - choices = json_array (); - GNUNET_assert (NULL != choices); - for (size_t i = 0; i < tcp->choices_len; i++) - { - /* Make deep copy, we're going to MODIFY it! */ - struct TALER_MERCHANT_ContractChoice choice - = tcp->choices[i]; - - choice.no_tip = uc->parse_request.no_tip; - if (! uc->parse_request.no_tip) - { - if (GNUNET_YES != - TALER_amount_cmp_currency (&choice.amount, - &uc->parse_request.tip)) - continue; /* tip does not match choice currency */ - choice.tip = uc->parse_request.tip; - if (0 > - TALER_amount_add (&choice.amount, - &choice.amount, - &uc->parse_request.tip)) - { - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - "tip"); - return; - } - } - GNUNET_assert (0 == - json_array_append_new ( - choices, - TALER_MERCHANT_json_from_contract_choice (&choice, - true))); - } - if (0 == json_array_size (choices)) - { - GNUNET_break_op (0); - json_decref (choices); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY, - "tip"); - return; - } - uc->compute_price.choices = choices; - } - /* Note: we already did the tip and pricing - fully here, so we skip these phases. */ - uc->phase = USE_PHASE_CREATE_ORDER; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - GNUNET_assert (0); - } - primary_currency = uc->template_contract.currency; - if (! uc->parse_request.no_amount) - primary_currency = uc->parse_request.amount.currency; - if (! uc->parse_request.no_tip) - primary_currency = uc->parse_request.tip.currency; - if (NULL == primary_currency) - { - ret = compute_totals_per_currency (uc); - } - else - { - uc->compute_price.totals - = GNUNET_new (struct TALER_Amount); - uc->compute_price.totals_len - = 1; - ret = compute_inventory_total ( - uc->parse_request.inventory.items_len, - uc->parse_request.inventory.items, - primary_currency, - uc->compute_price.totals); - } - if (GNUNET_SYSERR == ret) - { - use_reply_with_error ( - uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - "calculation of currency totals failed"); - return; - } - if (GNUNET_NO == ret) - { - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY, - NULL); - return; - } - - uc->phase++; -} - - -/* ***************** USE_PHASE_CHECK_TIP **************** */ - - -/** - * Check that tip specified is reasonable and add to total. - * - * @param[in,out] uc use context - */ -static void -handle_phase_check_tip (struct UseContext *uc) -{ - struct TALER_Amount *total_amount; - - if (uc->parse_request.no_tip) - { - uc->phase++; - return; - } - if (0 == uc->compute_price.totals_len) - { - if (! TMH_test_exchange_configured_for_currency ( - uc->parse_request.tip.currency)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - "Tip currency is not supported by backend"); - return; - } - uc->compute_price.totals - = GNUNET_new (struct TALER_Amount); - uc->compute_price.totals_len - = 1; - *uc->compute_price.totals - = uc->parse_request.tip; - uc->phase++; - return; - } - GNUNET_assert (1 == uc->compute_price.totals_len); - total_amount = &uc->compute_price.totals[0]; - if (GNUNET_YES != - TALER_amount_cmp_currency (&uc->parse_request.tip, - total_amount)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - uc->parse_request.tip.currency); - return; - } - if (0 > - TALER_amount_add (total_amount, - total_amount, - &uc->parse_request.tip)) - { - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - "tip"); - return; - } - uc->phase++; -} - - -/* ***************** USE_PHASE_CHECK_TOTAL **************** */ - -/** - * Check that if the client specified a total, - * it matches our own calculation. - * - * @param[in,out] uc use context - */ -static void -handle_phase_check_total (struct UseContext *uc) -{ - GNUNET_assert (1 <= uc->compute_price.totals_len); - if (! uc->parse_request.no_amount) - { - GNUNET_assert (1 == uc->compute_price.totals_len); - GNUNET_assert (GNUNET_YES == - TALER_amount_cmp_currency (&uc->parse_request.amount, - &uc->compute_price.totals[0])); - if (0 != - TALER_amount_cmp (&uc->parse_request.amount, - &uc->compute_price.totals[0])) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, - TALER_amount2s (&uc->compute_price.totals[0])); - return; - } - } - uc->phase++; -} - - -/* ***************** USE_PHASE_CREATE_ORDER **************** */ - - -/** - * Create order request for inventory templates. - * - * @param[in,out] uc use context - */ -static void -create_using_templates_inventory (struct UseContext *uc) -{ - json_t *inventory_products; - json_t *choices; - - inventory_products = json_array (); - GNUNET_assert (NULL != inventory_products); - for (unsigned int i = 0; - i < uc->parse_request.inventory.items_len; - i++) - { - const struct InventoryTemplateItemContext *item = - &uc->parse_request.inventory.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)))); - } - choices = json_array (); - GNUNET_assert (NULL != choices); - for (unsigned int i = 0; - i < uc->compute_price.totals_len; - i++) - { - GNUNET_assert (0 == - json_array_append_new ( - choices, - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - &uc->compute_price.totals[i]), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("tip", - uc->parse_request.no_tip - ? NULL - : &uc->parse_request.tip)) - ))); - } - - uc->ihc.request_body - = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("otp_id", - uc->lookup_template.etp.otp_id)), - GNUNET_JSON_pack_array_steal ("inventory_products", - inventory_products), - GNUNET_JSON_pack_object_steal ( - "order", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("version", - 1), - GNUNET_JSON_pack_array_steal ("choices", - choices), - GNUNET_JSON_pack_string ("summary", - NULL == uc->parse_request.summary - ? uc->template_contract.summary - : uc->parse_request.summary)))); -} - - -/** - * Create order request for fixed-order templates. - * - * @param[in,out] uc use context - */ -static void -create_using_templates_fixed (struct UseContext *uc) -{ - uc->ihc.request_body - = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("otp_id", - uc->lookup_template.etp.otp_id)), - GNUNET_JSON_pack_object_steal ( - "order", - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ( - "amount", - &uc->compute_price.totals[0]), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("tip", - uc->parse_request.no_tip - ? NULL - : &uc->parse_request.tip)), - GNUNET_JSON_pack_string ( - "summary", - NULL == uc->parse_request.summary - ? uc->template_contract.summary - : uc->parse_request.summary)))); -} - - -/** - * Create order request for paivana templates. - * - * @param[in,out] uc use context - */ -static void -create_using_templates_paivana (struct UseContext *uc) -{ - uc->ihc.request_body - = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ( - "session_id", - uc->parse_request.paivana.paivana_id), - GNUNET_JSON_pack_object_steal ( - "order", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("version", - 1), - GNUNET_JSON_pack_array_incref ("choices", - uc->compute_price.choices), - GNUNET_JSON_pack_string ( - "summary", - NULL == uc->parse_request.summary - ? uc->template_contract.summary - : uc->parse_request.summary), - GNUNET_JSON_pack_string ("fulfillment_url", - uc->parse_request.paivana.website)))); -} - - -static void -handle_phase_create_order (struct UseContext *uc) -{ - GNUNET_assert (NULL == uc->ihc.request_body); - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - create_using_templates_fixed (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - create_using_templates_paivana (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - create_using_templates_inventory (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - GNUNET_assert (0); - } - uc->phase++; -} - - -/* ***************** Main handler **************** */ - -MHD_RESULT -TMH_post_using_templates_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct UseContext *uc = hc->ctx; - - (void) rh; - if (NULL == uc) - { - uc = GNUNET_new (struct UseContext); - uc->hc = hc; - hc->ctx = uc; - hc->cc = &cleanup_use_context; - uc->ihc.instance = hc->instance; - uc->phase = USE_PHASE_PARSE_REQUEST; - uc->template_type = TALER_MERCHANT_TEMPLATE_TYPE_INVALID; - } - - while (1) - { - switch (uc->phase) - { - case USE_PHASE_PARSE_REQUEST: - handle_phase_parse_request (uc); - break; - case USE_PHASE_LOOKUP_TEMPLATE: - handle_phase_lookup_template (uc); - break; - case USE_PHASE_PARSE_TEMPLATE: - handle_phase_template_contract (uc); - break; - case USE_PHASE_DB_FETCH: - handle_phase_db_fetch (uc); - break; - case USE_PHASE_VERIFY: - handle_phase_verify (uc); - break; - case USE_PHASE_COMPUTE_PRICE: - handle_phase_compute_price (uc); - break; - case USE_PHASE_CHECK_TIP: - handle_phase_check_tip (uc); - break; - case USE_PHASE_CHECK_TOTAL: - handle_phase_check_total (uc); - break; - case USE_PHASE_CREATE_ORDER: - handle_phase_create_order (uc); - break; - case USE_PHASE_SUBMIT_ORDER: - return TMH_private_post_orders ( - NULL, /* not even used */ - connection, - &uc->ihc); - case USE_PHASE_FINISHED_MHD_YES: - return MHD_YES; - case USE_PHASE_FINISHED_MHD_NO: - return MHD_NO; - } - } -} diff --git a/src/backend/taler-merchant-httpd_post-using-templates.h b/src/backend/taler-merchant-httpd_post-using-templates.h @@ -1,40 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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 taler-merchant-httpd_post-using-templates.h - * @brief headers for POST /using-templates handler - * @author Priscilla Huang - */ -#ifndef TALER_MERCHANT_HTTPD_POST_USING_TEMPLATES_H -#define TALER_MERCHANT_HTTPD_POST_USING_TEMPLATES_H - -#include "taler-merchant-httpd.h" - -/** - * Generate a template that customer can use it. Returns an MHD_RESULT. - * - * @param rh details about this request handler - * @param connection the MHD connection to handle - * @param[in,out] hc context with further information about the request - * @return MHD result code - */ -MHD_RESULT -TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif