/*
This file is part of TALER
(C) 2019, 2020 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
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.c
* @brief implement GET /orders
* @author Christian Grothoff
*/
#include "platform.h"
#include "taler-merchant-httpd_private-get-orders.h"
#include
/**
* Stores state for adding an order to the array for the response.
*/
struct AddOrderState
{
/**
* The array of orders.
*/
json_t *pa;
/**
* The name of the instance we are querying for.
*/
const char *instance_id;
/**
* The result after adding the orders (0 for okay, anything else for an error).
*/
int result;
/**
* In the case of an error, what message to respond with.
*/
const char *ec_msg;
};
/**
* A pending GET /orders request that is in long polling mode.
*/
struct TMH_PendingOrder
{
/**
* Kept in a DLL.
*/
struct TMH_PendingOrder *prev;
/**
* Kept in a DLL.
*/
struct TMH_PendingOrder *next;
/**
* Which connection was suspended.
*/
struct MHD_Connection *con;
/**
* Associated heap node.
*/
struct GNUNET_CONTAINER_HeapNode *hn;
/**
* Which instance is this client polling? This also defines
* which DLL this struct is part of.
*/
struct TMH_MerchantInstance *mi;
/**
* At what time does this request expire? If set in the future, we
* may wait this long for a payment to arrive before responding.
*/
struct GNUNET_TIME_Absolute long_poll_timeout;
/**
* State for adding orders. The array `pa` must be
* json_decref()'ed when done with the `struct TMH_PendingOrder`!
*/
struct AddOrderState *aos;
/**
* Filter to apply.
*/
struct TALER_MERCHANTDB_OrderFilter of;
};
/**
* Task to timeout pending orders.
*/
static struct GNUNET_SCHEDULER_Task *order_timeout_task;
/**
* Heap for orders in long polling awaiting timeout.
*/
static struct GNUNET_CONTAINER_Heap *order_timeout_heap;
/**
* We are shutting down (or an instance is being deleted), force resume of all
* GET /orders requests.
*
* @param mi instance to force resuming for
*/
void
TMH_force_get_orders_resume (struct TMH_MerchantInstance *mi)
{
struct TMH_PendingOrder *po;
while (NULL != (po = mi->po_head))
{
GNUNET_CONTAINER_DLL_remove (mi->po_head,
mi->po_tail,
po);
GNUNET_assert (po ==
GNUNET_CONTAINER_heap_remove_root (order_timeout_heap));
MHD_resume_connection (po->con);
json_decref (po->aos->pa);
GNUNET_free (po);
}
if (NULL != order_timeout_task)
{
GNUNET_SCHEDULER_cancel (order_timeout_task);
order_timeout_task = NULL;
}
if (NULL != order_timeout_heap)
{
GNUNET_CONTAINER_heap_destroy (order_timeout_heap);
order_timeout_heap = NULL;
}
}
/**
* Task run to trigger timeouts on GET /orders requests with long polling.
*
* @param cls unused
*/
static void
order_timeout (void *cls)
{
struct TMH_PendingOrder *po;
struct TMH_MerchantInstance *mi;
(void) cls;
order_timeout_task = NULL;
while (1)
{
po = GNUNET_CONTAINER_heap_peek (order_timeout_heap);
if (NULL == po)
{
/* release data structure, we don't need it right now */
GNUNET_CONTAINER_heap_destroy (order_timeout_heap);
order_timeout_heap = NULL;
return;
}
if (0 !=
GNUNET_TIME_absolute_get_remaining (
po->long_poll_timeout).rel_value_us)
break;
GNUNET_assert (po ==
GNUNET_CONTAINER_heap_remove_root (order_timeout_heap));
po->hn = NULL;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Resuming long polled job due to timeout\n");
mi = po->mi;
GNUNET_CONTAINER_DLL_remove (mi->po_head,
mi->po_tail,
po);
json_decref (po->aos->pa);
MHD_resume_connection (po->con);
TMH_trigger_daemon (); /* we resumed, kick MHD */
GNUNET_free (po);
}
order_timeout_task = GNUNET_SCHEDULER_add_at (po->long_poll_timeout,
&order_timeout,
NULL);
}
/**
* Cleanup our "context", where we stored the JSON array
* we are building for the response.
*
* @param ctx context to clean up, must be a `struct AddOrderState *`
*/
static void
cleanup (void *ctx)
{
struct AddOrderState *aos = ctx;
json_decref (aos->pa);
GNUNET_free (aos);
}
/**
* 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
*/
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)
{
struct TALER_Amount *total_refund_amount = cls;
GNUNET_assert (0 <=
TALER_amount_add (total_refund_amount,
total_refund_amount,
refund_amount));
}
/**
* Add order details to our JSON array.
*
* @param[in,out] cls a `json_t *` JSON array to build
* @param order_id ID of the order
* @param order_serial serial ID of the order
* @param creation_time when was the order created
*/
static void
add_order (void *cls,
const char *order_id,
uint64_t order_serial,
struct GNUNET_TIME_Absolute creation_time)
{
struct AddOrderState *aos = cls;
json_t *contract_terms;
struct GNUNET_HashCode h_contract_terms;
enum GNUNET_DB_QueryStatus qs;
bool refundable = false;
bool paid;
{
qs = TMH_db->lookup_order_status (TMH_db->cls,
aos->instance_id,
order_id,
&h_contract_terms,
&paid);
/* qs == 0: contract terms don't exist, so the order cannot be paid. */
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
paid = false;
if (qs < 0)
{
aos->result = 1;
aos->ec_msg = "failed to lookup order status in database";
return;
}
}
if (paid)
{
/* if the order was paid, it must have been claimed, so use
lookup_contract_terms to avoid the order being deleted in the db. */
uint64_t os;
qs = TMH_db->lookup_contract_terms (TMH_db->cls,
aos->instance_id,
order_id,
&contract_terms,
&os);
}
else
{
qs = TMH_db->lookup_order (TMH_db->cls,
aos->instance_id,
order_id,
NULL,
&contract_terms);
}
if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
{
aos->result = 1;
aos->ec_msg = "failed to lookup order in database";
json_decref (contract_terms);
return;
}
{
struct TALER_Amount order_amount;
struct GNUNET_TIME_Absolute rd;
struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
struct GNUNET_JSON_Specification spec[] = {
TALER_JSON_spec_amount ("amount",
&order_amount),
GNUNET_JSON_spec_absolute_time ("refund_deadline",
&rd),
GNUNET_JSON_spec_end ()
};
if (GNUNET_OK !=
GNUNET_JSON_parse (contract_terms,
spec,
NULL, NULL))
{
aos->result = 1;
aos->ec_msg = "failed to parse order contract terms";
json_decref (contract_terms);
return;
}
if ((now.abs_value_us <= rd.abs_value_us) &&
paid)
{
struct TALER_Amount refund_amount;
GNUNET_assert (GNUNET_OK ==
TALER_amount_get_zero (TMH_currency,
&refund_amount));
qs = TMH_db->lookup_refunds_detailed (TMH_db->cls,
aos->instance_id,
&h_contract_terms,
&process_refunds_cb,
&refund_amount);
if (0 > qs)
{
aos->result = 1;
aos->ec_msg = "failed to lookup order refunds in database";
json_decref (contract_terms);
return;
}
if (0 < TALER_amount_cmp (&refund_amount,
&order_amount))
refundable = true;
}
}
GNUNET_assert (0 ==
json_array_append_new (
aos->pa,
json_pack (
"{s:s, s:I, s:o, s:O, s:O, s:b, s:b}",
"order_id",
order_id,
"row_id",
(json_int_t) order_serial,
"timestamp",
GNUNET_JSON_from_time_abs (creation_time),
"amount",
json_object_get (contract_terms,
"amount"),
"summary",
json_object_get (contract_terms,
"summary"),
"refundable",
refundable,
"paid",
paid)));
json_decref (contract_terms);
}
/**
* There has been a change or addition of a new @a order_id. Wake up
* long-polling clients that may have been waiting for this event.
*
* @param mi the instance where the order changed
* @param order_id the order that changed
* @param paid is the order paid by the customer?
* @param refunded was the order refunded?
* @param wire was the merchant paid via wire transfer?
* @param date execution date of the order
* @param order_serial_id serial ID of the order in the database
*/
void
TMH_notify_order_change (struct TMH_MerchantInstance *mi,
const char *order_id,
bool paid,
bool refunded,
bool wired,
struct GNUNET_TIME_Absolute date,
uint64_t order_serial_id)
{
struct TMH_PendingOrder *pn;
for (struct TMH_PendingOrder *po = mi->po_head;
NULL != po;
po = pn)
{
pn = po->next;
if (! ( ( ((TALER_EXCHANGE_YNA_YES == po->of.paid) == paid) ||
(TALER_EXCHANGE_YNA_ALL == po->of.paid) ) &&
( ((TALER_EXCHANGE_YNA_YES == po->of.refunded) == refunded) ||
(TALER_EXCHANGE_YNA_ALL == po->of.refunded) ) &&
( ((TALER_EXCHANGE_YNA_YES == po->of.wired) == wired) ||
(TALER_EXCHANGE_YNA_ALL == po->of.wired) ) ) )
continue;
if (po->of.delta > 0)
{
if (order_serial_id < po->of.start_row)
continue;
if (date.abs_value_us < po->of.date.abs_value_us)
continue;
po->of.delta--;
}
else
{
if (order_serial_id > po->of.start_row)
continue;
if (date.abs_value_us > po->of.date.abs_value_us)
continue;
po->of.delta++;
}
add_order (po->aos,
order_id,
order_serial_id,
date);
GNUNET_CONTAINER_DLL_remove (mi->po_head,
mi->po_tail,
po);
GNUNET_assert (po ==
GNUNET_CONTAINER_heap_remove_node (po->hn));
MHD_resume_connection (po->con);
TMH_trigger_daemon (); /* we resumed, kick MHD */
json_decref (po->aos->pa);
GNUNET_free (po);
}
}
/**
* Handle a GET "/orders" request.
*
* @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 (const struct TMH_RequestHandler *rh,
struct MHD_Connection *connection,
struct TMH_HandlerContext *hc)
{
struct AddOrderState *aos;
enum GNUNET_DB_QueryStatus qs;
struct TALER_MERCHANTDB_OrderFilter of;
if (NULL != hc->ctx)
{
/* resumed from long-polling, return answer we already have
in 'hc->ctx' */
struct AddOrderState *aos = hc->ctx;
if (0 != aos->result)
{
GNUNET_break (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_ORDERS_GET_DB_LOOKUP_ERROR,
aos->ec_msg);
}
return TALER_MHD_reply_json_pack (connection,
MHD_HTTP_OK,
"{s:O}",
"orders", aos->pa);
}
if (! (TALER_arg_to_yna (connection,
"paid",
TALER_EXCHANGE_YNA_ALL,
&of.paid)) )
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_PARAMETER_MALFORMED,
"paid");
if (! (TALER_arg_to_yna (connection,
"refunded",
TALER_EXCHANGE_YNA_ALL,
&of.refunded)) )
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_PARAMETER_MALFORMED,
"refunded");
if (! (TALER_arg_to_yna (connection,
"wired",
TALER_EXCHANGE_YNA_ALL,
&of.wired)) )
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_PARAMETER_MALFORMED,
"wired");
{
const char *start_row_str;
start_row_str = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"start");
if (NULL == start_row_str)
{
of.start_row = INT64_MAX;
}
else
{
char dummy[2];
unsigned long long ull;
if (1 !=
sscanf (start_row_str,
"%llu%1s",
&ull,
dummy))
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_PARAMETER_MALFORMED,
"date");
of.start_row = (uint64_t) ull;
}
}
{
const char *delta_str;
delta_str = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"delta");
if (NULL == delta_str)
{
of.delta = -20;
}
else
{
char dummy[2];
long long ll;
if (1 !=
sscanf (delta_str,
"%lld%1s",
&ll,
dummy))
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_PARAMETER_MALFORMED,
"delta");
of.delta = (uint64_t) ll;
}
}
{
const char *date_str;
date_str = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"date");
if (NULL == date_str)
{
if (of.delta > 0)
of.date = GNUNET_TIME_UNIT_ZERO_ABS;
else
of.date = GNUNET_TIME_UNIT_FOREVER_ABS;
}
else
{
if (GNUNET_OK !=
GNUNET_STRINGS_fancy_time_to_absolute (date_str,
&of.date))
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_PARAMETER_MALFORMED,
"date");
}
}
{
const char *timeout_ms_str;
timeout_ms_str = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"timeout_ms");
if (NULL == timeout_ms_str)
{
of.timeout = GNUNET_TIME_UNIT_ZERO;
}
else
{
char dummy[2];
unsigned long long ull;
if (1 !=
sscanf (timeout_ms_str,
"%lld%1s",
&ull,
dummy))
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_PARAMETER_MALFORMED,
"timeout_ms");
of.timeout = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS,
ull);
}
}
aos = GNUNET_new (struct AddOrderState);
GNUNET_assert (NULL != aos);
aos->pa = json_array ();
aos->instance_id = hc->instance->settings.id;
aos->result = 0;
GNUNET_assert (NULL != aos->pa);
{
qs = TMH_db->lookup_orders (TMH_db->cls,
hc->instance->settings.id,
&of,
&add_order,
aos);
if ((0 > qs) ||
(0 != aos->result))
{
int aos_result = aos->result;
const char *aos_ec_msg = aos->ec_msg;
GNUNET_break (0);
json_decref (aos->pa);
GNUNET_free (aos);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_ORDERS_GET_DB_LOOKUP_ERROR,
0 != aos_result ?
aos_ec_msg :
"failed to lookup orders in database");
}
}
if ( (0 == qs) &&
(of.timeout.rel_value_us > 0) )
{
struct TMH_MerchantInstance *mi = hc->instance;
struct TMH_PendingOrder *po;
/* setup timeout heap (if not yet exists) */
if (NULL == order_timeout_heap)
order_timeout_heap
= GNUNET_CONTAINER_heap_create (GNUNET_CONTAINER_HEAP_ORDER_MIN);
hc->ctx = aos;
hc->cc = &cleanup;
po = GNUNET_new (struct TMH_PendingOrder);
po->mi = mi;
po->con = connection;
po->aos = aos;
json_incref (po->aos->pa);
po->hn = GNUNET_CONTAINER_heap_insert (order_timeout_heap,
po,
po->long_poll_timeout.abs_value_us);
po->long_poll_timeout = GNUNET_TIME_relative_to_absolute (of.timeout);
po->of = of;
GNUNET_CONTAINER_DLL_insert (mi->po_head,
mi->po_tail,
po);
MHD_suspend_connection (connection);
/* start timeout task */
po = GNUNET_CONTAINER_heap_peek (order_timeout_heap);
if (NULL != order_timeout_task)
GNUNET_SCHEDULER_cancel (order_timeout_task);
order_timeout_task = GNUNET_SCHEDULER_add_at (po->long_poll_timeout,
&order_timeout,
NULL);
return MHD_YES;
}
{
json_t *pa = aos->pa;
GNUNET_free (aos);
return TALER_MHD_reply_json_pack (connection,
MHD_HTTP_OK,
"{s:o}",
"orders", pa);
}
}
/* end of taler-merchant-httpd_private-get-orders.c */