merchant

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

commit 893e793496541fc4d28d0d5b77739952de0e1e26
parent 93b7f0022fdb0f8fc62f2ef977e01a87973d5376
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Wed, 23 Jul 2025 08:25:29 +0200

Merge branch 'master' into dev/bohdan-potuzhnyi/donau-integration

Diffstat:
Mconfigure.ac | 2+-
Mdebian/changelog | 6++++++
Mdoc/doxygen/taler.doxy | 2+-
Msrc/backend/taler-merchant-httpd_config.c | 2+-
Msrc/backend/taler-merchant-httpd_exchanges.c | 16++++++++++++++--
Msrc/backend/taler-merchant-httpd_exchanges.h | 12++++++++++++
Msrc/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c | 7+++++++
Msrc/backend/taler-merchant-httpd_private-get-orders.c | 226++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/backend/taler-merchant-httpd_private-post-orders.c | 1224++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/backenddb/merchant-0020.sql | 3+++
Msrc/backenddb/pg_lookup_statistics_counter_by_interval.c | 2+-
11 files changed, 1122 insertions(+), 380 deletions(-)

diff --git a/configure.ac b/configure.ac @@ -18,7 +18,7 @@ # This configure file is in the public domain AC_PREREQ([2.69]) -AC_INIT([taler-merchant],[1.0.4],[taler-bug@gnunet.org]) +AC_INIT([taler-merchant],[1.0.5],[taler-bug@gnunet.org]) AC_CONFIG_SRCDIR([src/backend/taler-merchant-httpd.c]) AC_CONFIG_HEADERS([taler_merchant_config.h]) # support for non-recursive builds diff --git a/debian/changelog b/debian/changelog @@ -1,3 +1,9 @@ +taler-merchant (1.0.5) unstable; urgency=low + + * Release 1.0.5. + + -- Florian Dold <florian@dold.me> Wed, 16 Jul 2025 21:46:01 +0200 + taler-merchant (1.0.4) unstable; urgency=low * Release 1.0.4. diff --git a/doc/doxygen/taler.doxy b/doc/doxygen/taler.doxy @@ -5,7 +5,7 @@ #--------------------------------------------------------------------------- DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "GNU Taler: Merchant" -PROJECT_NUMBER = 1.0.4 +PROJECT_NUMBER = 1.0.5 PROJECT_LOGO = logo.svg OUTPUT_DIRECTORY = . CREATE_SUBDIRS = YES diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c @@ -43,7 +43,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "20:0:17" +#define MERCHANT_PROTOCOL_VERSION "20:0:8" /** diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c @@ -35,7 +35,7 @@ * Threshold after which exponential backoff should not increase. */ #define RETRY_BACKOFF_THRESHOLD GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_SECONDS, 60) + GNUNET_TIME_UNIT_SECONDS, 60) /** * This is how long /keys long-polls for, so we should @@ -43,7 +43,7 @@ * answer. See exchange_api_handle.c. */ #define LONG_POLL_THRESHOLD GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_SECONDS, 120) + GNUNET_TIME_UNIT_SECONDS, 120) /** @@ -258,6 +258,18 @@ lookup_exchange (const char *exchange_url) } +bool +TMH_EXCHANGES_check_trusted ( + const char *exchange_url) +{ + struct TMH_Exchange *exchange = lookup_exchange (exchange_url); + + if (NULL == exchange) + return false; + return exchange->trusted; +} + + /** * Check if we have any remaining pending requests for the * given @a exchange, and if we have the required data, call diff --git a/src/backend/taler-merchant-httpd_exchanges.h b/src/backend/taler-merchant-httpd_exchanges.h @@ -76,6 +76,18 @@ struct TMH_EXCHANGES_KeysOperation; /** + * Check if we trust the exchange at @a exchange_url. + * + * @param exchange_url exchange base url to check + * @return true if we trust that exchange (assuming the master + * public key matches) + */ +bool +TMH_EXCHANGES_check_trusted ( + const char *exchange_url); + + +/** * Get /keys of the given @a exchange. * * @param exchange URL of the exchange we would like to talk to diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c @@ -911,6 +911,13 @@ kyc_status_cb ( struct KycContext *kc = cls; struct ExchangeKycRequest *ekr; + if (! TMH_EXCHANGES_check_trusted (exchange_url)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Skipping exchange `%s': not trusted\n", + exchange_url); + return; + } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC status for `%s' at `%s' is %u/%s/%s/%s\n", payto_uri.full_payto, diff --git a/src/backend/taler-merchant-httpd_private-get-orders.c b/src/backend/taler-merchant-httpd_private-get-orders.c @@ -20,6 +20,7 @@ */ #include "platform.h" #include "taler-merchant-httpd_private-get-orders.h" +#include <taler/taler_merchant_util.h> #include <taler/taler_json_lib.h> #include <taler/taler_dbevents.h> @@ -302,11 +303,12 @@ add_order (void *cls, json_t *contract_terms = NULL; struct TALER_PrivateContractHashP h_contract_terms; enum GNUNET_DB_QueryStatus qs; - const char *summary; char *order_id = NULL; bool refundable = false; bool paid; - struct TALER_Amount order_amount; + bool wired; + struct TALER_MERCHANT_Contract *contract = NULL; + int16_t choice_index = -1; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Adding order `%s' (%llu) to result set at instance `%s'\n", @@ -341,13 +343,19 @@ add_order (void *cls, { /* First try to find the order in the contracts */ uint64_t os; - - qs = TMH_db->lookup_contract_terms (TMH_db->cls, - po->instance_id, - order_id, - &contract_terms, - &os, - NULL); + bool session_matches; + + qs = TMH_db->lookup_contract_terms3 (TMH_db->cls, + po->instance_id, + order_id, + NULL, + &contract_terms, + &os, + &paid, + &wired, + &session_matches, + NULL, + &choice_index); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) GNUNET_break (os == order_serial); } @@ -356,6 +364,8 @@ add_order (void *cls, /* Might still be unclaimed, so try order table */ struct TALER_MerchantPostDataHashP unused; + paid = false; + wired = false; qs = TMH_db->lookup_order (TMH_db->cls, po->instance_id, order_id, @@ -368,112 +378,152 @@ add_order (void *cls, GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Order %llu disappeared during iteration. Skipping.\n", (unsigned long long) order_serial); - json_decref (contract_terms); /* should still be NULL */ - GNUNET_free (order_id); - return; + goto cleanup; } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { GNUNET_break (0); po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; - json_decref (contract_terms); - GNUNET_free (order_id); - return; + goto cleanup; } + contract = TALER_MERCHANT_contract_parse (contract_terms, + true); + if (NULL == contract) { - struct GNUNET_TIME_Timestamp rd; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any ("amount", - &order_amount), - GNUNET_JSON_spec_timestamp ("refund_deadline", - &rd), - GNUNET_JSON_spec_string ("summary", - &summary), - GNUNET_JSON_spec_end () + GNUNET_break (0); + po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID; + goto cleanup; + } + + if (GNUNET_TIME_absolute_is_future ( + contract->refund_deadline.abs_time) && + paid) + { + struct ProcessRefundsClosure prc = { + .ec = TALER_EC_NONE }; + const struct TALER_Amount *brutto; - if (GNUNET_OK != - GNUNET_JSON_parse (contract_terms, - spec, - NULL, NULL)) + switch (contract->version) { + case TALER_MERCHANT_CONTRACT_VERSION_0: + brutto = &contract->details.v0.brutto; + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + { + struct TALER_MERCHANT_ContractChoice *choice + = &contract->details.v1.choices[choice_index]; + + GNUNET_assert (choice_index < contract->details.v1.choices_len); + brutto = &choice->amount; + } + break; + default: GNUNET_break (0); - po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID; - json_decref (contract_terms); - GNUNET_free (order_id); - return; + goto cleanup; + } + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (brutto->currency, + &prc.total_refund_amount)); + qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, + po->instance_id, + &h_contract_terms, + &process_refunds_cb, + &prc); + if (0 > qs) + { + GNUNET_break (0); + po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; + goto cleanup; } + if (TALER_EC_NONE != prc.ec) + { + GNUNET_break (0); + po->result = prc.ec; + goto cleanup; + } + if (0 > TALER_amount_cmp (&prc.total_refund_amount, + brutto)) + refundable = true; + } - if (TALER_amount_is_zero (&order_amount) && + switch (contract->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + if (TALER_amount_is_zero (&contract->details.v0.brutto) && (po->of.wired != TALER_EXCHANGE_YNA_ALL) ) { /* If we are actually filtering by wire status, and the order was over an amount of zero, do not return it as wire status is not exactly meaningful for orders over zero. */ - json_decref (contract_terms); - GNUNET_free (order_id); - return; + goto cleanup; } - - if (GNUNET_TIME_absolute_is_future (rd.abs_time) && - paid) + GNUNET_assert ( + 0 == + json_array_append_new ( + po->pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("order_id", + contract->order_id), + GNUNET_JSON_pack_uint64 ("row_id", + order_serial), + GNUNET_JSON_pack_timestamp ("timestamp", + creation_time), + TALER_JSON_pack_amount ( + "amount", + &contract->details.v0.brutto), + GNUNET_JSON_pack_string ("summary", + contract->summary), + GNUNET_JSON_pack_bool ("refundable", + refundable), + GNUNET_JSON_pack_bool ("paid", + paid)))); + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + if (-1 == choice_index) + choice_index = 0; /* default choice */ + GNUNET_assert (choice_index < contract->details.v1.choices_len); { - struct ProcessRefundsClosure prc = { - .ec = TALER_EC_NONE - }; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (order_amount.currency, - &prc.total_refund_amount)); - qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, - po->instance_id, - &h_contract_terms, - &process_refunds_cb, - &prc); - if (0 > qs) - { - GNUNET_break (0); - po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; - json_decref (contract_terms); - GNUNET_free (order_id); - return; - } - if (TALER_EC_NONE != prc.ec) - { - GNUNET_break (0); - po->result = prc.ec; - json_decref (contract_terms); - GNUNET_free (order_id); - return; - } - if (0 > TALER_amount_cmp (&prc.total_refund_amount, - &order_amount)) - refundable = true; + struct TALER_MERCHANT_ContractChoice *choice + = &contract->details.v1.choices[choice_index]; + + GNUNET_assert ( + 0 == + json_array_append_new ( + po->pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("order_id", + contract->order_id), + GNUNET_JSON_pack_uint64 ("row_id", + order_serial), + GNUNET_JSON_pack_timestamp ("timestamp", + creation_time), + TALER_JSON_pack_amount ("amount", + &choice->amount), + GNUNET_JSON_pack_string ("summary", + contract->summary), + GNUNET_JSON_pack_bool ("refundable", + refundable), + GNUNET_JSON_pack_bool ("paid", + paid)))); } + break; + default: + GNUNET_break (0); + goto cleanup; } - GNUNET_assert (0 == - json_array_append_new ( - po->pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("order_id", - order_id), - GNUNET_JSON_pack_uint64 ("row_id", - order_serial), - GNUNET_JSON_pack_timestamp ("timestamp", - creation_time), - TALER_JSON_pack_amount ("amount", - &order_amount), - GNUNET_JSON_pack_string ("summary", - summary), - GNUNET_JSON_pack_bool ("refundable", - refundable), - GNUNET_JSON_pack_bool ("paid", - paid)))); +cleanup: json_decref (contract_terms); GNUNET_free (order_id); + if (NULL != contract) + { + TALER_MERCHANT_contract_free (contract); + contract = NULL; + } } diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014-2024 Taler Systems SA + (C) 2014-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -157,6 +157,47 @@ struct RekeyExchange /** + * Data structure where we evaluate the viability of a given + * wire method for this order. + */ +struct WireMethodCandidate +{ + /** + * Kept in a DLL. + */ + struct WireMethodCandidate *next; + + /** + * Kept in a DLL. + */ + struct WireMethodCandidate *prev; + + /** + * The wire method we are evaluating. + */ + const struct TMH_WireMethod *wm; + + /** + * List of exchanges to use when we use this wire method. + */ + json_t *exchanges; + + /** + * Array of maximum amounts that could be paid over all available exchanges + * for this @a wm. Used to determine if this order creation requests exceeds + * legal limits. + */ + struct TALER_Amount *total_exchange_limits; + + /** + * Length of the @e total_exchange_limits array. + */ + unsigned int num_total_exchange_limits; + +}; + + +/** * Information we keep per order we are processing. */ struct OrderContext @@ -232,19 +273,6 @@ struct OrderContext } parse_request; - - /** - * Information set in the ORDER_PHASE_ADD_PAYMENT_DETAILS phase. - */ - struct - { - /** - * Wire method (and our bank account) we have selected - * to be included for this order. - */ - const struct TMH_WireMethod *wm; - } add_payment_details; - /** * Information set in the ORDER_PHASE_PARSE_ORDER phase. */ @@ -431,52 +459,78 @@ struct OrderContext } merge_inventory; /** - * Information set in the ORDER_PHASE_SET_EXCHANGES phase. + * Information set in the ORDER_PHASE_ADD_PAYMENT_DETAILS phase. */ struct { + /** - * Array of exchanges we find acceptable for this - * order. + * DLL of wire methods under evaluation. */ - json_t *exchanges; + struct WireMethodCandidate *wmc_head; /** - * Forced requests to /keys to update our exchange - * information. + * DLL of wire methods under evaluation. */ - struct RekeyExchange *pending_reload_head; + struct WireMethodCandidate *wmc_tail; /** - * Forced requests to /keys to update our exchange - * information. + * Array of maximum amounts that appear in the contract choices + * per currency. + * Determines the maximum amounts that a client could pay for this + * order and which we must thus make sure is acceptable for the + * selected wire method/account if possible. */ - struct RekeyExchange *pending_reload_tail; + struct TALER_Amount *max_choice_limits; /** - * Did we previously force reloading of /keys from - * all exchanges? Set to 'true' to prevent us from - * doing it again (and again...). + * Length of the @e max_choice_limits array. */ - bool forced_reload; + unsigned int num_max_choice_limits; + + /** + * Set to true if we may need an exchange. True if any amount is non-zero. + */ + bool need_exchange; + + } add_payment_details; + + /** + * Information set in the ORDER_PHASE_SELECT_WIRE_METHOD phase. + */ + struct + { + + /** + * Array of exchanges we find acceptable for this order and wire method. + */ + json_t *exchanges; /** - * Set to true once we are sure that we have at - * least one good exchange. + * Wire method (and our bank account) we have selected + * to be included for this order. */ - bool exchange_good; + const struct TMH_WireMethod *wm; + + } select_wire_method; + + /** + * Information set in the ORDER_PHASE_SET_EXCHANGES phase. + */ + struct + { /** - * Array of maximum amounts that could be paid over all - * available exchanges. Used to determine if this - * order creation requests exceeds legal limits. + * Forced requests to /keys to update our exchange + * information. */ - struct TALER_Amount *total_exchange_limits; + struct RekeyExchange *pending_reload_head; /** - * Length of the @e total_exchange_limits array. + * Forced requests to /keys to update our exchange + * information. */ - unsigned int num_total_exchange_limits; + struct RekeyExchange *pending_reload_tail; /** * How long do we wait at most until giving up on getting keys? @@ -489,6 +543,19 @@ struct OrderContext struct GNUNET_SCHEDULER_Task *wakeup_task; /** + * Did we previously force reloading of /keys from + * all exchanges? Set to 'true' to prevent us from + * doing it again (and again...). + */ + bool forced_reload; + + /** + * Set to true once we have attempted to load exchanges + * for the first time. + */ + bool exchanges_tried; + + /** * Details depending on the contract version. */ union @@ -638,6 +705,7 @@ struct OrderContext ORDER_PHASE_MERGE_INVENTORY, ORDER_PHASE_ADD_PAYMENT_DETAILS, ORDER_PHASE_SET_EXCHANGES, + ORDER_PHASE_SELECT_WIRE_METHOD, ORDER_PHASE_SET_MAX_FEE, ORDER_PHASE_SERIALIZE_ORDER, ORDER_PHASE_SALT_FORGETTABLE, @@ -687,6 +755,66 @@ TMH_force_orders_resume () /** + * Add the given @a val to the @a array. Adds the + * amount to a given entry in @a array if one with the same + * currency exists, otherwise extends the @a array. + * + * @param[in,out] array pointer to array of amounts + * @param[in,out] array_len length of @a array + * @param val amount to add + * @param cap cap for the sums to enforce, can be NULL + */ +static void +add_to_currency_vector (struct TALER_Amount **array, + unsigned int *array_len, + const struct TALER_Amount *val, + const struct TALER_Amount *cap) +{ + for (unsigned int i = 0; i<*array_len; i++) + { + struct TALER_Amount *ai = &(*array)[i]; + + if (GNUNET_OK == + TALER_amount_cmp_currency (ai, + val)) + { + enum TALER_AmountArithmeticResult aar; + + aar = TALER_amount_add (ai, + ai, + val); + /* If we have a cap, we tolerate the overflow */ + GNUNET_assert ( (aar >= 0) || + ( (TALER_AAR_INVALID_RESULT_OVERFLOW == aar) && + (NULL != cap) ) ); + if (TALER_AAR_INVALID_RESULT_OVERFLOW == aar) + { + *ai = *cap; + } + else if (NULL != cap) + GNUNET_assert (GNUNET_OK == + TALER_amount_min (ai, + ai, + cap)); + return; + } + } + GNUNET_array_append (*array, + *array_len, + *val); + if (NULL != cap) + { + struct TALER_Amount *ai = &(*array)[(*array_len) - 1]; + + GNUNET_assert (GNUNET_OK == + TALER_amount_min (ai, + ai, + cap)); + } +} + + +/** * Update the phase of @a oc based on @a mret. * * @param[in,out] oc order to update phase for @@ -748,6 +876,26 @@ reply_with_error (struct OrderContext *oc, /** + * Clean up memory used by @a wmc. + * + * @param[in,out] oc order context the WMC is part of + * @param[in] wmc wire method candidate to free + */ +static void +free_wmc (struct OrderContext *oc, + struct WireMethodCandidate *wmc) +{ + GNUNET_CONTAINER_DLL_remove (oc->add_payment_details.wmc_head, + oc->add_payment_details.wmc_tail, + wmc); + GNUNET_array_grow (wmc->total_exchange_limits, + wmc->num_total_exchange_limits, + 0); + GNUNET_free (wmc); +} + + +/** * Clean up memory used by @a cls. * * @param[in] cls the `struct OrderContext` to clean up @@ -758,6 +906,9 @@ clean_order (void *cls) struct OrderContext *oc = cls; struct RekeyExchange *rx; + while (NULL != oc->add_payment_details.wmc_head) + free_wmc (oc, + oc->add_payment_details.wmc_head); while (NULL != (rx = oc->set_exchanges.pending_reload_head)) { GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, @@ -767,17 +918,19 @@ clean_order (void *cls) GNUNET_free (rx->url); GNUNET_free (rx); } + GNUNET_array_grow (oc->add_payment_details.max_choice_limits, + oc->add_payment_details.num_max_choice_limits, + 0); if (NULL != oc->set_exchanges.wakeup_task) { GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); oc->set_exchanges.wakeup_task = NULL; } - if (NULL != oc->set_exchanges.exchanges) + if (NULL != oc->select_wire_method.exchanges) { - json_decref (oc->set_exchanges.exchanges); - oc->set_exchanges.exchanges = NULL; + json_decref (oc->select_wire_method.exchanges); + oc->select_wire_method.exchanges = NULL; } - GNUNET_free (oc->set_exchanges.total_exchange_limits); switch (oc->parse_order.version) { case TALER_MERCHANT_CONTRACT_VERSION_0: @@ -855,6 +1008,8 @@ clean_order (void *cls) } +/* ***************** ORDER_PHASE_EXECUTE_ORDER **************** */ + /** * Execute the database transaction to setup the order. * @@ -1035,7 +1190,7 @@ execute_transaction (struct OrderContext *oc) * @param[in,out] oc order context */ static void -execute_order (struct OrderContext *oc) +phase_execute_order (struct OrderContext *oc) { const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; @@ -1229,6 +1384,9 @@ execute_order (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_CHECK_CONTRACT **************** */ + + /** * Check that the contract is now well-formed. Upon success, continue * processing with execute_order(). @@ -1236,7 +1394,7 @@ execute_order (struct OrderContext *oc) * @param[in,out] oc order context */ static void -check_contract (struct OrderContext *oc) +phase_check_contract (struct OrderContext *oc) { struct TALER_PrivateContractHashP h_control; @@ -1273,6 +1431,9 @@ check_contract (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_SALT_FORGETTABLE **************** */ + + /** * Modify the final contract terms adding salts for * items that are forgettable. @@ -1280,7 +1441,7 @@ check_contract (struct OrderContext *oc) * @param[in,out] oc order context */ static void -salt_forgettable (struct OrderContext *oc) +phase_salt_forgettable (struct OrderContext *oc) { if (GNUNET_OK != TALER_JSON_contract_seed_forgettable (oc->parse_request.order, @@ -1298,6 +1459,8 @@ salt_forgettable (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_SERIALIZE_ORDER **************** */ + /** * Get rounded time interval. @a start is calculated by rounding * @a ts down to the nearest multiple of @a precision. @@ -1959,7 +2122,7 @@ output_contract_choices (struct OrderContext *oc) * @param[in,out] oc order context */ static void -serialize_order (struct OrderContext *oc) +phase_serialize_order (struct OrderContext *oc) { const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; @@ -2037,9 +2200,9 @@ serialize_order (struct OrderContext *oc) GNUNET_JSON_pack_array_incref ("products", oc->merge_inventory.products), GNUNET_JSON_pack_data_auto ("h_wire", - &oc->add_payment_details.wm->h_wire), + &oc->select_wire_method.wm->h_wire), GNUNET_JSON_pack_string ("wire_method", - oc->add_payment_details.wm->wire_method), + oc->select_wire_method.wm->wire_method), GNUNET_JSON_pack_string ("order_id", oc->parse_order.order_id), GNUNET_JSON_pack_timestamp ("timestamp", @@ -2062,7 +2225,7 @@ serialize_order (struct OrderContext *oc) GNUNET_JSON_pack_data_auto ("merchant_pub", &oc->hc->instance->merchant_pub), GNUNET_JSON_pack_array_incref ("exchanges", - oc->set_exchanges.exchanges), + oc->select_wire_method.exchanges), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("extra", (json_t *) oc->parse_order.extra)) @@ -2129,6 +2292,9 @@ serialize_order (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_SET_MAX_FEE **************** */ + + /** * Set @a max_fee in @a oc based on @a max_stefan_fee value if not overridden * by @a client_fee. If neither is set, set the fee to zero using currency @@ -2178,7 +2344,7 @@ compute_fee (struct OrderContext *oc, * @param[in,out] oc order context */ static void -set_max_fee (struct OrderContext *oc) +phase_set_max_fee (struct OrderContext *oc) { switch (oc->parse_order.version) { @@ -2210,6 +2376,159 @@ set_max_fee (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_SELECT_WIRE_METHOD **************** */ + +/** + * Check that the @a brutto amount is at or below the + * limits we have for the respective wire method candidate. + * + * @param wmc wire method candidate to check + * @param brutto amount to check + * @param true if the amount is OK, false if it is too high + */ +static bool +check_limits (struct WireMethodCandidate *wmc, + const struct TALER_Amount *brutto) +{ + for (unsigned int i = 0; i<wmc->num_total_exchange_limits; i++) + { + const struct TALER_Amount *total_exchange_limit + = &wmc->total_exchange_limits[i]; + + if (GNUNET_OK != + TALER_amount_cmp_currency (brutto, + total_exchange_limit)) + continue; + if (1 != + TALER_amount_cmp (brutto, + total_exchange_limit)) + return true; + } + return false; +} + + +/** + * Phase to select a wire method that will be acceptable for the order. + * If none is "perfect" (allows all choices), might jump back to the + * previous phase to force "/keys" downloads to see if that helps. + * + * @param[in,out] oc order context + */ +static void +phase_select_wire_method (struct OrderContext *oc) +{ + const struct TALER_Amount *ea; + struct WireMethodCandidate *best = NULL; + unsigned int max_choices = 0; + unsigned int want_choices; + + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) + { + unsigned int num_choices = 0; + + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + want_choices = 1; + ea = &oc->parse_order.details.v0.brutto; + if (TALER_amount_is_zero (ea) || + check_limits (wmc, + ea)) + num_choices++; + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + want_choices = oc->parse_choices.choices_len; + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + ea = &oc->parse_choices.choices[i].amount; + if (TALER_amount_is_zero (ea) || + check_limits (wmc, + ea)) + num_choices++; + } + break; + default: + GNUNET_assert (0); + } + if (num_choices > max_choices) + { + best = wmc; + max_choices = num_choices; + } + } + + if ( (want_choices > max_choices) && + (! oc->set_exchanges.forced_reload) ) + { + /* Not all choices in the contract can work with these + exchanges, try again with forcing /keys download */ + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) + { + json_array_clear (wmc->exchanges); + GNUNET_array_grow (wmc->total_exchange_limits, + wmc->num_total_exchange_limits, + 0); + } + oc->phase = ORDER_PHASE_SET_EXCHANGES; + return; + } + + if ( (NULL == best) && + (NULL != oc->parse_request.payment_target) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot create order: lacking suitable exchanges for payment target `%s'\n", + oc->parse_request.payment_target); + reply_with_error ( + oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD, + oc->parse_request.payment_target); + return; + } + + if (NULL == best) + { + /* We actually do not have ANY workable exchange(s) */ + reply_with_error ( + oc, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS, + NULL); + return; + } + + if (want_choices > max_choices) + { + /* Some choices are unpayable */ + GNUNET_log ( + GNUNET_ERROR_TYPE_WARNING, + "Creating order, but some choices do not work with the selected wire method\n"); + } + if ( (0 == json_array_size (best->exchanges)) && + (oc->add_payment_details.need_exchange) ) + { + /* We did not find any reasonable exchange */ + GNUNET_log ( + GNUNET_ERROR_TYPE_WARNING, + "Creating order, but only for choices without payment\n"); + } + + oc->select_wire_method.wm + = best->wm; + oc->select_wire_method.exchanges + = json_incref (best->exchanges); + oc->phase++; +} + + +/* ***************** ORDER_PHASE_SET_EXCHANGES **************** */ + /** * Exchange `/keys` processing is done, resume handling * the order. @@ -2220,8 +2539,7 @@ static void resume_with_keys (struct OrderContext *oc) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming order processing after /keys downloads (now have %u accounts)\n", - (unsigned int) json_array_size (oc->set_exchanges.exchanges)); + "Resuming order processing after /keys downloads\n"); GNUNET_assert (GNUNET_YES == oc->suspended); GNUNET_CONTAINER_DLL_remove (oc_head, oc_tail, @@ -2321,10 +2639,12 @@ update_stefan (struct OrderContext *oc, * too low and we failed to create an order. * * @param oc order context + * @param wmc wire method candidate to notify for * @param exchange_url exchange to notify about */ static void notify_kyc_required (const struct OrderContext *oc, + const struct WireMethodCandidate *wmc, const char *exchange_url) { struct GNUNET_DB_EventHeaderP es = { @@ -2335,8 +2655,8 @@ notify_kyc_required (const struct OrderContext *oc, char *extra; hws = GNUNET_STRINGS_data_to_string_alloc ( - &oc->add_payment_details.wm->h_wire, - sizeof (oc->add_payment_details.wm->h_wire)); + &wmc->wm->h_wire, + sizeof (wmc->wm->h_wire)); GNUNET_asprintf (&extra, "%s %s", @@ -2352,35 +2672,54 @@ notify_kyc_required (const struct OrderContext *oc, /** - * Compute the set of exchanges that would be acceptable - * for this order. + * Checks the limits that apply for this @a exchange and + * the @a wmc and if the exchange is acceptable at all, adds it + * to the list of exchanges for the @a wmc. * - * @param cls our `struct OrderContext` - * @param url base URL of an exchange (not used) + * @param oc context of the order * @param exchange internal handle for the exchange - * @param max_needed maximum amount needed in this currency + * @param exchange_url base URL of this exchange + * @param wmc wire method to evaluate this exchange for + * @return true if the exchange is acceptable for the contract */ -static void -get_acceptable (void *cls, - const char *url, +static bool +get_acceptable (struct OrderContext *oc, const struct TMH_Exchange *exchange, - const struct TALER_Amount *max_needed) + const char *exchange_url, + struct WireMethodCandidate *wmc) { - struct OrderContext *oc = cls; + const struct TALER_Amount *max_needed = NULL; unsigned int priority = 42; /* make compiler happy */ json_t *j_exchange; enum GNUNET_GenericReturnValue res; struct TALER_Amount max_amount; + for (unsigned int i = 0; i<oc->add_payment_details.num_max_choice_limits; i++) + { + struct TALER_Amount *val = &oc->add_payment_details.max_choice_limits[i]; + + if (0 == strcasecmp (val->currency, + TMH_EXCHANGES_get_currency (exchange))) + { + max_needed = val; + break; + } + } + if (NULL == max_needed) + { + /* exchange currency not relevant for any of our choices, skip it */ + return false; + } + max_amount = *max_needed; res = TMH_exchange_check_debit ( oc->hc->instance->settings.id, exchange, - oc->add_payment_details.wm, + wmc->wm, &max_amount); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s evaluated at %d with max %s\n", - url, + exchange_url, res, TALER_amount2s (&max_amount)); if (TALER_amount_is_zero (&max_amount)) @@ -2390,20 +2729,20 @@ get_acceptable (void *cls, /* Trigger re-checking the current deposit limit when * paying non-zero amount with zero deposit limit */ notify_kyc_required (oc, - url); + wmc, + exchange_url); } /* If deposit is impossible, we don't list the * exchange in the contract terms. */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is zero, skipping it\n", - url); - return; + exchange_url); + return false; } switch (res) { case GNUNET_OK: priority = 1024; /* high */ - oc->set_exchanges.exchange_good = true; break; case GNUNET_NO: if (oc->set_exchanges.forced_reload) @@ -2420,47 +2759,12 @@ get_acceptable (void *cls, } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is %s, adding it!\n", - url, + exchange_url, TALER_amount2s (&max_amount)); - { - bool found = false; - for (unsigned int i = 0; i<oc->set_exchanges.num_total_exchange_limits; i++) - { - struct TALER_Amount *limit - = &oc->set_exchanges.total_exchange_limits[i]; - - if (GNUNET_OK == - TALER_amount_cmp_currency (limit, - &max_amount)) - { - GNUNET_assert (0 <= - TALER_amount_add (limit, - limit, - &max_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_min (limit, - limit, - max_needed)); - found = true; - } - } - if (! found) - { - struct TALER_Amount limit; - - GNUNET_assert (GNUNET_OK == - TALER_amount_min (&limit, - &max_amount, - max_needed)); - GNUNET_array_append (oc->set_exchanges.total_exchange_limits, - oc->set_exchanges.num_total_exchange_limits, - limit); - } - } j_exchange = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("url", - url), + exchange_url), GNUNET_JSON_pack_uint64 ("priority", priority), TALER_JSON_pack_amount ("max_contribution", @@ -2468,9 +2772,16 @@ get_acceptable (void *cls, GNUNET_JSON_pack_data_auto ("master_pub", TMH_EXCHANGES_get_master_pub (exchange))); GNUNET_assert (NULL != j_exchange); + /* Add exchange to list of exchanges for this wire method + candidate */ GNUNET_assert (0 == - json_array_append_new (oc->set_exchanges.exchanges, + json_array_append_new (wmc->exchanges, j_exchange)); + add_to_currency_vector (&wmc->total_exchange_limits, + &wmc->num_total_exchange_limits, + &max_amount, + max_needed); + return true; } @@ -2492,6 +2803,7 @@ keys_cb ( struct OrderContext *oc = rx->oc; const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; + bool applicable = false; rx->fo = NULL; GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, @@ -2502,63 +2814,38 @@ keys_cb ( GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to download %skeys\n", rx->url); + goto cleanup; } - else + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got response for %skeys\n", + rx->url); + + /* Evaluate the use of this exchange for each wire method candidate */ + for (unsigned int j = 0; j<keys->accounts_len; j++) { - bool currency_ok = false; - struct TALER_Amount max_needed; + struct TALER_FullPayto full_payto = keys->accounts[j].fpayto_uri; + char *wire_method = TALER_payto_get_method (full_payto.full_payto); - switch (oc->parse_order.version) + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) { - case TALER_MERCHANT_CONTRACT_VERSION_0: - if (0 == strcasecmp (keys->currency, - oc->parse_order.details.v0.brutto.currency)) + if (0 == strcmp (wmc->wm->wire_method, + wire_method) ) { - max_needed = oc->parse_order.details.v0.brutto; - currency_ok = true; + applicable |= get_acceptable (oc, + exchange, + rx->url, + wmc); } - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - { - const struct TALER_Amount *amount - = &oc->parse_choices.choices[i].amount; - - if (0 == strcasecmp (keys->currency, - amount->currency)) - { - if (currency_ok) - { - TALER_amount_max (&max_needed, - &max_needed, - amount); - } - else - { - max_needed = *amount; - currency_ok = true; - } - } - } - break; - default: - GNUNET_assert (0); - } - if ( (currency_ok) && - (! TALER_amount_is_zero (&max_needed)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got response for %skeys\n", - rx->url); - if (settings->use_stefan) - update_stefan (oc, - keys); - get_acceptable (oc, - rx->url, - exchange, - &max_needed); } + GNUNET_free (wire_method); } + if (applicable && + settings->use_stefan) + update_stefan (oc, + keys); +cleanup: GNUNET_free (rx->url); GNUNET_free (rx); if (NULL != oc->set_exchanges.pending_reload_head) @@ -2624,102 +2911,49 @@ wakeup_timeout (void *cls) /** - * Check that the @a brutto amount is at or below the exchange - * limits we have for the respective currency. - * - * @param oc order context to check - * @param brutto amount to check - * @return true if the amount is OK, false if it is too high - */ -static bool -check_exchange_limits (const struct OrderContext *oc, - struct TALER_Amount *brutto) -{ - for (unsigned int i = 0; i<oc->set_exchanges.num_total_exchange_limits; i++) - { - const struct TALER_Amount *total_exchange_limit - = &oc->set_exchanges.total_exchange_limits[i]; - - if (GNUNET_OK != - TALER_amount_cmp_currency (brutto, - total_exchange_limit)) - continue; - if (1 != - TALER_amount_cmp (brutto, - total_exchange_limit)) - return true; - } - - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot create order: %s is above the sum of hard limits from supported exchanges\n", - TALER_amount2s (brutto)); - return false; -} - - -/** - * Set list of acceptable exchanges in @a oc. Upon success, continue - * processing with set_max_fee(). + * Set list of acceptable exchanges in @a oc. Upon success, continues + * processing with add_payment_details(). * * @param[in,out] oc order context * @return true to suspend execution */ static bool -set_exchanges (struct OrderContext *oc) +phase_set_exchanges (struct OrderContext *oc) { - bool need_exchange; - if (NULL != oc->set_exchanges.wakeup_task) { GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); oc->set_exchanges.wakeup_task = NULL; } - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - need_exchange = ! TALER_amount_is_zero ( - &oc->parse_order.details.v0.brutto); - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - need_exchange = false; - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - if (! TALER_amount_is_zero (&oc->parse_choices.choices[i].amount)) - { - need_exchange = true; - break; - } - break; - default: - GNUNET_assert (0); - } - if (! need_exchange) + + if (! oc->add_payment_details.need_exchange) { /* Total amount is zero, so we don't actually need exchanges! */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order total is zero, no need for exchanges\n"); - oc->set_exchanges.exchanges = json_array (); - GNUNET_assert (NULL != oc->set_exchanges.exchanges); - oc->phase++; + oc->select_wire_method.exchanges = json_array (); + GNUNET_assert (NULL != oc->select_wire_method.exchanges); + /* Pick first one, doesn't matter as the amount is zero */ + oc->select_wire_method.wm = oc->hc->instance->wm_head; + oc->phase = ORDER_PHASE_SET_MAX_FEE; return false; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Trying to find exchanges\n"); - if (NULL == oc->set_exchanges.exchanges) + if (NULL == oc->set_exchanges.pending_reload_head) { - oc->set_exchanges.keys_timeout - = GNUNET_TIME_relative_to_absolute (MAX_KEYS_WAIT); - oc->set_exchanges.exchanges = json_array (); - GNUNET_assert (NULL != oc->set_exchanges.exchanges); - TMH_exchange_get_trusted (&get_exchange_keys, - oc); - } - else if (! oc->set_exchanges.exchange_good) - { - if (! oc->set_exchanges.forced_reload) + if (! oc->set_exchanges.exchanges_tried) { + oc->set_exchanges.exchanges_tried = true; + oc->set_exchanges.keys_timeout + = GNUNET_TIME_relative_to_absolute (MAX_KEYS_WAIT); + TMH_exchange_get_trusted (&get_exchange_keys, + oc); + } + else if (! oc->set_exchanges.forced_reload) + { + /* Try one more time with forcing /keys download */ oc->set_exchanges.forced_reload = true; - GNUNET_assert (0 == - json_array_clear (oc->set_exchanges.exchanges)); TMH_exchange_get_trusted (&get_exchange_keys, oc); } @@ -2754,67 +2988,476 @@ set_exchanges (struct OrderContext *oc) oc); return true; /* reloads pending */ } - if (0 == json_array_size (oc->set_exchanges.exchanges)) + oc->phase++; + return false; +} + + +/* ***************** ORDER_PHASE_ADD_PAYMENT_DETAILS **************** */ + +/** + * Process the @a payment_target and add the details of how the + * order could be paid to @a order. On success, continue + * processing with add_payment_fees(). + * + * @param[in,out] oc order context + */ +static void +phase_add_payment_details (struct OrderContext *oc) +{ + /* First, determine the maximum amounts that could be paid per currency */ + switch (oc->parse_order.version) { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot create order: lacking trusted exchanges\n"); - reply_with_error ( - oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD, - oc->add_payment_details.wm->wire_method); - return false; + case TALER_MERCHANT_CONTRACT_VERSION_0: + GNUNET_array_append (oc->add_payment_details.max_choice_limits, + oc->add_payment_details.num_max_choice_limits, + oc->parse_order.details.v0.brutto); + if (! TALER_amount_is_zero ( + &oc->parse_order.details.v0.brutto)) + { + oc->add_payment_details.need_exchange = true; + } + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + const struct TALER_Amount *amount + = &oc->parse_choices.choices[i].amount; + bool found = false; + + if (! TALER_amount_is_zero (amount)) + { + oc->add_payment_details.need_exchange = true; + } + for (unsigned int j = 0; j<oc->add_payment_details.num_max_choice_limits; + j++) + { + struct TALER_Amount *mx = &oc->add_payment_details.max_choice_limits[j]; + if (GNUNET_YES == + TALER_amount_cmp_currency (mx, + amount)) + { + TALER_amount_max (mx, + mx, + amount); + found = true; + break; + } + } + if (! found) + { + GNUNET_array_append (oc->add_payment_details.max_choice_limits, + oc->add_payment_details.num_max_choice_limits, + *amount); + } + } + break; + default: + GNUNET_assert (0); } + /* Then, create a candidate for each available wire method */ + for (struct TMH_WireMethod *wm = oc->hc->instance->wm_head; + NULL != wm; + wm = wm->next) { - bool ok; - struct TALER_Amount ea; + struct WireMethodCandidate *wmc; + + /* Locate wire method that has a matching payment target */ + if (! wm->active) + continue; /* ignore inactive methods */ + if ( (NULL != oc->parse_request.payment_target) && + (0 != strcasecmp (oc->parse_request.payment_target, + wm->wire_method) ) ) + continue; /* honor client preference */ + wmc = GNUNET_new (struct WireMethodCandidate); + wmc->wm = wm; + wmc->exchanges = json_array (); + GNUNET_assert (NULL != wmc->exchanges); + GNUNET_CONTAINER_DLL_insert (oc->add_payment_details.wmc_head, + oc->add_payment_details.wmc_tail, + wmc); + } - switch (oc->parse_order.version) + if (NULL == oc->add_payment_details.wmc_head) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No wire method available for instance '%s'\n", + oc->hc->instance->settings.id); + reply_with_error (oc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE, + oc->parse_request.payment_target); + return; + } + + /* next, we'll evaluate available exchanges */ + oc->phase++; +} + + +/* ***************** ORDER_PHASE_MERGE_INVENTORY **************** */ + + +/** + * Merge the inventory products into products, querying the + * database about the details of those products. Upon success, + * continue processing by calling add_payment_details(). + * + * @param[in,out] oc order context to process + */ +static void +phase_merge_inventory (struct OrderContext *oc) +{ + /** + * parse_request.inventory_products => instructions to add products to contract terms + * parse_order.products => contains products that are not from the backend-managed inventory. + */ + if (NULL != oc->parse_order.products) + oc->merge_inventory.products + = json_deep_copy (oc->parse_order.products); + else + oc->merge_inventory.products + = json_array (); + /* Populate products from inventory product array and database */ + { + GNUNET_assert (NULL != oc->merge_inventory.products); + for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) { - case TALER_MERCHANT_CONTRACT_VERSION_0: - ea = oc->parse_order.details.v0.brutto; - ok = check_exchange_limits (oc, - &ea); - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - ok = true; - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + const struct InventoryProduct *ip + = &oc->parse_request.inventory_products[i]; + struct TALER_MERCHANTDB_ProductDetails pd; + enum GNUNET_DB_QueryStatus qs; + size_t num_categories = 0; + uint64_t *categories = NULL; + + qs = TMH_db->lookup_product (TMH_db->cls, + oc->hc->instance->settings.id, + ip->product_id, + &pd, + &num_categories, + &categories); + if (qs <= 0) { - ea = oc->parse_choices.choices[i].amount; - if (! check_exchange_limits (oc, - &ea)) + enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + unsigned int http_status = 0; + + switch (qs) { - ok = false; + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_FETCH_FAILED; break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Product %s from order unknown\n", + ip->product_id); + http_status = MHD_HTTP_NOT_FOUND; + ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* case listed to make compilers happy */ + GNUNET_assert (0); } + reply_with_error (oc, + http_status, + ec, + ip->product_id); + return; } - break; - default: - GNUNET_assert (0); - } + GNUNET_free (categories); + oc->parse_order.minimum_age + = GNUNET_MAX (oc->parse_order.minimum_age, + pd.minimum_age); + { + json_t *p; - if (! ok) - { - reply_with_error ( - oc, - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS, - TALER_amount2s (&ea)); - return false; + p = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_name", + pd.product_name), + GNUNET_JSON_pack_string ("description", + pd.description), + GNUNET_JSON_pack_object_steal ("description_i18n", + pd.description_i18n), + GNUNET_JSON_pack_string ("unit", + pd.unit), + TALER_JSON_pack_amount ("price", + &pd.price), + GNUNET_JSON_pack_array_steal ("taxes", + pd.taxes), + GNUNET_JSON_pack_string ("image", + pd.image), + GNUNET_JSON_pack_uint64 ( + "quantity", + ip->quantity)); + GNUNET_assert (NULL != p); + GNUNET_assert (0 == + json_array_append_new (oc->merge_inventory.products, + p)); + } + GNUNET_free (pd.description); + GNUNET_free (pd.unit); + GNUNET_free (pd.image); + json_decref (pd.address); } } + /* check if final product list is well-formed */ + if (! TMH_products_array_valid (oc->merge_inventory.products)) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order:products"); + return; + } + oc->phase++; +} + + +/* ***************** ORDER_PHASE_PARSE_CHOICES **************** */ + +/** + * Parse contract choices. Upon success, continue + * processing with merge_inventory(). + * + * @param[in,out] oc order context + */ +static void +phase_parse_choices (struct OrderContext *oc) +{ + const json_t *jchoices; + + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + oc->phase++; + return; + case TALER_MERCHANT_CONTRACT_VERSION_1: + /* handle below */ + break; + default: + GNUNET_assert (0); + } + + jchoices = oc->parse_order.details.v1.choices; - if (! oc->set_exchanges.exchange_good) + if (! json_is_array (jchoices)) + GNUNET_assert (0); + if (0 == json_array_size (jchoices)) { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Creating order, but possibly without usable trusted exchanges\n"); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "choices"); + return; + } + GNUNET_array_grow (oc->parse_choices.choices, + oc->parse_choices.choices_len, + json_array_size (jchoices)); + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + struct TALER_MERCHANT_ContractChoice *choice + = &oc->parse_choices.choices[i]; + const char *error_name; + unsigned int error_line; + const json_t *jinputs; + const json_t *joutputs; + bool no_fee; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("amount", + &choice->amount), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("max_fee", + &choice->max_fee), + &no_fee), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("inputs", + &jinputs), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("outputs", + &joutputs), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + + ret = GNUNET_JSON_parse (json_array_get (jchoices, + i), + spec, + &error_name, + &error_line); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Choice parsing failed: %s:%u\n", + error_name, + error_line); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "choice"); + return; + } + if ( (! no_fee) && + (GNUNET_OK != + TALER_amount_cmp_currency (&choice->amount, + &choice->max_fee)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "different currencies used for 'max_fee' and 'amount' currency"); + return; + } + + if (! TMH_test_exchange_configured_for_currency ( + choice->amount.currency)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, + choice->amount.currency); + return; + } + + if (NULL != jinputs) + { + const json_t *jinput; + size_t idx; + json_array_foreach ((json_t *) jinputs, idx, jinput) + { + struct TALER_MERCHANT_ContractInput input = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_choice_input ((json_t *) jinput, + &input, + idx, + true)) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "input"); + return; + } + + switch (input.type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == input.details.token.count) + continue; + + if (GNUNET_OK != + add_input_token_family (oc, + input.details.token.token_family_slug)) + + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, + input.details.token.token_family_slug); + return; + } + + GNUNET_array_append (choice->inputs, + choice->inputs_len, + input); + continue; + } + GNUNET_assert (0); + } + } + + if (NULL != joutputs) + { + const json_t *joutput; + size_t idx; + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MERCHANT_ContractOutput output = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_choice_output ((json_t *) joutput, + &output, + idx, + true)) + { + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "output"); + return; + } + + switch (output.type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + GNUNET_break (0); /* FIXME-#9059: not yet implemented! */ + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == output.details.token.count) + continue; + + if (0 == output.details.token.valid_at.abs_time.abs_value_us) + output.details.token.valid_at + = GNUNET_TIME_timestamp_get (); + if (GNUNET_OK != + add_output_token_family (oc, + output.details.token.token_family_slug, + output.details.token.valid_at, + &output.details.token.key_index)) + + { + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, + output.details.token.token_family_slug); + return; + } + + GNUNET_array_append (choice->outputs, + choice->outputs_len, + output); + continue; + } + GNUNET_assert (0); + } + } } oc->phase++; - return false; } +/* ***************** ORDER_PHASE_PARSE_ORDER **************** */ + + /** * Parse the order field of the request. Upon success, continue * processing with parse_choices(). @@ -2822,7 +3465,7 @@ set_exchanges (struct OrderContext *oc) * @param[in,out] oc order context */ static void -parse_order (struct OrderContext *oc) +phase_parse_order (struct OrderContext *oc) { const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; @@ -3337,7 +3980,6 @@ parse_order (struct OrderContext *oc) #ifdef HAVE_DONAU_DONAU_SERVICE_H - /** * Callback function that is called for each donau instance. * It simply adds the provided donau_url to the json. @@ -3389,6 +4031,7 @@ parse_donau_instances (struct OrderContext *oc, #endif + /** * Parse contract choices. Upon success, continue * processing with merge_inventory(). @@ -3554,8 +4197,9 @@ parse_choices (struct OrderContext *oc) size_t idx; json_array_foreach ((json_t *) joutputs, idx, joutput) { - - struct TALER_MERCHANT_ContractOutput output = {}; + struct TALER_MERCHANT_ContractOutput output = { + .details.token.count = 1 + }; if (GNUNET_OK != TALER_MERCHANT_parse_choice_output ((json_t *) joutput, @@ -3800,6 +4444,8 @@ merge_inventory (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_PARSE_REQUEST **************** */ + /** * Parse the client request. Upon success, * continue processing by calling parse_order(). @@ -3807,7 +4453,7 @@ merge_inventory (struct OrderContext *oc) * @param[in,out] oc order context to process */ static void -parse_request (struct OrderContext *oc) +phase_parse_request (struct OrderContext *oc) { const json_t *ip = NULL; const json_t *uuid = NULL; @@ -4020,6 +4666,9 @@ parse_request (struct OrderContext *oc) } +/* ***************** Main handler **************** */ + + MHD_RESULT TMH_private_post_orders ( const struct TMH_RequestHandler *rh, @@ -4044,38 +4693,41 @@ TMH_private_post_orders ( switch (oc->phase) { case ORDER_PHASE_PARSE_REQUEST: - parse_request (oc); + phase_parse_request (oc); break; case ORDER_PHASE_PARSE_ORDER: - parse_order (oc); + phase_parse_order (oc); break; case ORDER_PHASE_PARSE_CHOICES: - parse_choices (oc); + phase_parse_choices (oc); break; case ORDER_PHASE_MERGE_INVENTORY: - merge_inventory (oc); + phase_merge_inventory (oc); break; case ORDER_PHASE_ADD_PAYMENT_DETAILS: - add_payment_details (oc); + phase_add_payment_details (oc); break; case ORDER_PHASE_SET_EXCHANGES: - if (set_exchanges (oc)) + if (phase_set_exchanges (oc)) return MHD_YES; break; + case ORDER_PHASE_SELECT_WIRE_METHOD: + phase_select_wire_method (oc); + break; case ORDER_PHASE_SET_MAX_FEE: - set_max_fee (oc); + phase_set_max_fee (oc); break; case ORDER_PHASE_SERIALIZE_ORDER: - serialize_order (oc); + phase_serialize_order (oc); break; case ORDER_PHASE_CHECK_CONTRACT: - check_contract (oc); + phase_check_contract (oc); break; case ORDER_PHASE_SALT_FORGETTABLE: - salt_forgettable (oc); + phase_salt_forgettable (oc); break; case ORDER_PHASE_EXECUTE_ORDER: - execute_order (oc); + phase_execute_order (oc); break; case ORDER_PHASE_FINISHED_MHD_YES: GNUNET_log (GNUNET_ERROR_TYPE_INFO, diff --git a/src/backenddb/merchant-0020.sql b/src/backenddb/merchant-0020.sql @@ -26,6 +26,9 @@ SELECT _v.register_patch('merchant-0020', NULL, NULL); SET search_path TO merchant; +-- delete existing login tokens as we don't have a description, this +-- logs out all users but should be safe as a migration. +DELETE FROM merchant_login_tokens; ALTER TABLE merchant_login_tokens ADD description TEXT NOT NULL; diff --git a/src/backenddb/pg_lookup_statistics_counter_by_interval.c b/src/backenddb/pg_lookup_statistics_counter_by_interval.c @@ -155,7 +155,7 @@ TMH_PG_lookup_statistics_counter_by_interval ( struct LookupCounterStatisticsContext context = { .cb = cb, .cb_cls = cb_cls, - /* Can be overwritten by the lookup_token_families_cb */ + /* Can be overwritten by the lookup_statistics_counter_by_interval_cb */ .extract_failed = false, .description = NULL };