/*
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));
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)
{
struct GNUNET_HashCode unused;
/* We don't have contract terms, but the order may still exist. */
qs = TMH_db->lookup_order (TMH_db->cls,
hc->instance->settings.id,
hc->infix,
&claim_token,
&unused,
&gorc->contract_terms);
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);
}
/* 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;
}
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;
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;
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);
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;
}
}
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,
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;
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);
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);
}
}
order_status_url = TMH_make_order_status_url (connection,
hc->infix,
gorc->session_id,
hc->instance->settings.id,
&claim_token);
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;
}
}