diff options
author | Christian Grothoff <christian@grothoff.org> | 2024-02-24 17:28:29 +0100 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2024-02-24 17:28:29 +0100 |
commit | 1a695f06eaf4fa635f0235a28dac5dcdea5fd448 (patch) | |
tree | 92b9d51b07510f5183ebd23cc441e536ae39b44f | |
parent | 70a44acf8458cfed17191950cb41e69b6f06e64d (diff) | |
download | merchant-1a695f06eaf4fa635f0235a28dac5dcdea5fd448.tar.gz merchant-1a695f06eaf4fa635f0235a28dac5dcdea5fd448.tar.bz2 merchant-1a695f06eaf4fa635f0235a28dac5dcdea5fd448.zip |
major refactoring of taler-merchant-httpd_get-orders-ID.c --- no semantic change
-rw-r--r-- | src/backend/taler-merchant-httpd_get-orders-ID.c | 1746 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_get-orders-ID.h | 39 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_helper.c | 110 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_helper.h | 39 | ||||
-rw-r--r-- | src/include/taler_merchant_testing_lib.h | 49 | ||||
-rw-r--r-- | src/testing/test_merchant_api.c | 54 | ||||
-rw-r--r-- | src/testing/testing_api_cmd_wallet_get_order.c | 90 |
7 files changed, 1289 insertions, 838 deletions
diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c index 13ff3fc9..aeaba030 100644 --- a/src/backend/taler-merchant-httpd_get-orders-ID.c +++ b/src/backend/taler-merchant-httpd_get-orders-ID.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014-2022 Taler Systems SA + (C) 2014-2024 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 @@ -40,6 +40,25 @@ /** + * The different phases in which we handle the request. + */ +enum Phase +{ + GOP_INIT = 0, + GOP_LOOKUP_TERMS, + GOP_PARSE_CONTRACT, + GOP_CHECK_CLIENT_ACCESS, + GOP_REDIRECT_TO_PAID_ORDER, + GOP_CHECK_CLAIMED, + GOP_CHECK_PAID, + GOP_CHECK_REFUNDED, + GOP_RETURN_STATUS, + GOP_RETURN_MHD_YES, + GOP_RETURN_MHD_NO +}; + + +/** * Context for the operation. */ struct GetOrderData @@ -148,6 +167,12 @@ struct GetOrderData struct TALER_Amount refund_taken; /** + * Phase in which we currently are handling this + * request. + */ + enum Phase phase; + + /** * Return code: #TALER_EC_NONE if successful. */ enum TALER_ErrorCode ec; @@ -166,7 +191,7 @@ struct GetOrderData * doing repurchase detection. */ enum TALER_EXCHANGE_YesNoAll allow_refunded_for_repurchase; - + /** * Set to true if the client passed 'h_contract'. */ @@ -212,6 +237,25 @@ struct GetOrderData * a different currency then the main contract. */ bool bad_refund_currency_in_db; + + /** + * Did the hash of the contract match the contract + * hash supplied by the client? + */ + bool contract_match; + + /** + * True if we had a claim token and the claim token + * provided by the client matched our claim token. + */ + bool token_match; + + /** + * True if we found a (claimed) contract for the order, + * false if we had an unclaimed order. + */ + bool contract_available; + }; @@ -245,6 +289,112 @@ TMH_force_wallet_get_order_resume (void) /** + * Suspend this @a god until the trigger is satisfied. + * + * @param god request to suspend + */ +static void +suspend_god (struct GetOrderData *god) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending GET /orders/%s\n", + god->order_id); + /* We reset the contract terms and start by looking them up + again, as while we are suspended fundamental things could + change (such as the contract being claimed) */ + if (NULL != god->contract_terms) + { + json_decref (god->contract_terms); + god->fulfillment_url = NULL; + god->contract_terms = NULL; + god->contract_parsed = false; + god->merchant_base_url = NULL; + god->public_reorder_url = NULL; + } + GNUNET_assert (! god->suspended); + god->contract_parsed = false; + god->contract_match = false; + god->token_match = false; + god->contract_available = false; + god->phase = GOP_LOOKUP_TERMS; + god->suspended = GNUNET_YES; + GNUNET_CONTAINER_DLL_insert (god_head, + god_tail, + god); + MHD_suspend_connection (god->sc.con); +} + + +/** + * Clean up the session state for a GET /orders/$ID request. + * + * @param cls must be a `struct GetOrderData *` + */ +static void +god_cleanup (void *cls) +{ + struct GetOrderData *god = cls; + + if (NULL != god->contract_terms) + { + json_decref (god->contract_terms); + god->contract_terms = NULL; + } + if (NULL != god->refund_eh) + { + TMH_db->event_listen_cancel (god->refund_eh); + god->refund_eh = NULL; + } + if (NULL != god->pay_eh) + { + TMH_db->event_listen_cancel (god->pay_eh); + god->pay_eh = NULL; + } + GNUNET_free (god); +} + + +/** + * Finish the request by returning @a mret as the + * final result. + * + * @param[in,out] god request we are processing + * @param mret MHD result to return + */ +static void +phase_end (struct GetOrderData *god, + MHD_RESULT mret) +{ + god->phase = (MHD_YES == mret) + ? GOP_RETURN_MHD_YES + : GOP_RETURN_MHD_NO; +} + + +/** + * Finish the request by returning an error @a ec + * with HTTP status @a http_status and @a message. + * + * @param[in,out] god request we are processing + * @param http_status HTTP status code to return + * @param ec error code to return + * @param message human readable hint to return, can be NULL + */ +static void +phase_fail (struct GetOrderData *god, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *message) +{ + phase_end (god, + TALER_MHD_reply_with_error (god->sc.con, + http_status, + ec, + message)); +} + + +/** * We have received a trigger from the database * that we should (possibly) resume the request. * @@ -333,195 +483,385 @@ resume_by_event (void *cls, /** - * Suspend this @a god until the trigger is satisfied. + * First phase (after request parsing). + * Set up long-polling. * - * @param god request to suspend + * @param[in,out] god request context */ static void -suspend_god (struct GetOrderData *god) +phase_init (struct GetOrderData *god) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending GET /orders/%s\n", - god->order_id); - if (NULL != god->contract_terms) + god->phase++; + if (god->generate_html) + return; /* If HTML is requested, we never actually long poll. */ + if (! GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout)) + return; /* long polling not requested */ + + if (god->sc.awaiting_refund || + god->sc.awaiting_refund_obtained) { - json_decref (god->contract_terms); - god->fulfillment_url = NULL; - god->contract_terms = NULL; - god->contract_parsed = false; - god->merchant_base_url = NULL; - god->public_reorder_url = NULL; + struct TMH_OrderPayEventP refund_eh = { + .header.size = htons (sizeof (refund_eh)), + .header.type = htons (god->sc.awaiting_refund_obtained + ? TALER_DBEVENT_MERCHANT_REFUND_OBTAINED + : TALER_DBEVENT_MERCHANT_ORDER_REFUND), + .merchant_pub = god->hc->instance->merchant_pub + }; + + GNUNET_CRYPTO_hash (god->order_id, + strlen (god->order_id), + &refund_eh.h_order_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subscribing %p to refunds on %s\n", + god, + god->order_id); + god->refund_eh + = TMH_db->event_listen ( + TMH_db->cls, + &refund_eh.header, + GNUNET_TIME_absolute_get_remaining ( + god->sc.long_poll_timeout), + &resume_by_event, + god); + } + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), + .merchant_pub = god->hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subscribing to payments on %s\n", + god->order_id); + GNUNET_CRYPTO_hash (god->order_id, + strlen (god->order_id), + &pay_eh.h_order_id); + god->pay_eh + = TMH_db->event_listen ( + TMH_db->cls, + &pay_eh.header, + GNUNET_TIME_absolute_get_remaining ( + god->sc.long_poll_timeout), + &resume_by_event, + god); } - GNUNET_assert (! god->suspended); - god->suspended = GNUNET_YES; - GNUNET_CONTAINER_DLL_insert (god_head, - god_tail, - god); - MHD_suspend_connection (god->sc.con); } /** - * Create a taler://refund/ URI for the given @a con and @a order_id - * and @a instance_id. + * Lookup contract terms and check client has the + * right to access this order (by claim token or + * contract hash). * - * @param merchant_base_url URL to take host and path from; - * we cannot take it from the MHD connection as a browser - * may have changed 'http' to 'https' and we MUST be consistent - * with what the merchant's frontend used initially - * @param order_id the order id - * @return corresponding taler://refund/ URI, or NULL on missing "host" + * @param[in,out] god request context */ -static char * -make_taler_refund_uri (const char *merchant_base_url, - const char *order_id) +static void +phase_lookup_terms (struct GetOrderData *god) { - struct GNUNET_Buffer buf = { 0 }; - char *url; - struct GNUNET_Uri uri; + uint64_t order_serial; + struct TALER_ClaimTokenP db_claim_token; + enum GNUNET_DB_QueryStatus qs; - url = GNUNET_strdup (merchant_base_url); - if (-1 == GNUNET_uri_parse (&uri, - url)) + /* Convert order_id to h_contract_terms */ + TMH_db->preflight (TMH_db->cls); + GNUNET_assert (NULL == god->contract_terms); + qs = TMH_db->lookup_contract_terms ( + TMH_db->cls, + god->hc->instance->settings.id, + god->order_id, + &god->contract_terms, + &order_serial, + &db_claim_token); + if (0 > qs) { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ GNUNET_break (0); - GNUNET_free (url); - return NULL; + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_contract_terms"); + return; } - GNUNET_assert (NULL != order_id); - GNUNET_buffer_write_str (&buf, - "taler"); - if (0 == strcasecmp ("http", - uri.scheme)) - GNUNET_buffer_write_str (&buf, - "+http"); - GNUNET_buffer_write_str (&buf, - "://refund/"); - GNUNET_buffer_write_str (&buf, - uri.host); - if (0 != uri.port) - GNUNET_buffer_write_fstr (&buf, - ":%u", - (unsigned int) uri.port); - if (NULL != uri.path) - GNUNET_buffer_write_path (&buf, - uri.path); - GNUNET_buffer_write_path (&buf, - order_id); - GNUNET_buffer_write_path (&buf, - ""); // Trailing slash - GNUNET_free (url); - return GNUNET_buffer_reap_str (&buf); -} + /* Note: when "!ord.requireClaimToken" and the client does not provide + a claim token (all zeros!), then token_match==TRUE below: */ + god->token_match + = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + && (0 == GNUNET_memcmp (&db_claim_token, + &god->claim_token)); + /* Check if client provided the right hash code of the contract terms */ + if (NULL != god->contract_terms) + { + god->contract_available = true; + if (GNUNET_YES == + GNUNET_is_zero (&god->h_contract_terms)) + { + if (GNUNET_OK != + TALER_JSON_contract_hash (god->contract_terms, + &god->h_contract_terms)) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "contract terms"); + return; + } + } + else + { + struct TALER_PrivateContractHashP h; -char * -TMH_make_order_status_url (struct MHD_Connection *con, - const char *order_id, - const char *session_id, - const char *instance_id, - struct TALER_ClaimTokenP *claim_token, - struct TALER_PrivateContractHashP *h_contract) -{ - struct GNUNET_Buffer buf; - /* Number of query parameters written so far */ - unsigned int num_qp = 0; + if (GNUNET_OK != + TALER_JSON_contract_hash (god->contract_terms, + &h)) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "contract terms"); + return; + } + god->contract_match = (0 == + GNUNET_memcmp (&h, + &god->h_contract_terms)); + if (! god->contract_match) + { + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, + NULL); + return; + } + } + } - GNUNET_assert (NULL != instance_id); - GNUNET_assert (NULL != order_id); - if (GNUNET_OK != - TMH_base_url_by_connection (con, - instance_id, - &buf)) + if (god->contract_available) { - GNUNET_break (0); - return NULL; + god->claimed = true; } - GNUNET_buffer_write_path (&buf, - "/orders"); - GNUNET_buffer_write_path (&buf, - order_id); - if ( (NULL != claim_token) && - (! GNUNET_is_zero (claim_token)) ) + else { - /* 'token=' for human readability */ - GNUNET_buffer_write_str (&buf, - "?token="); - GNUNET_buffer_write_data_encoded (&buf, - (char *) claim_token, - sizeof (*claim_token)); - num_qp++; - } + struct TALER_ClaimTokenP db_claim_token; + struct TALER_MerchantPostDataHashP unused; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_order ( + TMH_db->cls, + god->hc->instance->settings.id, + god->order_id, + &db_claim_token, + &unused, + (NULL == god->contract_terms) + ? &god->contract_terms + : NULL); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_order"); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Unknown order id given: `%s'\n", + god->order_id); + phase_fail (god, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + god->order_id); + return; + } + /* Note: when "!ord.requireClaimToken" and the client does not provide + a claim token (all zeros!), then token_match==TRUE below: */ + god->token_match + = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && + (0 == GNUNET_memcmp (&db_claim_token, + &god->claim_token)); + } /* end unclaimed order logic */ + god->phase++; +} - if (NULL != session_id) - { - if (num_qp > 0) - GNUNET_buffer_write_str (&buf, - "&session_id="); - else - GNUNET_buffer_write_str (&buf, - "?session_id="); - GNUNET_buffer_write_str (&buf, - session_id); - num_qp++; - } - if (NULL != h_contract) +/** + * Parse contract terms. + * + * @param[in,out] god request context + */ +static void +phase_parse_contract (struct GetOrderData *god) +{ + struct GNUNET_JSON_Specification espec[] = { + TALER_JSON_spec_amount_any ("amount", + &god->contract_total), + TALER_JSON_spec_web_url ("merchant_base_url", + &god->merchant_base_url), + GNUNET_JSON_spec_mark_optional ( + /* this one does NOT have to be a Web URL! */ + GNUNET_JSON_spec_string ("fulfillment_url", + &god->fulfillment_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("public_reorder_url", + &god->public_reorder_url), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + GNUNET_assert (NULL != god->contract_terms); + if (god->contract_parsed) + return; /* not sure this is possible... */ + + res = GNUNET_JSON_parse (god->contract_terms, + espec, + &ename, + &eline); + if (GNUNET_OK != res) { - if (num_qp > 0) - GNUNET_buffer_write_str (&buf, - "&h_contract="); - else - GNUNET_buffer_write_str (&buf, - "?h_contract="); - GNUNET_buffer_write_data_encoded (&buf, - (char *) h_contract, - sizeof (*h_contract)); + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse contract %s in DB at field %s\n", + god->order_id, + ename); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + god->order_id); + return; } - - return GNUNET_buffer_reap_str (&buf); + god->contract_parsed = true; + god->phase++; } -char * -TMH_make_taler_pay_uri (struct MHD_Connection *con, - const char *order_id, - const char *session_id, - const char *instance_id, - struct TALER_ClaimTokenP *claim_token) +/** + * Check that this order is unclaimed or claimed by + * this client. + * + * @param[in,out] god request context + */ +static void +phase_check_client_access (struct GetOrderData *god) { - struct GNUNET_Buffer buf; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Token match: %d, contract_available: %d, contract match: %d, claimed: %d\n", + god->token_match, + god->contract_available, + god->contract_match, + god->claimed); - GNUNET_assert (NULL != instance_id); - GNUNET_assert (NULL != order_id); - if (GNUNET_OK != - TMH_taler_uri_by_connection (con, - "pay", - instance_id, - &buf)) + if (god->claim_token_provided && ! god->token_match) { - GNUNET_break (0); - return NULL; + /* Authentication provided but wrong. */ + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, + "authentication with claim token provided but wrong"); + return; } - GNUNET_buffer_write_path (&buf, - order_id); - GNUNET_buffer_write_path (&buf, - (NULL == session_id) - ? "" - : session_id); - if ( (NULL != claim_token) && - (! GNUNET_is_zero (claim_token))) + + if (god->h_contract_provided && ! god->contract_match) { - /* Just 'c=' because this goes into QR - codes, so this is more compact. */ - GNUNET_buffer_write_str (&buf, - "?c="); - GNUNET_buffer_write_data_encoded (&buf, - (char *) claim_token, - sizeof (struct TALER_ClaimTokenP)); + /* Authentication provided but wrong. */ + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH, + NULL); + return; } - return GNUNET_buffer_reap_str (&buf); + if (! (god->token_match || + god->contract_match) ) + { + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Neither claim token nor contract matched\n"); + /* Client has no rights to this order */ + if (NULL == god->public_reorder_url) + { + /* We cannot give the client a new order, just fail */ + if (! GNUNET_is_zero (&god->h_contract_terms)) + { + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, + NULL); + return; + } + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, + "no 'public_reorder_url'"); + return; + } + /* We have a fulfillment URL, redirect the client there, maybe + the frontend can generate a fresh order for this new customer */ + if (god->generate_html) + { + /* Contract was claimed (maybe by another device), so this client + cannot get the status information. Redirect to fulfillment page, + where the client may be able to pickup a fresh order -- or might + be able authenticate via session ID */ + struct MHD_Response *reply; + MHD_RESULT ret; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Contract claimed, redirecting to fulfillment page for order %s\n", + god->order_id); + reply = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + if (NULL == reply) + { + GNUNET_break (0); + phase_end (god, + MHD_NO); + return; + } + GNUNET_break (MHD_YES == + MHD_add_response_header (reply, + MHD_HTTP_HEADER_LOCATION, + god->public_reorder_url)); + ret = MHD_queue_response (god->sc.con, + MHD_HTTP_FOUND, + reply); + MHD_destroy_response (reply); + phase_end (god, + ret); + return; + } + /* Need to generate JSON reply */ + phase_end (god, + TALER_MHD_REPLY_JSON_PACK ( + god->sc.con, + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_string ("public_reorder_url", + god->public_reorder_url))); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Claim token or contract matched\n"); + god->phase++; } @@ -565,9 +905,9 @@ get_order_summary (const struct GetOrderData *god) * @param already_paid_order_id if for the fulfillment URI there is * already a paid order, this is the order ID to redirect * the wallet to; NULL if not applicable - * @return #MHD_YES on success + * @return true to exit due to suspension */ -static MHD_RESULT +static bool send_pay_request (struct GetOrderData *god, const char *already_paid_order_id) { @@ -584,7 +924,7 @@ send_pay_request (struct GetOrderData *god, GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending request: long polling for payment\n"); suspend_god (god); - return MHD_YES; + return true; } /* Check if resource_id has been paid for in the same session @@ -592,27 +932,30 @@ send_pay_request (struct GetOrderData *god, */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Sending payment request\n"); - taler_pay_uri = TMH_make_taler_pay_uri (god->sc.con, - god->order_id, - god->session_id, - god->hc->instance->settings.id, - &god->claim_token); - order_status_url = TMH_make_order_status_url (god->sc.con, - god->order_id, - god->session_id, - god->hc->instance->settings.id, - &god->claim_token, - NULL); + taler_pay_uri = TMH_make_taler_pay_uri ( + god->sc.con, + god->order_id, + god->session_id, + god->hc->instance->settings.id, + &god->claim_token); + order_status_url = TMH_make_order_status_url ( + god->sc.con, + god->order_id, + god->session_id, + god->hc->instance->settings.id, + &god->claim_token, + NULL); if ( (NULL == taler_pay_uri) || (NULL == order_status_url) ) { GNUNET_break_op (0); GNUNET_free (taler_pay_uri); GNUNET_free (order_status_url); - return TALER_MHD_reply_with_error (god->sc.con, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, - "host"); + phase_fail (god, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, + "host"); + return false; } if (god->generate_html) { @@ -631,7 +974,9 @@ send_pay_request (struct GetOrderData *god, if (NULL == reply) { GNUNET_break (0); - return MHD_NO; + phase_end (god, + MHD_NO); + return false; } GNUNET_break (MHD_YES == MHD_add_response_header (reply, @@ -644,7 +989,9 @@ send_pay_request (struct GetOrderData *god, MHD_HTTP_FOUND, reply); MHD_destroy_response (reply); - return ret; + phase_end (god, + ret); + return false; } } @@ -655,7 +1002,9 @@ send_pay_request (struct GetOrderData *god, if (NULL == qr) { GNUNET_break (0); - return MHD_NO; + phase_end (god, + MHD_NO); + return false; } { enum GNUNET_GenericReturnValue res; @@ -670,12 +1019,13 @@ send_pay_request (struct GetOrderData *god, qr), GNUNET_JSON_pack_string ("order_summary", get_order_summary (god))); - res = TALER_TEMPLATING_reply (god->sc.con, - MHD_HTTP_PAYMENT_REQUIRED, - "request_payment", - god->hc->instance->settings.id, - taler_pay_uri, - context); + res = TALER_TEMPLATING_reply ( + god->sc.con, + MHD_HTTP_PAYMENT_REQUIRED, + "request_payment", + god->hc->instance->settings.id, + taler_pay_uri, + context); if (GNUNET_SYSERR == res) { GNUNET_break (0); @@ -706,7 +1056,151 @@ send_pay_request (struct GetOrderData *god, } GNUNET_free (taler_pay_uri); GNUNET_free (order_status_url); - return ret; + phase_end (god, + ret); + return false; +} + + +/** + * Check if the client already paid for an equivalent + * order under this session, and if so redirect to + * that order. + * + * @param[in,out] god request context + * @return true to exit due to suspension + */ +static bool +phase_redirect_to_paid_order (struct GetOrderData *god) +{ + if ( (NULL != god->session_id) && + (NULL != god->fulfillment_url) ) + { + /* Check if client paid for this fulfillment article + already within this session, but using a different + order ID. If so, redirect the client to the order + it already paid. Allows, for example, the case + where a mobile phone pays for a browser's session, + where the mobile phone has a different order + ID (because it purchased the article earlier) + than the one that the browser is waiting for. */ + char *already_paid_order_id = NULL; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Running re-purchase detection for %s/%s\n", + god->session_id, + god->fulfillment_url); + qs = TMH_db->lookup_order_by_fulfillment ( + TMH_db->cls, + god->hc->instance->settings.id, + god->fulfillment_url, + god->session_id, + TALER_EXCHANGE_YNA_NO != + god->allow_refunded_for_repurchase, + &already_paid_order_id); + if (qs < 0) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "order by fulfillment"); + return false; + } + if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || + (0 != strcmp (god->order_id, + already_paid_order_id)) ) + { + bool ret; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending pay request for order %s (already paid: %s)\n", + god->order_id, + already_paid_order_id); + ret = send_pay_request (god, + already_paid_order_id); + GNUNET_free (already_paid_order_id); + return ret; + } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + GNUNET_free (already_paid_order_id); + } + god->phase++; + return false; +} + + +/** + * Check if order is unclaimed, and if so request + * payment. + * + * @param[in,out] god request context + * @return true to exit due to suspension + */ +static bool +phase_check_claimed (struct GetOrderData *god) +{ + if (god->claimed) + { + god->phase++; + return false; + } + /* Order is unclaimed, no need to check for payments or even + refunds, simply always generate payment request */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order unclaimed, sending pay request for order %s\n", + god->order_id); + return send_pay_request (god, + NULL); +} + + +/** + * Check if the order has been paid, and if not + * request payment. + * + * @param[in,out] god request context + * @return true to exit due to suspension + */ +static bool +phase_check_paid (struct GetOrderData *god) +{ + enum GNUNET_DB_QueryStatus qs; + struct TALER_PrivateContractHashP h_contract; + bool paid; + + qs = TMH_db->lookup_order_status ( + TMH_db->cls, + god->hc->instance->settings.id, + god->order_id, + &h_contract, + &paid); + if (0 >= qs) + { + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_order_status"); + return false; + } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + if (! paid) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order claimed but unpaid, sending pay request for order %s\n", + god->order_id); + return send_pay_request (god, + NULL); + } + god->phase++; + return false; } @@ -773,46 +1267,275 @@ process_refunds_cb (void *cls, /** - * Clean up the session state for a GET /orders/$ID request. + * Check if the order has been refunded. * - * @param cls must be a `struct GetOrderData *` + * @param[in,out] god request context + * @return true to exit due to suspension */ -static void -god_cleanup (void *cls) +static bool +phase_check_refunded (struct GetOrderData *god) { - struct GetOrderData *god = cls; + enum GNUNET_DB_QueryStatus qs; - if (NULL != god->contract_terms) + if ( (god->sc.awaiting_refund) && + (GNUNET_OK != + TALER_amount_cmp_currency (&god->contract_total, + &god->sc.refund_expected)) ) { - json_decref (god->contract_terms); - god->contract_terms = NULL; + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + god->contract_total.currency); + return false; } - if (NULL != god->refund_eh) + + /* At this point, we know the contract was paid. Let's check for + refunds. First, clear away refunds found from previous invocations. */ + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (god->contract_total.currency, + &god->refund_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (god->contract_total.currency, + &god->refund_taken)); + qs = TMH_db->lookup_refunds_detailed ( + TMH_db->cls, + god->hc->instance->settings.id, + &god->h_contract_terms, + &process_refunds_cb, + god); + if (0 > qs) { - TMH_db->event_listen_cancel (god->refund_eh); - god->refund_eh = NULL; + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_refunds_detailed"); + return false; } - if (NULL != god->pay_eh) + if (god->bad_refund_currency_in_db) { - TMH_db->event_listen_cancel (god->pay_eh); - god->pay_eh = NULL; + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "currency mix-up between contract price and refunds in database"); + return false; + } + if ( ((god->sc.awaiting_refund) && + ( (! god->refunded) || + (1 != TALER_amount_cmp (&god->refund_amount, + &god->sc.refund_expected)) )) || + ( (god->sc.awaiting_refund_obtained) && + (god->refund_pending) ) ) + { + /* Client is waiting for a refund larger than what we have, suspend + until timeout */ + struct GNUNET_TIME_Relative remaining; + + remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); + if ( (! GNUNET_TIME_relative_is_zero (remaining)) && + (! god->generate_html) ) + { + /* yes, indeed suspend */ + if (god->sc.awaiting_refund) + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting refund exceeding %s\n", + TALER_amount2s (&god->sc.refund_expected)); + if (god->sc.awaiting_refund_obtained) + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting pending refunds\n"); + suspend_god (god); + return true; + } + } + god->phase++; + return false; +} + + +/** + * Create a taler://refund/ URI for the given @a con and @a order_id + * and @a instance_id. + * + * @param merchant_base_url URL to take host and path from; + * we cannot take it from the MHD connection as a browser + * may have changed 'http' to 'https' and we MUST be consistent + * with what the merchant's frontend used initially + * @param order_id the order id + * @return corresponding taler://refund/ URI, or NULL on missing "host" + */ +static char * +make_taler_refund_uri (const char *merchant_base_url, + const char *order_id) +{ + struct GNUNET_Buffer buf = { 0 }; + char *url; + struct GNUNET_Uri uri; + + url = GNUNET_strdup (merchant_base_url); + if (-1 == GNUNET_uri_parse (&uri, + url)) + { + GNUNET_break (0); + GNUNET_free (url); + return NULL; + } + GNUNET_assert (NULL != order_id); + GNUNET_buffer_write_str (&buf, + "taler"); + if (0 == strcasecmp ("http", + uri.scheme)) + GNUNET_buffer_write_str (&buf, + "+http"); + GNUNET_buffer_write_str (&buf, + "://refund/"); + GNUNET_buffer_write_str (&buf, + uri.host); + if (0 != uri.port) + GNUNET_buffer_write_fstr (&buf, + ":%u", + (unsigned int) uri.port); + if (NULL != uri.path) + GNUNET_buffer_write_path (&buf, + uri.path); + GNUNET_buffer_write_path (&buf, + order_id); + GNUNET_buffer_write_path (&buf, + ""); // Trailing slash + GNUNET_free (url); + return GNUNET_buffer_reap_str (&buf); +} + + +/** + * Generate the order status response. + * + * @param[in,out] god request context + */ +static void +phase_return_status (struct GetOrderData *god) +{ + /* All operations done, build final response */ + if (! god->generate_html) + { + phase_end (god, + TALER_MHD_REPLY_JSON_PACK ( + god->sc.con, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_url", + god->fulfillment_url)), + GNUNET_JSON_pack_bool ("refunded", + god->refunded), + GNUNET_JSON_pack_bool ("refund_pending", + god->refund_pending), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken), + TALER_JSON_pack_amount ("refund_amount", + &god->refund_amount))); + return; + } + + if (god->refund_pending) + { + char *qr; + char *uri; + + GNUNET_assert (NULL != god->contract_terms); + uri = make_taler_refund_uri (god->merchant_base_url, + god->order_id); + if (NULL == uri) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "refund URI"); + return; + } + qr = TMH_create_qrcode (uri); + if (NULL == qr) + { + GNUNET_break (0); + GNUNET_free (uri); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "qr code"); + return; + } + + { + enum GNUNET_GenericReturnValue res; + json_t *context; + + context = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("order_summary", + get_order_summary (god)), + TALER_JSON_pack_amount ("refund_amount", + &god->refund_amount), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken), + GNUNET_JSON_pack_string ("taler_refund_uri", + uri), + GNUNET_JSON_pack_string ("taler_refund_qrcode_svg", + qr)); + res = TALER_TEMPLATING_reply ( + god->sc.con, + MHD_HTTP_OK, + "offer_refund", + god->hc->instance->settings.id, + uri, + context); + GNUNET_break (GNUNET_OK == res); + json_decref (context); + phase_end (god, + (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES); + } + GNUNET_free (uri); + GNUNET_free (qr); + return; + } + + { + enum GNUNET_GenericReturnValue res; + json_t *context; + + context = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("contract_terms", + god->contract_terms), + GNUNET_JSON_pack_string ("order_summary", + get_order_summary (god)), + TALER_JSON_pack_amount ("refund_amount", + &god->refund_amount), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken)); + res = TALER_TEMPLATING_reply ( + god->sc.con, + MHD_HTTP_OK, + "show_order_details", + god->hc->instance->settings.id, + NULL, + context); + GNUNET_break (GNUNET_OK == res); + json_decref (context); + phase_end (god, + (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES); } - GNUNET_free (god); } -// FIXME: this function should probably be refactored... MHD_RESULT TMH_get_orders_ID (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct GetOrderData *god = hc->ctx; - const char *order_id = hc->infix; - enum GNUNET_DB_QueryStatus qs; - bool contract_match = false; - bool token_match = false; - bool contract_available = false; (void) rh; if (NULL == god) @@ -822,8 +1545,9 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, hc->cc = &god_cleanup; god->sc.con = connection; god->hc = hc; - god->order_id = order_id; - god->generate_html = TMH_MHD_test_html_desired (connection); + god->order_id = hc->infix; + god->generate_html + = TMH_MHD_test_html_desired (connection); /* first-time initialization / sanity checks */ TALER_MHD_parse_request_arg_auto (connection, @@ -890,65 +1614,9 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, min_refund); } } - TALER_MHD_parse_request_timeout (connection, &god->sc.long_poll_timeout); - /* If HTML is requested, we never actually long poll. Makes no sense */ - if ( (! god->generate_html) && - (GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout)) ) - { - if (god->sc.awaiting_refund || - god->sc.awaiting_refund_obtained) - { - struct TMH_OrderPayEventP refund_eh = { - .header.size = htons (sizeof (refund_eh)), - .header.type = htons (god->sc.awaiting_refund_obtained - ? TALER_DBEVENT_MERCHANT_REFUND_OBTAINED - : TALER_DBEVENT_MERCHANT_ORDER_REFUND), - .merchant_pub = hc->instance->merchant_pub - }; - - GNUNET_CRYPTO_hash (god->order_id, - strlen (god->order_id), - &refund_eh.h_order_id); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subscribing %p to refunds on %s\n", - god, - god->order_id); - god->refund_eh - = TMH_db->event_listen ( - TMH_db->cls, - &refund_eh.header, - GNUNET_TIME_absolute_get_remaining ( - god->sc.long_poll_timeout), - &resume_by_event, - god); - } - { - struct TMH_OrderPayEventP pay_eh = { - .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), - .merchant_pub = hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subscribing to payments on %s\n", - god->order_id); - GNUNET_CRYPTO_hash (god->order_id, - strlen (god->order_id), - &pay_eh.h_order_id); - god->pay_eh - = TMH_db->event_listen ( - TMH_db->cls, - &pay_eh.header, - GNUNET_TIME_absolute_get_remaining ( - god->sc.long_poll_timeout), - &resume_by_event, - god); - } - } /* end of do long-polling / no HTML */ - - } /* end of first-time initialization / sanity checks */ + } if (GNUNET_SYSERR == god->suspended) return MHD_NO; /* we are in shutdown */ @@ -960,530 +1628,50 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, god); } - /* Convert order_id to h_contract_terms */ - TMH_db->preflight (TMH_db->cls); - if (NULL == god->contract_terms) - { - uint64_t order_serial; - struct TALER_ClaimTokenP db_claim_token; - - qs = TMH_db->lookup_contract_terms (TMH_db->cls, - hc->instance->settings.id, - order_id, - &god->contract_terms, - &order_serial, - &db_claim_token); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_contract_terms"); - } - /* Note: when "!ord.requireClaimToken" and the client does not provide - a claim token (all zeros!), then token_match==TRUE below: */ - token_match = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - && (0 == GNUNET_memcmp (&db_claim_token, - &god->claim_token)); - } - - /* Check if client provided the right hash code of the contract terms */ - if (NULL != god->contract_terms) - { - contract_available = true; - if (GNUNET_YES == - GNUNET_is_zero (&god->h_contract_terms)) - { - if (GNUNET_OK != - TALER_JSON_contract_hash (god->contract_terms, - &god->h_contract_terms)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "contract terms"); - } - } - else - { - struct TALER_PrivateContractHashP h; - - if (GNUNET_OK != - TALER_JSON_contract_hash (god->contract_terms, - &h)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "contract terms"); - } - contract_match = (0 == - GNUNET_memcmp (&h, - &god->h_contract_terms)); - if (! contract_match) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, - NULL); - } - } - } - - if (contract_available) - { - god->claimed = true; - } - else - { - struct TALER_ClaimTokenP db_claim_token; - struct TALER_MerchantPostDataHashP unused; - - qs = TMH_db->lookup_order (TMH_db->cls, - hc->instance->settings.id, - order_id, - &db_claim_token, - &unused, - (NULL == god->contract_terms) - ? &god->contract_terms - : NULL); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_order"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Unknown order id given: `%s'\n", - order_id); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - order_id); - } - /* Note: when "!ord.requireClaimToken" and the client does not provide - a claim token (all zeros!), then token_match==TRUE below: */ - token_match = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && - (0 == GNUNET_memcmp (&db_claim_token, - &god->claim_token)); - } /* end unclaimed order logic */ - - GNUNET_assert (NULL != god->contract_terms); - if (! god->contract_parsed) - { - struct GNUNET_JSON_Specification espec[] = { - TALER_JSON_spec_amount_any ("amount", - &god->contract_total), - TALER_JSON_spec_web_url ("merchant_base_url", - &god->merchant_base_url), - GNUNET_JSON_spec_mark_optional ( - /* this one does NOT have to be a Web URL! */ - GNUNET_JSON_spec_string ("fulfillment_url", - &god->fulfillment_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("public_reorder_url", - &god->public_reorder_url), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - const char *ename; - unsigned int eline; - - res = GNUNET_JSON_parse (god->contract_terms, - espec, - &ename, - &eline); - if (GNUNET_OK != res) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse contract %s in DB at field %s\n", - order_id, - ename); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - order_id); - } - god->contract_parsed = true; - } - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Token match: %d, contract_available: %d, contract match: %d, claimed: %d\n", - token_match, - contract_available, - contract_match, - god->claimed); - - if (god->claim_token_provided && ! token_match) - { - /* Authentication provided but wrong. */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, - "authentication with claim token provided but wrong"); - } - - if (god->h_contract_provided && ! contract_match) - { - /* Authentication provided but wrong. */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH, - NULL); - } - - if (! (token_match || - contract_match) ) - { - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Neither claim token nor contract matched\n"); - /* Client has no rights to this order */ - if (NULL == god->public_reorder_url) - { - /* We cannot give the client a new order, just fail */ - if (! GNUNET_is_zero (&god->h_contract_terms)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, - NULL); - } - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, - "no 'public_reorder_url'"); - } - /* We have a fulfillment URL, redirect the client there, maybe - the frontend can generate a fresh order for this new customer */ - if (god->generate_html) - { - /* Contract was claimed (maybe by another device), so this client - cannot get the status information. Redirect to fulfillment page, - where the client may be able to pickup a fresh order -- or might - be able authenticate via session ID */ - struct MHD_Response *reply; - MHD_RESULT ret; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Contract claimed, redirecting to fulfillment page for order %s\n", - order_id); - reply = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - if (NULL == reply) - { - GNUNET_break (0); - return MHD_NO; - } - GNUNET_break (MHD_YES == - MHD_add_response_header (reply, - MHD_HTTP_HEADER_LOCATION, - god->public_reorder_url)); - ret = MHD_queue_response (connection, - MHD_HTTP_FOUND, - reply); - MHD_destroy_response (reply); - return ret; - } - /* Need to generate JSON reply */ - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_string ("public_reorder_url", - god->public_reorder_url)); - } - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Claim token or contract matched\n"); - - if ( (NULL != god->session_id) && - (NULL != god->fulfillment_url) ) - { - /* Check if client paid for this fulfillment article - already within this session, but using a different - order ID. If so, redirect the client to the order - it already paid. Allows, for example, the case - where a mobile phone pays for a browser's session, - where the mobile phone has a different order - ID (because it purchased the article earlier) - than the one that the browser is waiting for. */ - char *already_paid_order_id = NULL; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Running re-purchase detection for %s/%s\n", - god->session_id, - god->fulfillment_url); - qs = TMH_db->lookup_order_by_fulfillment (TMH_db->cls, - hc->instance->settings.id, - god->fulfillment_url, - god->session_id, - TALER_EXCHANGE_YNA_NO != - god->allow_refunded_for_repurchase, - &already_paid_order_id); - if (qs < 0) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "order by fulfillment"); - } - if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || - (0 != strcmp (order_id, - already_paid_order_id)) ) - { - MHD_RESULT ret; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Sending pay request for order %s (already paid: %s)\n", - order_id, - already_paid_order_id); - ret = send_pay_request (god, - already_paid_order_id); - GNUNET_free (already_paid_order_id); - return ret; - } - GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); - GNUNET_free (already_paid_order_id); - } - - if (! god->claimed) + while (1) { - /* Order is unclaimed, no need to check for payments or even - refunds, simply always generate payment request */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order unclaimed, sending pay request for order %s\n", - order_id); - return send_pay_request (god, - NULL); - } - - { - /* Check if paid. */ - struct TALER_PrivateContractHashP h_contract; - bool paid; - - qs = TMH_db->lookup_order_status (TMH_db->cls, - hc->instance->settings.id, - order_id, - &h_contract, - &paid); - if (0 >= qs) + "Handling request in phase %d\n", + (int) god->phase); + switch (god->phase) { - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_order_status"); - } - GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); - if (! paid) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order claimed but unpaid, sending pay request for order %s\n", - order_id); - return send_pay_request (god, - NULL); - } - } - - if ( (god->sc.awaiting_refund) && - (GNUNET_OK != - TALER_amount_cmp_currency (&god->contract_total, - &god->sc.refund_expected)) ) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - god->contract_total.currency); - } - - /* At this point, we know the contract was paid. Let's check for - refunds. First, clear away refunds found from previous invocations. */ - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (god->contract_total.currency, - &god->refund_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (god->contract_total.currency, - &god->refund_taken)); - qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, - hc->instance->settings.id, - &god->h_contract_terms, - &process_refunds_cb, - god); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_refunds_detailed"); - } - if (god->bad_refund_currency_in_db) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "currency mix-up between contract price and refunds in database"); - } - if ( ((god->sc.awaiting_refund) && - ( (! god->refunded) || - (1 != TALER_amount_cmp (&god->refund_amount, - &god->sc.refund_expected)) )) || - ( (god->sc.awaiting_refund_obtained) && - (god->refund_pending) ) ) - { - /* Client is waiting for a refund larger than what we have, suspend - until timeout */ - struct GNUNET_TIME_Relative remaining; - - remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); - if ( (! GNUNET_TIME_relative_is_zero (remaining)) && - (! god->generate_html) ) - { - /* yes, indeed suspend */ - if (god->sc.awaiting_refund) - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting refund exceeding %s\n", - TALER_amount2s (&god->sc.refund_expected)); - if (god->sc.awaiting_refund_obtained) - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting pending refunds\n"); - suspend_god (god); + case GOP_INIT: + phase_init (god); + break; + case GOP_LOOKUP_TERMS: + phase_lookup_terms (god); + break; + case GOP_PARSE_CONTRACT: + phase_parse_contract (god); + break; + case GOP_CHECK_CLIENT_ACCESS: + phase_check_client_access (god); + break; + case GOP_REDIRECT_TO_PAID_ORDER: + if (phase_redirect_to_paid_order (god)) + return MHD_YES; + break; + case GOP_CHECK_CLAIMED: + if (phase_check_claimed (god)) + return MHD_YES; + break; + case GOP_CHECK_PAID: + if (phase_check_paid (god)) + return MHD_YES; + break; + case GOP_CHECK_REFUNDED: + if (phase_check_refunded (god)) + return MHD_YES; + break; + case GOP_RETURN_STATUS: + phase_return_status (god); + break; + case GOP_RETURN_MHD_YES: return MHD_YES; - } - } - - /* All operations done, build final response */ - if (god->generate_html) - { - enum GNUNET_GenericReturnValue res; - - if (god->refund_pending) - { - char *qr; - char *uri; - - GNUNET_assert (NULL != god->contract_terms); - uri = make_taler_refund_uri (god->merchant_base_url, - order_id); - if (NULL == uri) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (god->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "refund URI"); - } - qr = TMH_create_qrcode (uri); - if (NULL == qr) - { - GNUNET_break (0); - GNUNET_free (uri); - return TALER_MHD_reply_with_error (god->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "qr code"); - } - { - json_t *context; - - context = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("order_summary", - get_order_summary (god)), - TALER_JSON_pack_amount ("refund_amount", - &god->refund_amount), - TALER_JSON_pack_amount ("refund_taken", - &god->refund_taken), - GNUNET_JSON_pack_string ("taler_refund_uri", - uri), - GNUNET_JSON_pack_string ("taler_refund_qrcode_svg", - qr)); - res = TALER_TEMPLATING_reply (god->sc.con, - MHD_HTTP_OK, - "offer_refund", - hc->instance->settings.id, - uri, - context); - json_decref (context); - } - GNUNET_free (uri); - GNUNET_free (qr); - } - else - { - json_t *context; - - context = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_object_incref ("contract_terms", - god->contract_terms), - GNUNET_JSON_pack_string ("order_summary", - get_order_summary (god)), - TALER_JSON_pack_amount ("refund_amount", - &god->refund_amount), - TALER_JSON_pack_amount ("refund_taken", - &god->refund_taken)); - res = TALER_TEMPLATING_reply (god->sc.con, - MHD_HTTP_OK, - "show_order_details", - hc->instance->settings.id, - NULL, - context); - json_decref (context); - } - if (GNUNET_SYSERR == res) - { - GNUNET_break (0); + case GOP_RETURN_MHD_NO: return MHD_NO; } - return MHD_YES; } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_url", - god->fulfillment_url)), - GNUNET_JSON_pack_bool ("refunded", - god->refunded), - GNUNET_JSON_pack_bool ("refund_pending", - god->refund_pending), - TALER_JSON_pack_amount ("refund_taken", - &god->refund_taken), - TALER_JSON_pack_amount ("refund_amount", - &god->refund_amount)); } diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.h b/src/backend/taler-merchant-httpd_get-orders-ID.h index 97b8525b..49ef5a73 100644 --- a/src/backend/taler-merchant-httpd_get-orders-ID.h +++ b/src/backend/taler-merchant-httpd_get-orders-ID.h @@ -32,45 +32,6 @@ TMH_force_wallet_get_order_resume (void); /** - * Create a taler://pay/ URI for the given @a con and @a order_id - * and @a session_id and @a instance_id. - * - * @param con HTTP connection - * @param order_id the order id - * @param session_id session, may be NULL - * @param instance_id instance, may be "default" - * @param claim_token claim token for the order, may be NULL - * @return corresponding taler://pay/ URI, or NULL on missing "host" - */ -char * -TMH_make_taler_pay_uri (struct MHD_Connection *con, - const char *order_id, - const char *session_id, - const char *instance_id, - struct TALER_ClaimTokenP *claim_token); - -/** - * Create a http(s) URL for the given @a con and @a order_id - * and @a instance_id to display the /orders/{order_id} page. - * - * @param con HTTP connection - * @param order_id the order id - * @param session_id session, may be NULL - * @param instance_id instance, may be "default" - * @param claim_token claim token for the order, may be NULL - * @param h_contract contract hash for authentication, may be NULL - * @return corresponding http(s):// URL, or NULL on missing "host" - */ -char * -TMH_make_order_status_url (struct MHD_Connection *con, - const char *order_id, - const char *session_id, - const char *instance_id, - struct TALER_ClaimTokenP *claim_token, - struct TALER_PrivateContractHashP *h_contract); - - -/** * Handle a GET "/orders/$ID" request. * * @param rh context of the handler diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c index 9261df91..f21b2e48 100644 --- a/src/backend/taler-merchant-httpd_helper.c +++ b/src/backend/taler-merchant-httpd_helper.c @@ -980,3 +980,113 @@ TMH_exchange_accounts_by_method ( } return emc.accounts; } + + +char * +TMH_make_order_status_url (struct MHD_Connection *con, + const char *order_id, + const char *session_id, + const char *instance_id, + struct TALER_ClaimTokenP *claim_token, + struct TALER_PrivateContractHashP *h_contract) +{ + struct GNUNET_Buffer buf; + /* Number of query parameters written so far */ + unsigned int num_qp = 0; + + GNUNET_assert (NULL != instance_id); + GNUNET_assert (NULL != order_id); + if (GNUNET_OK != + TMH_base_url_by_connection (con, + instance_id, + &buf)) + { + GNUNET_break (0); + return NULL; + } + GNUNET_buffer_write_path (&buf, + "/orders"); + GNUNET_buffer_write_path (&buf, + order_id); + if ( (NULL != claim_token) && + (! GNUNET_is_zero (claim_token)) ) + { + /* 'token=' for human readability */ + GNUNET_buffer_write_str (&buf, + "?token="); + GNUNET_buffer_write_data_encoded (&buf, + (char *) claim_token, + sizeof (*claim_token)); + num_qp++; + } + + if (NULL != session_id) + { + if (num_qp > 0) + GNUNET_buffer_write_str (&buf, + "&session_id="); + else + GNUNET_buffer_write_str (&buf, + "?session_id="); + GNUNET_buffer_write_str (&buf, + session_id); + num_qp++; + } + + if (NULL != h_contract) + { + if (num_qp > 0) + GNUNET_buffer_write_str (&buf, + "&h_contract="); + else + GNUNET_buffer_write_str (&buf, + "?h_contract="); + GNUNET_buffer_write_data_encoded (&buf, + (char *) h_contract, + sizeof (*h_contract)); + } + + return GNUNET_buffer_reap_str (&buf); +} + + +char * +TMH_make_taler_pay_uri (struct MHD_Connection *con, + const char *order_id, + const char *session_id, + const char *instance_id, + struct TALER_ClaimTokenP *claim_token) +{ + struct GNUNET_Buffer buf; + + GNUNET_assert (NULL != instance_id); + GNUNET_assert (NULL != order_id); + if (GNUNET_OK != + TMH_taler_uri_by_connection (con, + "pay", + instance_id, + &buf)) + { + GNUNET_break (0); + return NULL; + } + GNUNET_buffer_write_path (&buf, + order_id); + GNUNET_buffer_write_path (&buf, + (NULL == session_id) + ? "" + : session_id); + if ( (NULL != claim_token) && + (! GNUNET_is_zero (claim_token))) + { + /* Just 'c=' because this goes into QR + codes, so this is more compact. */ + GNUNET_buffer_write_str (&buf, + "?c="); + GNUNET_buffer_write_data_encoded (&buf, + (char *) claim_token, + sizeof (struct TALER_ClaimTokenP)); + } + + return GNUNET_buffer_reap_str (&buf); +} diff --git a/src/backend/taler-merchant-httpd_helper.h b/src/backend/taler-merchant-httpd_helper.h index 827cc03b..6783b9d4 100644 --- a/src/backend/taler-merchant-httpd_helper.h +++ b/src/backend/taler-merchant-httpd_helper.h @@ -193,6 +193,45 @@ TMH_taler_uri_by_connection (struct MHD_Connection *connection, /** + * Create a taler://pay/ URI for the given @a con and @a order_id + * and @a session_id and @a instance_id. + * + * @param con HTTP connection + * @param order_id the order id + * @param session_id session, may be NULL + * @param instance_id instance, may be "default" + * @param claim_token claim token for the order, may be NULL + * @return corresponding taler://pay/ URI, or NULL on missing "host" + */ +char * +TMH_make_taler_pay_uri (struct MHD_Connection *con, + const char *order_id, + const char *session_id, + const char *instance_id, + struct TALER_ClaimTokenP *claim_token); + +/** + * Create a http(s) URL for the given @a con and @a order_id + * and @a instance_id to display the /orders/{order_id} page. + * + * @param con HTTP connection + * @param order_id the order id + * @param session_id session, may be NULL + * @param instance_id instance, may be "default" + * @param claim_token claim token for the order, may be NULL + * @param h_contract contract hash for authentication, may be NULL + * @return corresponding http(s):// URL, or NULL on missing "host" + */ +char * +TMH_make_order_status_url (struct MHD_Connection *con, + const char *order_id, + const char *session_id, + const char *instance_id, + struct TALER_ClaimTokenP *claim_token, + struct TALER_PrivateContractHashP *h_contract); + + +/** * Put data from an exchange's HTTP response into * a JSON reply * diff --git a/src/include/taler_merchant_testing_lib.h b/src/include/taler_merchant_testing_lib.h index 011f96a8..56063a0f 100644 --- a/src/include/taler_merchant_testing_lib.h +++ b/src/include/taler_merchant_testing_lib.h @@ -644,9 +644,10 @@ TALER_TESTING_cmd_poll_orders_start (const char *label, * @param poll_start_reference reference to the #TALER_TESTING_cmd_poll_orders_start command */ struct TALER_TESTING_Command -TALER_TESTING_cmd_poll_orders_conclude (const char *label, - unsigned int http_status, - const char *poll_start_reference); +TALER_TESTING_cmd_poll_orders_conclude ( + const char *label, + unsigned int http_status, + const char *poll_start_reference); /** @@ -662,13 +663,41 @@ TALER_TESTING_cmd_poll_orders_conclude (const char *label, * @param http_status expected HTTP response code for the request. */ struct TALER_TESTING_Command -TALER_TESTING_cmd_wallet_get_order (const char *label, - const char *merchant_url, - const char *order_reference, - bool paid, - bool refunded, - bool refund_pending, - unsigned int http_status); +TALER_TESTING_cmd_wallet_get_order ( + const char *label, + const char *merchant_url, + const char *order_reference, + bool paid, + bool refunded, + bool refund_pending, + unsigned int http_status); + + +/** + * Define a GET /orders/$ORDER_ID CMD. + * + * @param label the command label + * @param merchant_url base URL of the merchant which will + * serve the request. + * @param order_reference reference to a command that created an order. + * @param session_id session ID to check for + * @param paid whether the order has been paid for or not. + * @param refunded whether the order has been refunded. + * @param refund_pending whether the order has refunds that haven't been obtained. + * @param repurchase_order_ref command of a paid equivalent order the merchant should be refering us to, or NULL + * @param http_status expected HTTP response code for the request. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_wallet_get_order2 ( + const char *label, + const char *merchant_url, + const char *order_reference, + const char *session_id, + bool paid, + bool refunded, + bool refund_pending, + const char *repurchase_order_ref, + unsigned int http_status); /** diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c index 48e8cec4..bfa534d5 100644 --- a/src/testing/test_merchant_api.c +++ b/src/testing/test_merchant_api.c @@ -1514,6 +1514,14 @@ run (void *cls, "EUR:1.00", "EUR:0.99", "repurchase-session"), + TALER_TESTING_cmd_wallet_get_order ( + "repurchase-wallet-check-primary-order", + merchant_url, + "post-order-repurchase-original", + true, + false, + false, + MHD_HTTP_OK), TALER_TESTING_cmd_merchant_get_order3 ( "repurchase-check-primary-payment", merchant_url, @@ -1556,6 +1564,26 @@ run (void *cls, "repurchase-session", "post-order-repurchase-original", MHD_HTTP_OK), + TALER_TESTING_cmd_wallet_get_order2 ( + "repurchase-wallet-check-order-secondary", + merchant_url, + "post-order-repurchase-secondary", + "repurchase-session", + false, + false, + false, + "post-order-repurchase-original", + MHD_HTTP_PAYMENT_REQUIRED), + TALER_TESTING_cmd_wallet_get_order2 ( + "repurchase-wallet-check-order-secondary-bad-session", + merchant_url, + "post-order-repurchase-secondary", + "wrong-session", + false, + false, + false, + NULL, + MHD_HTTP_PAYMENT_REQUIRED), TALER_TESTING_cmd_merchant_order_refund ( "refund-repurchased", merchant_url, @@ -1563,8 +1591,30 @@ run (void *cls, "repurchase-original", "EUR:1.0", MHD_HTTP_OK), + TALER_TESTING_cmd_wallet_get_order2 ( + "repurchase-wallet-check-primary-order-refunded-no-session", + merchant_url, + "post-order-repurchase-original", + NULL, + true, + true, + true, + "post-order-repurchase-original", + MHD_HTTP_OK), +#ifdef XFAIL + TALER_TESTING_cmd_wallet_get_order2 ( + "repurchase-wallet-check-primary-order-refunded", + merchant_url, + "post-order-repurchase-original", + "repurchase-session", + true, + true, + true, + "post-order-repurchase-original", + MHD_HTTP_OK), +#endif TALER_TESTING_cmd_merchant_get_order3 ( - "repurchase-check-secondary-payment", + "repurchase-check-refunded", merchant_url, "post-order-repurchase-secondary", TALER_MERCHANT_OSC_CLAIMED, @@ -1879,7 +1929,7 @@ run (void *cls, merchant_url, "product-2", MHD_HTTP_CONFLICT), -#if 0 +#if 1 TALER_TESTING_cmd_batch ("pay", pay), TALER_TESTING_cmd_batch ("double-spending", diff --git a/src/testing/testing_api_cmd_wallet_get_order.c b/src/testing/testing_api_cmd_wallet_get_order.c index fd4a914d..d55ff0a7 100644 --- a/src/testing/testing_api_cmd_wallet_get_order.c +++ b/src/testing/testing_api_cmd_wallet_get_order.c @@ -59,6 +59,18 @@ struct WalletGetOrderState const char *order_reference; /** + * Reference to a command that created a paid + * equivalent order that we expect to be referred + * to during repurchase detection, or NULL. + */ + const char *repurchase_order_ref; + + /** + * Session Id the order needs to be bound to. + */ + const char *session_id; + + /** * Whether the order was paid or not. */ bool paid; @@ -125,6 +137,31 @@ wallet_get_order_cb ( const char *order_id; const struct TALER_ClaimTokenP *claim_token; + if (NULL != gos->repurchase_order_ref) + { + const struct TALER_TESTING_Command *rep_cmd; + const char *rep_id; + const char *ri; + + rep_cmd = TALER_TESTING_interpreter_lookup_command ( + gos->is, + gos->repurchase_order_ref); + if (GNUNET_OK != + TALER_TESTING_get_trait_order_id (rep_cmd, + &rep_id)) + { + TALER_TESTING_FAIL (gos->is); + } + ri = owgr->details.payment_required.already_paid_order_id; + if ( (NULL == ri) || + (0 != + strcmp (ri, + rep_id)) ) + { + TALER_TESTING_FAIL (gos->is); + } + } + if (GNUNET_OK != TALER_MERCHANT_parse_pay_uri ( owgr->details.payment_required.taler_pay_uri, @@ -243,7 +280,7 @@ wallet_get_order_run (void *cls, order_id, h_contract, GNUNET_TIME_UNIT_ZERO, - NULL, + gos->session_id, NULL, false, &wallet_get_order_cb, @@ -274,13 +311,48 @@ wallet_get_order_cleanup (void *cls, struct TALER_TESTING_Command -TALER_TESTING_cmd_wallet_get_order (const char *label, - const char *merchant_url, - const char *order_reference, - bool paid, - bool refunded, - bool refund_pending, - unsigned int http_status) +TALER_TESTING_cmd_wallet_get_order ( + const char *label, + const char *merchant_url, + const char *order_reference, + bool paid, + bool refunded, + bool refund_pending, + unsigned int http_status) +{ + struct WalletGetOrderState *gos; + + gos = GNUNET_new (struct WalletGetOrderState); + gos->merchant_url = merchant_url; + gos->order_reference = order_reference; + gos->http_status = http_status; + gos->paid = paid; + gos->refunded = refunded; + gos->refund_pending = refund_pending; + { + struct TALER_TESTING_Command cmd = { + .cls = gos, + .label = label, + .run = &wallet_get_order_run, + .cleanup = &wallet_get_order_cleanup + }; + + return cmd; + } +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_wallet_get_order2 ( + const char *label, + const char *merchant_url, + const char *order_reference, + const char *session_id, + bool paid, + bool refunded, + bool refund_pending, + const char *repurchase_order_ref, + unsigned int http_status) { struct WalletGetOrderState *gos; @@ -289,8 +361,10 @@ TALER_TESTING_cmd_wallet_get_order (const char *label, gos->order_reference = order_reference; gos->http_status = http_status; gos->paid = paid; + gos->session_id = session_id; gos->refunded = refunded; gos->refund_pending = refund_pending; + gos->repurchase_order_ref = repurchase_order_ref; { struct TALER_TESTING_Command cmd = { .cls = gos, |