merchant

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

commit 0edb2ad9afbe76d2c738138278a94630878d20a0
parent 7b365a122fdffec83c2cf7bd969bd3263c387e59
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun, 29 Mar 2026 20:52:38 +0200

fix a few minor API inconsistencies, especially in the C APIs

Diffstat:
Msrc/backend/taler-merchant-httpd_get-private-reports-REPORT_ID.c | 5++++-
Msrc/include/taler/taler-merchant/get-orders-ORDER_ID.h | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/include/taler/taler-merchant/post-orders-ORDER_ID-abort.h | 40++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler/taler-merchant/post-orders-ORDER_ID-pay.h | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/lib/merchant_api_get-orders-ORDER_ID.c | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/lib/merchant_api_post-orders-ORDER_ID-abort.c | 34++++++++++++++++++++++++++++------
Msrc/lib/merchant_api_post-orders-ORDER_ID-pay.c | 24++++++++++++++++++++++--
Msrc/testing/testing_api_cmd_pay_order.c | 8+++++++-
Msrc/testing/testing_api_cmd_wallet_get_order.c | 15+++++++++------
9 files changed, 328 insertions(+), 30 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_get-private-reports-REPORT_ID.c b/src/backend/taler-merchant-httpd_get-private-reports-REPORT_ID.c @@ -114,7 +114,10 @@ TMH_private_get_report (const struct TMH_RequestHandler *rh, GNUNET_JSON_pack_time_rel ("report_frequency", frequency), GNUNET_JSON_pack_time_rel ("report_frequency_shift", - frequency_shift)); + frequency_shift), + GNUNET_JSON_pack_timestamp ( + "next_transmission", + GNUNET_TIME_absolute_to_timestamp (next_transmission))); GNUNET_free (report_program_section); GNUNET_free (report_description); GNUNET_free (mime_type); diff --git a/src/include/taler/taler-merchant/get-orders-ORDER_ID.h b/src/include/taler/taler-merchant/get-orders-ORDER_ID.h @@ -53,7 +53,22 @@ enum TALER_MERCHANT_GetOrdersOption /** * If true, wait until refund is confirmed obtained. */ - TALER_MERCHANT_GET_ORDERS_OPTION_AWAIT_REFUND_OBTAINED + TALER_MERCHANT_GET_ORDERS_OPTION_AWAIT_REFUND_OBTAINED, + + /** + * Hash of the contract terms (for authentication). + */ + TALER_MERCHANT_GET_ORDERS_OPTION_H_CONTRACT, + + /** + * Claim token for unclaimed order authentication. + */ + TALER_MERCHANT_GET_ORDERS_OPTION_TOKEN, + + /** + * Whether refunded orders count for repurchase detection. + */ + TALER_MERCHANT_GET_ORDERS_OPTION_ALLOW_REFUNDED_FOR_REPURCHASE }; @@ -99,6 +114,24 @@ struct TALER_MERCHANT_GetOrdersOptionValue */ bool await_refund_obtained; + /** + * Value if @e option is + * #TALER_MERCHANT_GET_ORDERS_OPTION_H_CONTRACT. + */ + struct TALER_PrivateContractHashP h_contract; + + /** + * Value if @e option is + * #TALER_MERCHANT_GET_ORDERS_OPTION_TOKEN. + */ + struct TALER_ClaimTokenP token; + + /** + * Value if @e option is + * #TALER_MERCHANT_GET_ORDERS_OPTION_ALLOW_REFUNDED_FOR_REPURCHASE. + */ + enum TALER_EXCHANGE_YesNoAll allow_refunded_for_repurchase; + } details; }; @@ -118,15 +151,13 @@ struct TALER_MERCHANT_GetOrdersHandle; * @param ctx the context * @param url base URL of the merchant backend * @param order_id identifier of the order to check - * @param h_contract hash of the contract terms (for authentication) * @return handle to operation */ struct TALER_MERCHANT_GetOrdersHandle * TALER_MERCHANT_get_orders_create ( struct GNUNET_CURL_Context *ctx, const char *url, - const char *order_id, - const struct TALER_PrivateContractHashP *h_contract); + const char *order_id); /** @@ -192,6 +223,45 @@ TALER_MERCHANT_get_orders_create ( .details.await_refund_obtained = (b) \ } +/** + * Set hash of the contract terms (for authentication). + * + * @param h hash of contract terms + * @return representation of the option as a struct TALER_MERCHANT_GetOrdersOptionValue + */ +#define TALER_MERCHANT_get_orders_option_h_contract(h) \ + (const struct TALER_MERCHANT_GetOrdersOptionValue) \ + { \ + .option = TALER_MERCHANT_GET_ORDERS_OPTION_H_CONTRACT, \ + .details.h_contract = (h) \ + } + +/** + * Set claim token for unclaimed order authentication. + * + * @param t claim token + * @return representation of the option as a struct TALER_MERCHANT_GetOrdersOptionValue + */ +#define TALER_MERCHANT_get_orders_option_token(t) \ + (const struct TALER_MERCHANT_GetOrdersOptionValue) \ + { \ + .option = TALER_MERCHANT_GET_ORDERS_OPTION_TOKEN, \ + .details.token = (t) \ + } + +/** + * Set whether refunded orders count for repurchase detection. + * + * @param v yes/no/all value + * @return representation of the option as a struct TALER_MERCHANT_GetOrdersOptionValue + */ +#define TALER_MERCHANT_get_orders_option_allow_refunded_for_repurchase(v) \ + (const struct TALER_MERCHANT_GetOrdersOptionValue) \ + { \ + .option = TALER_MERCHANT_GET_ORDERS_OPTION_ALLOW_REFUNDED_FOR_REPURCHASE, \ + .details.allow_refunded_for_repurchase = (v) \ + } + /** * Set the requested options for the operation. diff --git a/src/include/taler/taler-merchant/post-orders-ORDER_ID-abort.h b/src/include/taler/taler-merchant/post-orders-ORDER_ID-abort.h @@ -129,6 +129,46 @@ struct TALER_MERCHANT_PostOrdersAbortResponse } ok; + /** + * Details on #MHD_HTTP_BAD_GATEWAY. + * The response body has the same refunds array as on + * #MHD_HTTP_OK, but at least one coin had an exchange error. + */ + struct + { + + /** + * Number of aborted coins in @a aborts. + */ + unsigned int num_aborts; + + /** + * Array of aborted coin results. + */ + const struct TALER_MERCHANT_PostOrdersAbortedCoin *aborts; + + } bad_gateway; + + /** + * Details on #MHD_HTTP_GATEWAY_TIMEOUT. + */ + struct + { + + /** + * Taler error code from the exchange, if available. + */ + enum TALER_ErrorCode exchange_ec; + + /** + * HTTP status code returned by the exchange, if available. + * 0 if no exchange was involved or the request timed out + * before a response was received. + */ + unsigned int exchange_http_status; + + } gateway_timeout; + } details; }; diff --git a/src/include/taler/taler-merchant/post-orders-ORDER_ID-pay.h b/src/include/taler/taler-merchant/post-orders-ORDER_ID-pay.h @@ -266,7 +266,12 @@ enum TALER_MERCHANT_PostOrdersPayOption /** * Output donation receipts. */ - TALER_MERCHANT_POST_ORDERS_PAY_OPTION_OUTPUT_DONAU + TALER_MERCHANT_POST_ORDERS_PAY_OPTION_OUTPUT_DONAU, + + /** + * Payment choice index (wallet mode). + */ + TALER_MERCHANT_POST_ORDERS_PAY_OPTION_CHOICE_INDEX }; @@ -372,6 +377,12 @@ struct TALER_MERCHANT_PostOrdersPayOptionValue uint64_t year; } output_donau; + /** + * Value if @e option is + * #TALER_MERCHANT_POST_ORDERS_PAY_OPTION_CHOICE_INDEX. + */ + int choice_index; + } details; }; @@ -429,6 +440,32 @@ struct TALER_MERCHANT_PostOrdersPayResponse } ok; /** + * Details on #MHD_HTTP_CONFLICT. + * Set when a coin was already spent (insufficient funds) or + * the order was already fully paid. + */ + struct + { + + /** + * Base URL of the exchange that reported the conflict, + * or NULL if not applicable. + */ + const char *exchange_url; + + /** + * Taler error code from the exchange, if available. + */ + enum TALER_ErrorCode exchange_ec; + + /** + * HTTP status code returned by the exchange, if available. + */ + unsigned int exchange_http_status; + + } conflict; + + /** * Details on #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS. */ struct @@ -446,6 +483,31 @@ struct TALER_MERCHANT_PostOrdersPayResponse } unavailable_for_legal_reasons; + /** + * Details on #MHD_HTTP_BAD_GATEWAY. + * The merchant's interaction with the exchange failed. + */ + struct + { + + /** + * Base URL of the exchange that returned an error, + * or NULL if not available. + */ + const char *exchange_url; + + /** + * Taler error code from the exchange, if available. + */ + enum TALER_ErrorCode exchange_ec; + + /** + * HTTP status code returned by the exchange, if available. + */ + unsigned int exchange_http_status; + + } bad_gateway; + } details; }; @@ -563,6 +625,20 @@ struct TALER_MERCHANT_PostOrdersPayResponse .details.output_donau.year = (y) \ } +/** + * Set payment choice index (wallet mode). + * Use -1 to indicate no choice (the default). + * + * @param i choice index + * @return representation of the option + */ +#define TALER_MERCHANT_post_orders_pay_option_choice_index(i) \ + (const struct TALER_MERCHANT_PostOrdersPayOptionValue) \ + { \ + .option = TALER_MERCHANT_POST_ORDERS_PAY_OPTION_CHOICE_INDEX, \ + .details.choice_index = (i) \ + } + /** * Set the requested options for the operation. @@ -631,7 +707,6 @@ TALER_MERCHANT_post_orders_pay_frontend_create ( * @param url base URL of the merchant backend * @param order_id identifier of the order to pay * @param h_contract_terms hash of the contract terms - * @param choice_index payment choice index * @param amount total payment amount * @param max_fee maximum acceptable fee * @param merchant_pub merchant public key @@ -650,7 +725,6 @@ TALER_MERCHANT_post_orders_pay_create ( const char *url, const char *order_id, const struct TALER_PrivateContractHashP *h_contract_terms, - int choice_index, // fIXME: make this an option? const struct TALER_Amount *amount, const struct TALER_Amount *max_fee, const struct TALER_MerchantPublicKeyP *merchant_pub, diff --git a/src/lib/merchant_api_get-orders-ORDER_ID.c b/src/lib/merchant_api_get-orders-ORDER_ID.c @@ -77,6 +77,11 @@ struct TALER_MERCHANT_GetOrdersHandle struct TALER_PrivateContractHashP h_contract; /** + * Claim token for unclaimed order authentication. + */ + struct TALER_ClaimTokenP token; + + /** * Session ID for repurchase detection, or NULL. */ char *session_id; @@ -92,6 +97,21 @@ struct TALER_MERCHANT_GetOrdersHandle struct TALER_Amount min_refund; /** + * Whether refunded orders count for repurchase detection. + */ + enum TALER_EXCHANGE_YesNoAll allow_refunded_for_repurchase; + + /** + * True if @e h_contract was set. + */ + bool have_h_contract; + + /** + * True if @e token was set. + */ + bool have_token; + + /** * True if @e min_refund was set. */ bool have_min_refund; @@ -219,6 +239,10 @@ handle_get_order_finished (void *cls, TALER_MERCHANT_get_orders_cancel (oph); return; } + case MHD_HTTP_BAD_REQUEST: + owgr.hr.ec = TALER_JSON_get_error_code (json); + owgr.hr.hint = TALER_JSON_get_error_hint (json); + break; case MHD_HTTP_FORBIDDEN: owgr.hr.ec = TALER_JSON_get_error_code (json); owgr.hr.hint = TALER_JSON_get_error_hint (json); @@ -231,6 +255,14 @@ handle_get_order_finished (void *cls, owgr.hr.ec = TALER_JSON_get_error_code (json); owgr.hr.hint = TALER_JSON_get_error_hint (json); break; + case MHD_HTTP_CONFLICT: + owgr.hr.ec = TALER_JSON_get_error_code (json); + owgr.hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + owgr.hr.ec = TALER_JSON_get_error_code (json); + owgr.hr.hint = TALER_JSON_get_error_hint (json); + break; default: TALER_MERCHANT_parse_error_details_ (json, response_code, @@ -251,8 +283,7 @@ struct TALER_MERCHANT_GetOrdersHandle * TALER_MERCHANT_get_orders_create ( struct GNUNET_CURL_Context *ctx, const char *url, - const char *order_id, - const struct TALER_PrivateContractHashP *h_contract) + const char *order_id) { struct TALER_MERCHANT_GetOrdersHandle *oph; @@ -260,7 +291,7 @@ TALER_MERCHANT_get_orders_create ( oph->ctx = ctx; oph->base_url = GNUNET_strdup (url); oph->order_id = GNUNET_strdup (order_id); - oph->h_contract = *h_contract; + oph->allow_refunded_for_repurchase = TALER_EXCHANGE_YNA_NO; return oph; } @@ -295,6 +326,18 @@ TALER_MERCHANT_get_orders_set_options_ ( case TALER_MERCHANT_GET_ORDERS_OPTION_AWAIT_REFUND_OBTAINED: oph->await_refund_obtained = opt->details.await_refund_obtained; break; + case TALER_MERCHANT_GET_ORDERS_OPTION_H_CONTRACT: + oph->h_contract = opt->details.h_contract; + oph->have_h_contract = true; + break; + case TALER_MERCHANT_GET_ORDERS_OPTION_TOKEN: + oph->token = opt->details.token; + oph->have_token = true; + break; + case TALER_MERCHANT_GET_ORDERS_OPTION_ALLOW_REFUNDED_FOR_REPURCHASE: + oph->allow_refunded_for_repurchase + = opt->details.allow_refunded_for_repurchase; + break; default: GNUNET_break (0); return GNUNET_NO; @@ -319,11 +362,18 @@ TALER_MERCHANT_get_orders_start ( / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us); { struct GNUNET_CRYPTO_HashAsciiEncoded h_contract_s; + char *token_s = NULL; char *path; char timeout_ms[32]; - - GNUNET_CRYPTO_hash_to_enc (&oph->h_contract.hash, - &h_contract_s); + const char *yna_s; + + if (oph->have_h_contract) + GNUNET_CRYPTO_hash_to_enc (&oph->h_contract.hash, + &h_contract_s); + if (oph->have_token) + token_s = GNUNET_STRINGS_data_to_string_alloc ( + &oph->token, + sizeof (oph->token)); GNUNET_snprintf (timeout_ms, sizeof (timeout_ms), "%u", @@ -331,10 +381,17 @@ TALER_MERCHANT_get_orders_start ( GNUNET_asprintf (&path, "orders/%s", oph->order_id); + yna_s = (TALER_EXCHANGE_YNA_NO != oph->allow_refunded_for_repurchase) + ? TALER_yna_to_string (oph->allow_refunded_for_repurchase) + : NULL; oph->url = TALER_url_join (oph->base_url, path, "h_contract", - h_contract_s.encoding, + oph->have_h_contract + ? h_contract_s.encoding + : NULL, + "token", + token_s, "session_id", oph->session_id, "timeout_ms", @@ -349,8 +406,11 @@ TALER_MERCHANT_get_orders_start ( oph->await_refund_obtained ? "yes" : NULL, + "allow_refunded_for_repurchase", + yna_s, NULL); GNUNET_free (path); + GNUNET_free (token_s); } if (NULL == oph->url) return TALER_EC_GENERIC_CONFIGURATION_INVALID; diff --git a/src/lib/merchant_api_post-orders-ORDER_ID-abort.c b/src/lib/merchant_api_post-orders-ORDER_ID-abort.c @@ -203,8 +203,19 @@ check_abort_refund (struct TALER_MERCHANT_PostOrdersAbortHandle *poah, } } } - ar->details.ok.num_aborts = num_refunds; - ar->details.ok.aborts = res; + switch (ar->hr.http_status) + { + case MHD_HTTP_OK: + ar->details.ok.num_aborts = num_refunds; + ar->details.ok.aborts = res; + break; + case MHD_HTTP_BAD_GATEWAY: + ar->details.bad_gateway.num_aborts = num_refunds; + ar->details.bad_gateway.aborts = res; + break; + default: + GNUNET_assert (0); + } poah->cb (poah->cb_cls, ar); poah->cb = NULL; @@ -279,13 +290,24 @@ handle_abort_finished (void *cls, ar.hr.hint = TALER_JSON_get_error_hint (json); break; case MHD_HTTP_BAD_GATEWAY: + if (GNUNET_OK == + check_abort_refund (poah, + &ar, + json)) + { + TALER_MERCHANT_post_orders_abort_cancel (poah); + return; + } + ar.hr.http_status = 0; + ar.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_GATEWAY_TIMEOUT: TALER_MERCHANT_parse_error_details_ (json, response_code, &ar.hr); - break; - case MHD_HTTP_GATEWAY_TIMEOUT: - ar.hr.ec = TALER_JSON_get_error_code (json); - ar.hr.hint = TALER_JSON_get_error_hint (json); + ar.details.gateway_timeout.exchange_ec = ar.hr.exchange_code; + ar.details.gateway_timeout.exchange_http_status + = ar.hr.exchange_http_status; break; default: TALER_MERCHANT_parse_error_details_ (json, diff --git a/src/lib/merchant_api_post-orders-ORDER_ID-pay.c b/src/lib/merchant_api_post-orders-ORDER_ID-pay.c @@ -379,6 +379,15 @@ handle_pay_finished (void *cls, TALER_MERCHANT_parse_error_details_ (json, MHD_HTTP_CONFLICT, &pr.hr); + { + const char *eu = json_string_value ( + json_object_get (json, "exchange_url")); + + pr.details.conflict.exchange_url = eu; + pr.details.conflict.exchange_ec = pr.hr.exchange_code; + pr.details.conflict.exchange_http_status + = pr.hr.exchange_http_status; + } break; case MHD_HTTP_GONE: TALER_MERCHANT_parse_error_details_ (json, @@ -452,6 +461,15 @@ handle_pay_finished (void *cls, TALER_MERCHANT_parse_error_details_ (json, response_code, &pr.hr); + { + const char *eu = json_string_value ( + json_object_get (json, "exchange_url")); + + pr.details.bad_gateway.exchange_url = eu; + pr.details.bad_gateway.exchange_ec = pr.hr.exchange_code; + pr.details.bad_gateway.exchange_http_status + = pr.hr.exchange_http_status; + } break; case MHD_HTTP_GATEWAY_TIMEOUT: TALER_MERCHANT_parse_error_details_ (json, @@ -517,7 +535,6 @@ TALER_MERCHANT_post_orders_pay_create ( const char *url, const char *order_id, const struct TALER_PrivateContractHashP *h_contract_terms, - int choice_index, const struct TALER_Amount *amount, const struct TALER_Amount *max_fee, const struct TALER_MerchantPublicKeyP *merchant_pub, @@ -545,7 +562,7 @@ TALER_MERCHANT_post_orders_pay_create ( poph->order_id = GNUNET_strdup (order_id); poph->am_wallet = true; poph->h_contract_terms = *h_contract_terms; - poph->choice_index = choice_index; + poph->choice_index = -1; poph->amount = *amount; poph->max_fee = *max_fee; poph->merchant_pub = *merchant_pub; @@ -675,6 +692,9 @@ TALER_MERCHANT_post_orders_pay_set_options_ ( src->blinded_udi.blinded_message); } break; + case TALER_MERCHANT_POST_ORDERS_PAY_OPTION_CHOICE_INDEX: + poph->choice_index = options[i].details.choice_index; + break; default: GNUNET_break (0); return GNUNET_SYSERR; diff --git a/src/testing/testing_api_cmd_pay_order.c b/src/testing/testing_api_cmd_pay_order.c @@ -1372,7 +1372,6 @@ pay_run (void *cls, ps->merchant_url, order_id, h_proposal, - ps->choice_index, &ps->total_amount, &max_fee, &merchant_pub, @@ -1386,6 +1385,13 @@ pay_run (void *cls, if (NULL == ps->oph) TALER_TESTING_FAIL (is); + if (0 <= ps->choice_index) + GNUNET_assert ( + GNUNET_OK == + TALER_MERCHANT_post_orders_pay_set_options ( + ps->oph, + TALER_MERCHANT_post_orders_pay_option_choice_index ( + ps->choice_index))); if (NULL != ps->session_id) GNUNET_assert ( GNUNET_OK == diff --git a/src/testing/testing_api_cmd_wallet_get_order.c b/src/testing/testing_api_cmd_wallet_get_order.c @@ -278,12 +278,13 @@ wallet_get_order_run (void *cls, gos->ogh = TALER_MERCHANT_get_orders_create ( TALER_TESTING_interpreter_get_context (is), gos->merchant_url, - order_id, - h_contract); - if (NULL != gos->session_id) + order_id); + GNUNET_assert ( + GNUNET_OK == TALER_MERCHANT_get_orders_set_options ( gos->ogh, - TALER_MERCHANT_get_orders_option_session_id (gos->session_id)); + TALER_MERCHANT_get_orders_option_h_contract (*h_contract), + TALER_MERCHANT_get_orders_option_session_id (gos->session_id))); { enum TALER_ErrorCode ec; @@ -685,8 +686,10 @@ wallet_poll_order_start_run (void *cls, pos->ogh = TALER_MERCHANT_get_orders_create ( TALER_TESTING_interpreter_get_context (is), pos->merchant_url, - order_id, - h_contract); + order_id); + TALER_MERCHANT_get_orders_set_options ( + pos->ogh, + TALER_MERCHANT_get_orders_option_h_contract (*h_contract)); TALER_MERCHANT_get_orders_set_options ( pos->ogh, TALER_MERCHANT_get_orders_option_timeout (pos->timeout),