/*
This file is part of TALER
(C) 2017-2021 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 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
#include "taler-merchant-httpd_mhd.h"
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_helper.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;
/**
* 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;
/**
* Database event we are waiting on to be resuming
* for payment or refunds.
*/
struct GNUNET_DB_EventHandler *eh;
/**
* Database event we are waiting on to be resuming
* for session capture.
*/
struct GNUNET_DB_EventHandler *session_eh;
/**
* 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 request is currently suspended.
*/
bool suspended;
/**
* Set to true if this payment has been refunded and
* @e refund_amount is initialized.
*/
bool refunded;
/**
* Set to true if this payment has been refunded and
* some refunds remain to be picked up by the wallet.
*/
bool refund_pending;
/**
* 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_assert (gorc->suspended);
GNUNET_CONTAINER_DLL_remove (gorc_head,
gorc_tail,
gorc);
gorc->suspended = false;
MHD_resume_connection (gorc->sc.con);
TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
}
/**
* We have received a trigger from the database
* that we should (possibly) resume the request.
*
* @param cls a `struct GetOrderRequestContext` to resume
* @param extra string encoding refund amount (or NULL)
* @param extra_size number of bytes in @a extra
*/
static void
resume_by_event (void *cls,
const void *extra,
size_t extra_size)
{
struct GetOrderRequestContext *gorc = cls;
(void) extra;
(void) extra_size;
if (! gorc->suspended)
return; /* duplicate event is possible */
gorc->suspended = false;
GNUNET_CONTAINER_DLL_remove (gorc_head,
gorc_tail,
gorc);
MHD_resume_connection (gorc->sc.con);
TALER_MHD_daemon_trigger (); /* 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,
GNUNET_JSON_PACK (
TALER_JSON_pack_ec (ec),
TMH_pack_exchange_reply (exchange_hr),
GNUNET_JSON_pack_data_auto ("coin_pub",
coin_pub))));
else
GNUNET_assert (0 ==
json_array_append_new (
gorc->wire_reports,
GNUNET_JSON_PACK (
TALER_JSON_pack_ec (ec),
GNUNET_JSON_pack_data_auto ("coin_pub",
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_GENERIC_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_MERCHANT_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_GENERIC_DB_STORE_FAILED,
&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_MERCHANT_PRIVATE_GET_ORDERS_ID_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_MERCHANT_PRIVATE_GET_ORDERS_ID_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 == hr)
{
/* failed */
GNUNET_CONTAINER_DLL_remove (gorc->tq_head,
gorc->tq_tail,
tq);
GNUNET_free (tq);
gorc_resume (gorc,
MHD_HTTP_GATEWAY_TIMEOUT,
TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT);
return;
}
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_BAD_GATEWAY,
TALER_EC_MERCHANT_GENERIC_EXCHANGE_CONNECT_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_MERCHANT_GET_ORDERS_ID_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_MERCHANT_GET_ORDERS_ID_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);
if (NULL != gorc->eh)
{
TMH_db->event_listen_cancel (gorc->eh);
gorc->eh = NULL;
}
if (NULL != gorc->session_eh)
{
TMH_db->event_listen_cancel (gorc->session_eh);
gorc->session_eh = NULL;
}
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 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,
GNUNET_JSON_PACK (
TALER_JSON_pack_amount ("amount",
refund_amount),
GNUNET_JSON_pack_time_abs ("timestamp",
timestamp),
GNUNET_JSON_pack_string ("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;
gorc->refund_pending = pending;
}
/**
* 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,
GNUNET_JSON_PACK (
GNUNET_JSON_pack_data_auto ("wtid",
wtid),
GNUNET_JSON_pack_string ("exchange_url",
exchange_url),
TALER_JSON_pack_amount ("amount",
&wired),
GNUNET_JSON_pack_time_abs ("execution_time",
execution_time_round),
GNUNET_JSON_pack_bool ("confirmed",
transfer_confirmed))));
}
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 };
const char *summary;
struct GNUNET_TIME_Absolute timestamp;
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");
/* process 'transfer' argument */
{
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;
}
/* process 'timeout_ms' argument */
{
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 int timeout_ms;
char dummy;
struct GNUNET_TIME_Relative timeout;
if (1 != sscanf (long_poll_timeout_s,
"%u%c",
&timeout_ms,
&dummy))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"timeout_ms must be non-negative number");
}
timeout = GNUNET_TIME_relative_multiply (
GNUNET_TIME_UNIT_MILLISECONDS,
timeout_ms);
gorc->sc.long_poll_timeout
= GNUNET_TIME_relative_to_absolute (timeout);
if (! GNUNET_TIME_relative_is_zero (timeout))
{
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_CRYPTO_hash (hc->infix,
strlen (hc->infix),
&pay_eh.h_order_id);
gorc->eh = TMH_db->event_listen (TMH_db->cls,
&pay_eh.header,
timeout,
&resume_by_event,
gorc);
if ( (NULL != gorc->session_id) &&
(NULL != gorc->fulfillment_url) )
{
struct TMH_SessionEventP session_eh = {
.header.size = htons (sizeof (session_eh)),
.header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED),
.merchant_pub = hc->instance->merchant_pub
};
GNUNET_CRYPTO_hash (gorc->session_id,
strlen (gorc->session_id),
&session_eh.h_session_id);
GNUNET_CRYPTO_hash (gorc->fulfillment_url,
strlen (gorc->fulfillment_url),
&session_eh.h_fulfillment_url);
gorc->session_eh = TMH_db->event_listen (TMH_db->cls,
&session_eh.header,
timeout,
&resume_by_event,
gorc);
}
}
}
else
{
gorc->sc.long_poll_timeout = GNUNET_TIME_UNIT_ZERO_ABS;
}
}
} /* end first-time per-request initialization */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"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)
{
/* Free memory filled with old contract terms before fetching the latest
ones from the DB. Note that we cannot simply skip the database
interaction as the contract terms loaded previously might be from an
earlier *unclaimed* order state (which we loaded in a previous
invocation of this function and we are back here due to long polling)
and thus the contract terms could have changed during claiming. Thus,
we need to fetch the latest contract terms from the DB again. *///
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,
NULL);
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_GENERIC_DB_FETCH_FAILED,
"contract terms");
}
{
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_GENERIC_DB_FETCH_FAILED,
"order");
}
if (order_only && (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) )
{
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
hc->infix);
}
if (order_only)
{
gorc->contract_terms = ct;
}
else if (NULL != ct)
{
json_decref (ct);
}
}
/* extract the fulfillment URL, total amount, summary and timestamp
from the contract terms! */
{
struct GNUNET_JSON_Specification spec[] = {
TALER_JSON_spec_amount ("amount",
TMH_currency,
&gorc->contract_amount),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_string ("fulfillment_url",
&gorc->fulfillment_url)),
GNUNET_JSON_spec_string ("summary",
&summary),
TALER_JSON_spec_absolute_time ("timestamp",
×tamp),
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_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
hc->infix);
}
}
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_GENERIC_FAILED_COMPUTE_JSON_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_GENERIC_DB_FETCH_FAILED,
"payment status");
}
}
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_GENERIC_DB_FETCH_FAILED,
"order by fulfillment");
}
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);
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 (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
"host");
}
ret = TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_OK,
GNUNET_JSON_pack_string ("taler_pay_uri",
taler_pay_uri),
GNUNET_JSON_pack_string ("order_status_url",
order_status_url),
GNUNET_JSON_pack_string ("order_status",
"unpaid"),
GNUNET_JSON_pack_string ("already_paid_order_id",
already_paid_order_id),
GNUNET_JSON_pack_string ("already_paid_fulfillment_url",
gorc->fulfillment_url),
TALER_JSON_pack_amount ("total_amount",
&gorc->contract_amount),
GNUNET_JSON_pack_string ("summary",
summary),
GNUNET_JSON_pack_time_abs ("creation_time",
timestamp));
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) &&
(! order_only) )
{
if (GNUNET_TIME_absolute_is_future (gorc->sc.long_poll_timeout))
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Suspending GET /private/orders/%s\n",
hc->infix);
GNUNET_CONTAINER_DLL_insert (gorc_head,
gorc_tail,
gorc);
gorc->suspended = true;
MHD_suspend_connection (gorc->sc.con);
return MHD_YES;
}
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Order %s claimed but not paid yet\n",
hc->infix);
return TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_OK,
GNUNET_JSON_pack_object_incref ("contract_terms",
gorc->contract_terms),
GNUNET_JSON_pack_string ("order_status",
"claimed"));
}
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_set_zero (TMH_currency,
&gorc->deposits_total));
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_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->suspended = true;
MHD_suspend_connection (connection);
gorc->tt = GNUNET_SCHEDULER_add_delayed (EXCHANGE_TIMEOUT,
&exchange_timeout_cb,
gorc);
return MHD_YES;
}
}
if ( (! paid) &&
(GNUNET_TIME_absolute_is_future (gorc->sc.long_poll_timeout)) )
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Suspending GET /private/orders/%s\n",
hc->infix);
GNUNET_assert (! gorc->suspended);
GNUNET_CONTAINER_DLL_insert (gorc_head,
gorc_tail,
gorc);
gorc->suspended = true;
MHD_suspend_connection (gorc->sc.con);
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,
NULL);
ret = TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_OK,
GNUNET_JSON_pack_string ("taler_pay_uri",
taler_pay_uri),
GNUNET_JSON_pack_string ("order_status_url",
order_status_url),
GNUNET_JSON_pack_string ("order_status",
"unpaid"),
TALER_JSON_pack_amount ("total_amount",
&gorc->contract_amount),
GNUNET_JSON_pack_string ("summary",
summary),
GNUNET_JSON_pack_time_abs ("creation_time",
timestamp));
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_set_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_GENERIC_DB_FETCH_FAILED,
"detailed refunds");
}
/* Generate final reply, including wire details if we have them */
{
MHD_RESULT ret;
char *order_status_url;
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (TMH_currency,
&gorc->deposits_total));
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_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_GENERIC_DB_FETCH_FAILED,
"transfer details");
}
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_MERCHANT_GENERIC_DB_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_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
"deposit fees exceed total minus refunds");
}
if (0 >=
TALER_amount_cmp (&expect_total,
&gorc->deposits_total))
{
/* 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 */
TMH_notify_order_change (hc->instance,
TMH_OSF_PAID
| TMH_OSF_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,
GNUNET_JSON_pack_array_steal ("wire_reports",
gorc->wire_reports),
GNUNET_JSON_pack_uint64 ("exchange_code",
gorc->exchange_ec),
GNUNET_JSON_pack_uint64 ("exchange_http_status",
gorc->exchange_hc),
/* legacy: */
GNUNET_JSON_pack_uint64 ("exchange_ec",
gorc->exchange_ec),
/* legacy: */
GNUNET_JSON_pack_uint64 ("exchange_hc",
gorc->exchange_hc),
TALER_JSON_pack_amount ("deposit_total",
&gorc->deposits_total),
GNUNET_JSON_pack_object_incref ("contract_terms",
gorc->contract_terms),
GNUNET_JSON_pack_string ("order_status",
"paid"),
GNUNET_JSON_pack_bool ("refunded",
gorc->refunded),
GNUNET_JSON_pack_bool ("wired",
wired),
GNUNET_JSON_pack_bool ("refund_pending",
gorc->refund_pending),
TALER_JSON_pack_amount ("refund_amount",
&gorc->refund_amount),
GNUNET_JSON_pack_array_steal ("wire_details",
gorc->wire_details),
GNUNET_JSON_pack_array_steal ("refund_details",
gorc->refund_details),
GNUNET_JSON_pack_string ("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;
}
}