/* This file is part of TALER (C) 2017, 2019, 2020 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file backend/taler-merchant-httpd_private-get-orders-ID.c * @brief implementation of GET /private/orders/ID handler * @author Florian Dold * @author Christian Grothoff */ #include "platform.h" #include "taler-merchant-httpd_private-get-orders-ID.h" #include "taler-merchant-httpd_get-orders-ID.h" #include #include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_private-get-orders.h" /** * How long do we wait on the exchange? */ #define EXCHANGE_TIMEOUT GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_SECONDS, 30) /** * Data structure we keep for a check payment request. */ struct GetOrderRequestContext; /** * Request to an exchange for details about wire transfers * in response to a coin's deposit operation. */ struct TransferQuery { /** * Kept in a DLL. */ struct TransferQuery *next; /** * Kept in a DLL. */ struct TransferQuery *prev; /** * Handle to query exchange about deposit status. */ struct TALER_EXCHANGE_DepositGetHandle *dgh; /** * Handle for ongoing exchange operation. */ struct TMH_EXCHANGES_FindOperation *fo; /** * Overall request this TQ belongs with. */ struct GetOrderRequestContext *gorc; /** * Hash of the merchant's bank account the transfer (presumably) went to. */ struct GNUNET_HashCode h_wire; /** * Value deposited (including deposit fee). */ struct TALER_Amount amount_with_fee; /** * Deposit fee paid for this coin. */ struct TALER_Amount deposit_fee; /** * Public key of the coin this is about. */ struct TALER_CoinSpendPublicKeyP coin_pub; /** * Which deposit operation is this about? */ uint64_t deposit_serial; }; /** * Data structure we keep for a check payment request. */ struct GetOrderRequestContext { /** * Entry in the #resume_timeout_heap for this check payment, if we are * suspended. */ struct TMH_SuspendedConnection sc; /** * Which merchant instance is this for? */ struct TMH_HandlerContext *hc; /** * session of the client */ const char *session_id; /** * Fulfillment URL extracted from the contract. For repurchase detection. * Only valid as long as @e contract_terms is valid! NULL if there is * no fulfillment URL in the contract. */ const char *fulfillment_url; /** * Kept in a DLL while suspended on exchange. */ struct GetOrderRequestContext *next; /** * Kept in a DLL while suspended on exchange. */ struct GetOrderRequestContext *prev; /** * Handle to the exchange, only valid while the @e fo succeeds. */ struct TALER_EXCHANGE_Handle *eh; /** * Head of DLL of individual queries for transfer data. */ struct TransferQuery *tq_head; /** * Tail of DLL of individual queries for transfer data. */ struct TransferQuery *tq_tail; /** * Timeout task while waiting on exchange. */ struct GNUNET_SCHEDULER_Task *tt; /** * Contract terms of the payment we are checking. NULL when they * are not (yet) known. */ json_t *contract_terms; /** * Wire details for the payment, to be returned in the reply. NULL * if not available. */ json_t *wire_details; /** * Problems we encountered when looking up Wire details * for the payment, to be returned. NULL if not available. */ json_t *wire_reports; /** * Details about refunds, NULL if there are no refunds. */ json_t *refund_details; /** * Hash over the @e contract_terms. */ struct GNUNET_HashCode h_contract_terms; /** * Total amount the exchange deposited into our bank account * (confirmed or unconfirmed), excluding fees. */ struct TALER_Amount deposits_total; /** * Total amount in deposit fees we paid for all coins. */ struct TALER_Amount deposit_fees_total; /** * Total value of the coins that the exchange deposited into our bank * account (confirmed or unconfirmed), including deposit fees. */ struct TALER_Amount value_total; /** * Total we were to be paid under the contract, excluding refunds. */ struct TALER_Amount contract_amount; /** * Serial ID of the order. */ uint64_t order_serial; /** * Total refunds granted for this payment. Only initialized * if @e refunded is set to true. */ struct TALER_Amount refund_amount; /** * Exchange HTTP error code encountered while trying to determine wire transfer * details. #TALER_EC_NONE for no error encountered. */ unsigned int exchange_hc; /** * Exchange error code encountered while trying to determine wire transfer * details. #TALER_EC_NONE for no error encountered. */ enum TALER_ErrorCode exchange_ec; /** * Error code encountered while trying to determine wire transfer * details. #TALER_EC_NONE for no error encountered. */ enum TALER_ErrorCode wire_ec; /** * HTTP status to return with @e wire_ec, 0 if @e wire_ec is #TALER_EC_NONE. */ unsigned int wire_hc; /** * Set to true if this payment has been refunded and * @e refund_amount is initialized. */ bool refunded; /** * Did the client request us to fetch the wire transfer status? * If false, we may still return it if it is available. */ bool transfer_status_requested; }; /** * Head of list of suspended requests waiting on the exchange. */ static struct GetOrderRequestContext *gorc_head; /** * Tail of list of suspended requests waiting on the exchange. */ static struct GetOrderRequestContext *gorc_tail; /** * Resume processing the request, cancelling all pending asynchronous * operations. * * @param gorc request to resume * @param http_status HTTP status to return, 0 to continue with success * @param ec error code for the request, #TALER_EC_NONE on success */ static void gorc_resume (struct GetOrderRequestContext *gorc, unsigned int http_status, enum TALER_ErrorCode ec) { struct TransferQuery *tq; if (NULL != gorc->tt) { GNUNET_SCHEDULER_cancel (gorc->tt); gorc->tt = NULL; } while (NULL != (tq = gorc->tq_head)) { if (NULL != tq->fo) { TMH_EXCHANGES_find_exchange_cancel (tq->fo); tq->fo = NULL; } if (NULL != tq->dgh) { TALER_EXCHANGE_deposits_get_cancel (tq->dgh); tq->dgh = NULL; } } gorc->wire_hc = http_status; gorc->wire_ec = ec; GNUNET_CONTAINER_DLL_remove (gorc_head, gorc_tail, gorc); MHD_resume_connection (gorc->sc.con); TMH_trigger_daemon (); /* we resumed, kick MHD */ } /** * Add a report about trouble obtaining wire transfer data to the reply. * * @param gorc request to add wire report to * @param ec error code to add * @param coin_pub public key of the affected coin * @param exchange_hr details from exchange, NULL if exchange is blameless */ static void gorc_report (struct GetOrderRequestContext *gorc, enum TALER_ErrorCode ec, struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_EXCHANGE_HttpResponse *exchange_hr) { if (NULL != exchange_hr) GNUNET_assert (0 == json_array_append_new ( gorc->wire_reports, json_pack ("{s:I, s:s, s:I, s:I, s:o }", "code", (json_int_t) ec, "hint", TALER_ErrorCode_get_hint (ec), "exchange_ec", (json_int_t) exchange_hr->ec, "exchange_hc", (json_int_t) exchange_hr->http_status, "coin_pub", GNUNET_JSON_from_data_auto (coin_pub)))); else GNUNET_assert (0 == json_array_append_new ( gorc->wire_reports, json_pack ("{s:I, s:s, s:o }", "code", (json_int_t) ec, "hint", TALER_ErrorCode_get_hint (ec), "coin_pub", GNUNET_JSON_from_data_auto (coin_pub)))); } /** * Timeout trying to get current wire transfer data from the exchange. * Clean up and continue. * * @param cls closure, must be a `struct GetOrderRequestContext *` */ static void exchange_timeout_cb (void *cls) { struct GetOrderRequestContext *gorc = cls; gorc->tt = NULL; gorc_resume (gorc, MHD_HTTP_REQUEST_TIMEOUT, TALER_EC_GET_ORDERS_EXCHANGE_TIMEOUT); } /** * Function called with detailed wire transfer data. * * @param cls closure with a `struct TransferQuery *` * @param hr HTTP response data * @param dd details about the deposit (NULL on errors) */ static void deposit_get_cb (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, const struct TALER_EXCHANGE_DepositData *dd) { struct TransferQuery *tq = cls; struct GetOrderRequestContext *gorc = tq->gorc; GNUNET_CONTAINER_DLL_remove (gorc->tq_head, gorc->tq_tail, tq); if (NULL == dd) { gorc_report (gorc, TALER_EC_GET_ORDERS_EXCHANGE_TRACKING_FAILURE, &tq->coin_pub, hr); GNUNET_free (tq); if (NULL == gorc->tq_head) gorc_resume (gorc, 0, TALER_EC_NONE); return; } else if (MHD_HTTP_OK == hr->http_status) { enum GNUNET_DB_QueryStatus qs; qs = TMH_db->insert_deposit_to_transfer (TMH_db->cls, tq->deposit_serial, dd); if (qs < 0) { gorc_report (gorc, TALER_EC_GET_ORDERS_DB_STORE_TRACKING_FAILURE, &tq->coin_pub, NULL); GNUNET_free (tq); if (NULL == gorc->tq_head) gorc_resume (gorc, 0, TALER_EC_NONE); return; } /* Compute total amount *wired* */ if (0 > TALER_amount_add (&gorc->deposits_total, &gorc->deposits_total, &dd->coin_contribution)) { gorc_report (gorc, TALER_EC_GET_ORDERS_AMOUNT_ARITHMETIC_FAILURE, &tq->coin_pub, NULL); GNUNET_free (tq); if (NULL == gorc->tq_head) gorc_resume (gorc, 0, TALER_EC_NONE); return; } if (0 > TALER_amount_add (&gorc->deposit_fees_total, &gorc->deposit_fees_total, &tq->deposit_fee)) { gorc_report (gorc, TALER_EC_GET_ORDERS_AMOUNT_ARITHMETIC_FAILURE, &tq->coin_pub, NULL); GNUNET_free (tq); if (NULL == gorc->tq_head) gorc_resume (gorc, 0, TALER_EC_NONE); return; } } else { /* got a 'preliminary' reply from the exchange, simply skip */ gorc_report (gorc, TALER_EC_NONE, &tq->coin_pub, hr); } GNUNET_free (tq); if (NULL != gorc->tq_head) return; /* *all* are done, resume! */ gorc_resume (gorc, 0, TALER_EC_NONE); } /** * Function called with the result of a #TMH_EXCHANGES_find_exchange() * operation. * * @param cls closure with a `struct GetOrderRequestContext *` * @param hr HTTP response details * @param eh handle to the exchange context * @param payto_uri payto://-URI of the exchange * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available * @param exchange_trusted true if this exchange is trusted by config */ static void exchange_found_cb (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, struct TALER_EXCHANGE_Handle *eh, const char *payto_uri, const struct TALER_Amount *wire_fee, bool exchange_trusted) { struct TransferQuery *tq = cls; struct GetOrderRequestContext *gorc = tq->gorc; tq->fo = NULL; if (NULL == eh) { /* failed */ GNUNET_CONTAINER_DLL_remove (gorc->tq_head, gorc->tq_tail, tq); GNUNET_free (tq); gorc->exchange_hc = hr->http_status; gorc->exchange_ec = hr->ec; gorc_resume (gorc, MHD_HTTP_FAILED_DEPENDENCY, TALER_EC_GET_ORDERS_EXCHANGE_LOOKUP_FAILURE); return; } tq->dgh = TALER_EXCHANGE_deposits_get (eh, &gorc->hc->instance->merchant_priv, &tq->h_wire, &gorc->h_contract_terms, &tq->coin_pub, &deposit_get_cb, tq); if (NULL == tq->dgh) { GNUNET_CONTAINER_DLL_remove (gorc->tq_head, gorc->tq_tail, tq); GNUNET_free (tq); gorc_resume (gorc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_EXCHANGE_REQUEST_FAILURE); } } /** * Function called with each @a coin_pub that was deposited into the * @a h_wire account of the merchant for the @a deposit_serial as part * of the payment for the order identified by @a cls. * * Queries the exchange for the payment status associated with the * given coin. * * @param cls a `struct GetOrderRequestContext` * @param deposit_serial identifies the deposit operation * @param exchange_url URL of the exchange that issued @a coin_pub * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param h_wire hash of the merchant's wire account into which the deposit was made * @param coin_pub public key of the deposited coin */ static void deposit_cb (void *cls, uint64_t deposit_serial, const char *exchange_url, const struct GNUNET_HashCode *h_wire, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub) { struct GetOrderRequestContext *gorc = cls; struct TransferQuery *tq; tq = GNUNET_new (struct TransferQuery); tq->gorc = gorc; tq->deposit_serial = deposit_serial; GNUNET_CONTAINER_DLL_insert (gorc->tq_head, gorc->tq_tail, tq); tq->coin_pub = *coin_pub; tq->h_wire = *h_wire; tq->amount_with_fee = *amount_with_fee; tq->deposit_fee = *deposit_fee; tq->fo = TMH_EXCHANGES_find_exchange (exchange_url, NULL, GNUNET_NO, &exchange_found_cb, tq); if (NULL == tq->fo) { gorc_resume (gorc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_EXCHANGE_LOOKUP_START_FAILURE); } } /** * Clean up the session state for a GET /private/order/ID request. * * @param cls closure, must be a `struct GetOrderRequestContext *` */ static void gorc_cleanup (void *cls) { struct GetOrderRequestContext *gorc = cls; if (NULL != gorc->contract_terms) json_decref (gorc->contract_terms); if (NULL != gorc->wire_details) json_decref (gorc->wire_details); if (NULL != gorc->refund_details) json_decref (gorc->refund_details); if (NULL != gorc->wire_reports) json_decref (gorc->wire_reports); GNUNET_assert (NULL == gorc->tt); GNUNET_free (gorc); } /** * Function called with information about a refund. * It is responsible for summing up the refund amount. * * @param cls closure * @param refund_serial unique serial number of the refund * @param timestamp time of the refund (for grouping of refunds in the wallet UI) * @param coin_pub public coin from which the refund comes from * @param exchange_url URL of the exchange that issued @a coin_pub * @param rtransaction_id identificator of the refund * @param reason human-readable explanation of the refund * @param timestamp when was the refund made * @param refund_amount refund amount which is being taken from @a coin_pub * @param pending true if the this refund was not yet processed by the wallet/exchange */ static void process_refunds_cb (void *cls, uint64_t refund_serial, struct GNUNET_TIME_Absolute timestamp, const struct TALER_CoinSpendPublicKeyP *coin_pub, const char *exchange_url, uint64_t rtransaction_id, const char *reason, const struct TALER_Amount *refund_amount, bool pending) { struct GetOrderRequestContext *gorc = cls; GNUNET_assert (0 == json_array_append_new ( gorc->refund_details, json_pack ("{s:o, s:o, s:s}", "amount", TALER_JSON_from_amount (refund_amount), "timestamp", GNUNET_JSON_from_time_abs (timestamp), "reason", reason))); /* For refunded coins, we are not charged deposit fees, so subtract those again */ for (struct TransferQuery *tq = gorc->tq_head; NULL != tq; tq = tq->next) { if (0 == GNUNET_memcmp (&tq->coin_pub, coin_pub)) { GNUNET_assert (0 <= TALER_amount_subtract (&gorc->deposit_fees_total, &gorc->deposit_fees_total, &tq->deposit_fee)); } } GNUNET_assert (0 <= TALER_amount_add (&gorc->refund_amount, &gorc->refund_amount, refund_amount)); gorc->refunded = true; } /** * Function called with available wire details, to be added to * the response. * * @param cls a `struct GetOrderRequestContext` * @param wtid wire transfer subject of the wire transfer for the coin * @param exchange_url base URL of the exchange that made the payment * @param execution_time when was the payment made * @param deposit_value contribution of the coin to the total wire transfer value * @param deposit_fee deposit fee charged by the exchange for the coin * @param transfer_confirmed did the merchant confirm that a wire transfer with * @a wtid over the total amount happened? */ static void process_transfer_details (void *cls, const struct TALER_WireTransferIdentifierRawP *wtid, const char *exchange_url, struct GNUNET_TIME_Absolute execution_time, const struct TALER_Amount *deposit_value, const struct TALER_Amount *deposit_fee, bool transfer_confirmed) { struct GetOrderRequestContext *gorc = cls; json_t *wire_details = gorc->wire_details; struct TALER_Amount wired; struct GNUNET_TIME_Absolute execution_time_round = execution_time; /* Compute total amount *wired* */ GNUNET_assert (0 < TALER_amount_add (&gorc->deposits_total, &gorc->deposits_total, deposit_value)); GNUNET_assert (0 < TALER_amount_add (&gorc->deposit_fees_total, &gorc->deposit_fees_total, deposit_fee)); GNUNET_TIME_round_abs (&execution_time_round); GNUNET_assert (0 <= TALER_amount_subtract (&wired, deposit_value, deposit_fee)); GNUNET_assert (0 == json_array_append_new ( wire_details, json_pack ("{s:o, s:s, s:o, s:o, s:b}", "wtid", GNUNET_JSON_from_data_auto (wtid), "exchange_url", exchange_url, "amount", TALER_JSON_from_amount (&wired), "execution_time", GNUNET_JSON_from_time_abs (execution_time_round), "confirmed", transfer_confirmed))); } /** * Manages a GET /private/orders/ID call, checking the status of a payment and * refunds and, if necessary, constructing the URL for a payment redirect URL. * * @param rh context of the handler * @param connection the MHD connection to handle * @param[in,out] hc context with further information about the request * @return MHD result code */ MHD_RESULT TMH_private_get_orders_ID (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct GetOrderRequestContext *gorc = hc->ctx; enum GNUNET_DB_QueryStatus qs; bool paid; bool wired; bool order_only = false; struct TALER_ClaimTokenP claim_token = { 0 }; if (NULL == gorc) { /* First time here, parse request and check order is known */ GNUNET_assert (NULL != hc->infix); gorc = GNUNET_new (struct GetOrderRequestContext); hc->cc = &gorc_cleanup; hc->ctx = gorc; gorc->sc.con = connection; gorc->hc = hc; gorc->wire_details = json_array (); GNUNET_assert (NULL != gorc->wire_details); gorc->refund_details = json_array (); GNUNET_assert (NULL != gorc->refund_details); gorc->wire_reports = json_array (); GNUNET_assert (NULL != gorc->wire_reports); gorc->session_id = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "session_id"); { const char *long_poll_timeout_s; long_poll_timeout_s = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "timeout_ms"); if (NULL != long_poll_timeout_s) { unsigned long long timeout; if (1 != sscanf (long_poll_timeout_s, "%llu", &timeout)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_PARAMETER_MALFORMED, "timeout_ms must be non-negative number"); } gorc->sc.long_poll_timeout = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( GNUNET_TIME_UNIT_MILLISECONDS, timeout)); } else { gorc->sc.long_poll_timeout = GNUNET_TIME_UNIT_ZERO_ABS; } } { const char *transfer_s; transfer_s = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "transfer"); if ( (NULL != transfer_s) && (0 == strcasecmp (transfer_s, "yes")) ) gorc->transfer_status_requested = true; } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Starting GET /private/orders/%s processing with timeout %s\n", hc->infix, GNUNET_STRINGS_absolute_time_to_string ( gorc->sc.long_poll_timeout)); if (NULL != gorc->contract_terms) { /* Clear old contract terms, might be from an earlier unclaimed contract and thus could have changed during claiming. */ json_decref (gorc->contract_terms); gorc->contract_terms = NULL; gorc->fulfillment_url = NULL; } TMH_db->preflight (TMH_db->cls); qs = TMH_db->lookup_contract_terms (TMH_db->cls, hc->instance->settings.id, hc->infix, &gorc->contract_terms, &gorc->order_serial); if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { order_only = true; } 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 (GNUNET_DB_STATUS_HARD_ERROR == qs); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_DB_FETCH_CONTRACT_TERMS_ERROR, NULL); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_GET_ORDERS_ORDER_NOT_FOUND, hc->infix); } { struct GNUNET_HashCode unused; json_t *ct = NULL; /* We need the order for two cases: Either when the contract doesn't exist yet, * or when the order is claimed but unpaid, and we need the claim token. */ qs = TMH_db->lookup_order (TMH_db->cls, hc->instance->settings.id, hc->infix, &claim_token, &unused, &ct); 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 (GNUNET_DB_STATUS_HARD_ERROR == qs); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_DB_FETCH_CONTRACT_TERMS_ERROR, NULL); } if (order_only && (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ) { return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_GET_ORDERS_ORDER_NOT_FOUND, hc->infix); } if (order_only) { gorc->contract_terms = ct; } else if (NULL != ct) { json_decref (ct); } } /* extract the fulfillment URL and total amount from the contract terms! */ { struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount ("amount", &gorc->contract_amount), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (gorc->contract_terms, spec, NULL, NULL)) { GNUNET_break (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_DB_FETCH_CONTRACT_TERMS_ERROR, hc->infix); } if (0 != strcasecmp (TMH_currency, gorc->contract_amount.currency)) { GNUNET_break (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_DB_FETCH_CONTRACT_TERMS_ERROR, gorc->contract_amount.currency); } } gorc->fulfillment_url = json_string_value (json_object_get (gorc->contract_terms, "fulfillment_url")); if (! order_only) { if (GNUNET_OK != TALER_JSON_contract_hash (gorc->contract_terms, &gorc->h_contract_terms)) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_FAILED_COMPUTE_PROPOSAL_HASH, NULL); } } if (TALER_EC_NONE != gorc->wire_ec) { return TALER_MHD_reply_with_error (connection, gorc->wire_hc, gorc->wire_ec, NULL); } GNUNET_assert (NULL != gorc->contract_terms); TMH_db->preflight (TMH_db->cls); if (order_only) { paid = false; wired = false; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Order %s unclaimed, no need to lookup payment status\n", hc->infix); } else { qs = TMH_db->lookup_payment_status (TMH_db->cls, gorc->order_serial, gorc->session_id, &paid, &wired); if (0 > qs) { /* single, read-only SQL statements should never cause serialization problems, and the entry should exist as per above */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_DB_FETCH_PAYMENT_STATUS, NULL); } } if ( (! paid) && (NULL != gorc->fulfillment_url) && (NULL != gorc->session_id) ) { char *already_paid_order_id = NULL; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Running re-purchase detection for %s/%s\n", gorc->session_id, gorc->fulfillment_url); qs = TMH_db->lookup_order_by_fulfillment (TMH_db->cls, hc->instance->settings.id, gorc->fulfillment_url, gorc->session_id, &already_paid_order_id); if (0 > qs) { /* single, read-only SQL statements should never cause serialization problems, and the entry should exist as per above */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_DB_FETCH_PAYMENT_STATUS, NULL); } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { /* User did pay for this order, but under a different session; ask wallet to switch order ID */ char *taler_pay_uri; char *order_status_url; MHD_RESULT ret; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Found already paid order %s\n", already_paid_order_id); taler_pay_uri = TMH_make_taler_pay_uri (connection, hc->infix, gorc->session_id, hc->instance->settings.id, &claim_token); order_status_url = TMH_make_order_status_url (connection, hc->infix, gorc->session_id, hc->instance->settings.id, &claim_token, NULL); ret = TALER_MHD_reply_json_pack (connection, MHD_HTTP_OK, "{s:s, s:s, s:s, s:s, s:s}", "taler_pay_uri", taler_pay_uri, "order_status_url", order_status_url, "order_status", "unpaid", "already_paid_order_id", already_paid_order_id, "already_paid_fulfillment_url", gorc->fulfillment_url); GNUNET_free (taler_pay_uri); GNUNET_free (already_paid_order_id); return ret; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "No already paid order for %s/%s\n", gorc->session_id, gorc->fulfillment_url); } if (paid && (! wired) && gorc->transfer_status_requested) { /* suspend connection, wait for exchange to check wire transfer status there */ gorc->transfer_status_requested = false; /* only try ONCE */ GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TMH_currency, &gorc->deposits_total)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TMH_currency, &gorc->deposit_fees_total)); TMH_db->lookup_deposits_by_order (TMH_db->cls, gorc->order_serial, &deposit_cb, gorc); if (NULL != gorc->tq_head) { GNUNET_CONTAINER_DLL_insert (gorc_head, gorc_tail, gorc); gorc->tt = GNUNET_SCHEDULER_add_delayed (EXCHANGE_TIMEOUT, &exchange_timeout_cb, gorc); MHD_suspend_connection (connection); return MHD_YES; } } if ( (! paid) && (0 != gorc->sc.long_poll_timeout.abs_value_us) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Suspending GET /private/orders/%s\n", hc->infix); TMH_long_poll_suspend (hc->infix, gorc->session_id, gorc->fulfillment_url, hc->instance, &gorc->sc, NULL); return MHD_YES; } if (! paid) { /* User never paid for this order */ char *taler_pay_uri; char *order_status_url; MHD_RESULT ret; struct TALER_ClaimTokenP *ct = NULL; /* Already claimed, so we include the claim token so that * the order status page will show the QR code and won't run * into a redirect loop. */ if (! order_only) ct = &claim_token; taler_pay_uri = TMH_make_taler_pay_uri (connection, hc->infix, gorc->session_id, hc->instance->settings.id, ct); order_status_url = TMH_make_order_status_url (connection, hc->infix, gorc->session_id, hc->instance->settings.id, ct, NULL); ret = TALER_MHD_reply_json_pack (connection, MHD_HTTP_OK, "{s:s, s:s, s:s}", "taler_pay_uri", taler_pay_uri, "order_status_url", order_status_url, "order_status", "unpaid"); GNUNET_free (taler_pay_uri); GNUNET_free (order_status_url); return ret; } /* Here we know the user DID pay, compute refunds... */ GNUNET_assert (! order_only); GNUNET_assert (paid); /* Accumulate refunds, if any. */ { GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TMH_currency, &gorc->refund_amount)); qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, hc->instance->settings.id, &gorc->h_contract_terms, &process_refunds_cb, gorc); } if (0 > qs) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_DB_FETCH_TRANSACTION_ERROR, NULL); } /* Generate final reply, including wire details if we have them */ { MHD_RESULT ret; char *order_status_url; GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TMH_currency, &gorc->deposits_total)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TMH_currency, &gorc->deposit_fees_total)); qs = TMH_db->lookup_transfer_details_by_order (TMH_db->cls, gorc->order_serial, &process_transfer_details, gorc); if (0 > qs) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, NULL); } if (! wired) { /* we believe(d) the wire transfer did not happen yet, check if maybe in light of new evidence it did */ struct TALER_Amount expect_total; if (0 > TALER_amount_subtract (&expect_total, &gorc->contract_amount, &gorc->refund_amount)) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_CONTRACT_CONTENT_INVALID, "refund exceeds contract value"); } if (0 > TALER_amount_subtract (&expect_total, &expect_total, &gorc->deposit_fees_total)) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GET_ORDERS_CONTRACT_CONTENT_INVALID, "deposit fees exceed total minus refunds"); } if (0 >= TALER_amount_cmp (&expect_total, &gorc->deposits_total)) { struct GNUNET_TIME_Absolute timestamp; /* expect_total <= gorc->deposits_total: good: we got paid */ wired = true; qs = TMH_db->mark_order_wired (TMH_db->cls, gorc->order_serial); GNUNET_break (qs >= 0); /* just warn if transaction failed */ { struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_absolute_time ("timestamp", ×tamp), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_internal_json_data (connection, gorc->contract_terms, spec); if (GNUNET_YES != res) { GNUNET_break (0); return res; } } TMH_notify_order_change (hc->instance, hc->infix, true, /* paid */ false, /* technically unknown, but OK here */ true, /* wired */ timestamp, gorc->order_serial); } } { struct GNUNET_HashCode *h_contract = NULL; /* In a session-bound payment, allow the browser to check the order * status page (e.g. to get a refund). * * Note that we don't allow this outside of session-based payment, as * otherwise this becomes an oracle to convert order_id to h_contract. */if (NULL != gorc->session_id) h_contract = &gorc->h_contract_terms; order_status_url = TMH_make_order_status_url (connection, hc->infix, gorc->session_id, hc->instance->settings.id, &claim_token, h_contract); } ret = TALER_MHD_reply_json_pack (connection, MHD_HTTP_OK, "{s:o, s:I, s:I, s:o, s:O," " s:s, s:b, s:b, s:o, s:o, s:o, s:s}", "wire_reports", gorc->wire_reports, "exchange_ec", (json_int_t) gorc->exchange_ec, "exchange_hc", (json_int_t) gorc->exchange_hc, "deposit_total", TALER_JSON_from_amount ( &gorc->deposits_total), "contract_terms", gorc->contract_terms, "order_status", "paid", "refunded", gorc->refunded, "wired", wired, "refund_amount", TALER_JSON_from_amount ( &gorc->refund_amount), "wire_details", gorc->wire_details, "refund_details", gorc->refund_details, "order_status_url", order_status_url); GNUNET_free (order_status_url); gorc->wire_details = NULL; gorc->wire_reports = NULL; gorc->refund_details = NULL; return ret; } }