merchant

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

commit a5919709416909e9c193489cfca56013fa3958f2
parent fe8d74937835ffa0c29ea13838b78839008578ba
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Fri, 23 Jan 2026 11:04:54 +0900

implement better error reporting for #10907

Diffstat:
Msrc/backend/taler-merchant-httpd_private-post-orders.c | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/backend/taler-merchant-kyccheck.c | 20++++++++++++++++++++
2 files changed, 147 insertions(+), 48 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -78,7 +78,7 @@ * refuses a forced download. */ #define MAX_KEYS_WAIT \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500) + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500) /** * Generate the base URL for the given merchant instance. @@ -585,6 +585,12 @@ struct OrderContext struct GNUNET_SCHEDULER_Task *wakeup_task; /** + * Array of reasons why a particular exchange may be + * limited or not be eligible. + */ + json_t *exchange_rejections; + + /** * Did we previously force reloading of /keys from * all exchanges? Set to 'true' to prevent us from * doing it again (and again...). @@ -985,6 +991,11 @@ clean_order (void *cls) json_decref (oc->select_wire_method.exchanges); oc->select_wire_method.exchanges = NULL; } + if (NULL != oc->set_exchanges.exchange_rejections) + { + json_decref (oc->set_exchanges.exchange_rejections); + oc->set_exchanges.exchange_rejections = NULL; + } switch (oc->parse_order.version) { case TALER_MERCHANT_CONTRACT_VERSION_0: @@ -1297,6 +1308,34 @@ execute_transaction (struct OrderContext *oc) /** + * The request was successful, generate the #MHD_HTTP_OK response. + * + * @param[in,out] oc context to update + * @param claim_token claim token to use, NULL if none + */ +static void +yield_success_response (struct OrderContext *oc, + const struct TALER_ClaimTokenP *claim_token) +{ + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + oc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("order_id", + oc->parse_order.order_id), + GNUNET_JSON_pack_timestamp ("pay_deadline", + oc->parse_order.pay_deadline), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ( + "token", + claim_token))); + finalize_order (oc, + ret); +} + + +/** * Transform an order into a proposal and store it in the * database. Write the resulting proposal or an error message * of a MHD connection. @@ -1357,25 +1396,10 @@ phase_execute_order (struct OrderContext *oc) /* DB transaction succeeded, check for idempotent */ if (oc->execute_order.idempotent) { - // FIXME: duplicated code... - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - oc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("order_id", - oc->parse_order.order_id), - GNUNET_JSON_pack_timestamp ("pay_deadline", - oc->parse_order.pay_deadline), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_data_varsize ( - "token", - GNUNET_is_zero (&oc->execute_order.token) - ? NULL - : &oc->execute_order.token, - sizeof (oc->execute_order.token)))); - finalize_order (oc, - ret); + yield_success_response (oc, + GNUNET_is_zero (&oc->execute_order.token) + ? NULL + : &oc->execute_order.token); return; } if (oc->execute_order.conflict) @@ -1505,28 +1529,10 @@ phase_execute_order (struct OrderContext *oc) /* Everything in-stock, generate positive response */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order creation succeeded\n"); - - { - // FIXME: duplicated code... - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - oc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("order_id", - oc->parse_order.order_id), - GNUNET_JSON_pack_timestamp ("pay_deadline", - oc->parse_order.pay_deadline), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_data_varsize ( - "token", - GNUNET_is_zero (&oc->parse_request.claim_token) - ? NULL - : &oc->parse_request.claim_token, - sizeof (oc->parse_request.claim_token)))); - finalize_order (oc, - ret); - } + yield_success_response (oc, + GNUNET_is_zero (&oc->parse_request.claim_token) + ? NULL + : &oc->parse_request.claim_token); } @@ -2647,12 +2653,20 @@ phase_select_wire_method (struct OrderContext *oc) if (NULL == best) { + MHD_RESULT mret; + /* 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); + mret = TALER_MHD_reply_json_steal ( + oc->connection, + GNUNET_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS), + GNUNET_JSON_pack_array_steal ( + "exchange_rejections", + oc->set_exchanges.exchange_rejections)), + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS); + finalize_order (oc, + mret); return; } @@ -2825,6 +2839,34 @@ notify_kyc_required (const struct OrderContext *oc, /** + * Add a reason why a particular exchange was rejected to our + * response data. + * + * @param[in,out] oc order context to update + * @param exchange_url exchange this is about + * @param ec error code to set for the exchange + */ +static void +add_rejection (struct OrderContext *oc, + const char *exchange_url, + enum TALER_ErrorCode ec) +{ + if (NULL == oc->set_exchanges.exchange_rejections) + { + oc->set_exchanges.exchange_rejections = json_array (); + GNUNET_assert (NULL != oc->set_exchanges.exchange_rejections); + } + GNUNET_assert (0 == + json_array_append_new ( + oc->set_exchanges.exchange_rejections, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("exchange_url", + exchange_url), + TALER_JSON_pack_ec (ec)))); +} + + +/** * 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. @@ -2847,7 +2889,9 @@ get_acceptable (struct OrderContext *oc, enum TMH_ExchangeStatus res; struct TALER_Amount max_amount; - for (unsigned int i = 0; i<oc->add_payment_details.num_max_choice_limits; i++) + 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]; @@ -2861,6 +2905,13 @@ get_acceptable (struct OrderContext *oc, if (NULL == max_needed) { /* exchange currency not relevant for any of our choices, skip it */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Exchange %s with currency `%s' is not applicable to this order\n", + exchange_url, + TMH_EXCHANGES_get_currency (exchange)); + add_rejection (oc, + exchange_url, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH); return false; } @@ -2890,6 +2941,9 @@ get_acceptable (struct OrderContext *oc, GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is zero, skipping it\n", exchange_url); + add_rejection (oc, + exchange_url, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LEGALLY_REFUSED); return false; } switch (res) @@ -3004,6 +3058,9 @@ keys_cb ( "Failed to download %skeys\n", rx->url); oc->set_exchanges.promising_exchange = true; + add_rejection (oc, + rx->url, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE); goto cleanup; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -3016,10 +3073,17 @@ keys_cb ( struct TALER_FullPayto full_payto = keys->accounts[j].fpayto_uri; char *wire_method = TALER_payto_get_method (full_payto.full_payto); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Exchange `%s' has wire method `%s'\n", + rx->url, + wire_method); for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; NULL != wmc; wmc = wmc->next) { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Order could use wire method `%s'\n", + wmc->wm->wire_method); if (0 == strcmp (wmc->wm->wire_method, wire_method) ) { @@ -3031,6 +3095,21 @@ keys_cb ( } GNUNET_free (wire_method); } + if ( (! applicable) && + (! oc->set_exchanges.forced_reload) ) + { + /* Checks for 'forced_reload' to not log the error *again* + if we forced a re-load and are encountering the + applicability error a 2nd time */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Exchange `%s' %u wire methods are not applicable to this order\n", + rx->url, + keys->accounts_len); + add_rejection (oc, + rx->url, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED) + ; + } if (applicable && settings->use_stefan) update_stefan (oc, diff --git a/src/backend/taler-merchant-kyccheck.c b/src/backend/taler-merchant-kyccheck.c @@ -1089,6 +1089,8 @@ find_accounts (void *cls) if (qs < 0) { GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); return; } for (struct Account *a = a_head; @@ -1128,6 +1130,8 @@ account_changed (void *cls, (void) extra_size; if (NULL != account_task) return; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received account change notification: reloading accounts\n"); account_task = GNUNET_SCHEDULER_add_now (&find_accounts, NULL); @@ -1155,6 +1159,8 @@ find_keys (const char *exchange_url) if (qs < 0) { GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) @@ -1232,11 +1238,15 @@ keys_changed (void *cls, (0 == extra_size) ) { GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); return; } if ('\0' != url[extra_size - 1]) { GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -1270,11 +1280,15 @@ rule_triggered (void *cls, (0 == extra_size) ) { GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); return; } if ('\0' != text[extra_size - 1]) { GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); return; } space = memchr (extra, @@ -1283,6 +1297,8 @@ rule_triggered (void *cls, if (NULL == space) { GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != @@ -1292,12 +1308,16 @@ rule_triggered (void *cls, sizeof (h_wire))) { GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); return; } exchange_url = &space[1]; if (! TALER_is_web_url (exchange_url)) { GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); return; }