summaryrefslogtreecommitdiff
path: root/src/backend/taler-merchant-httpd_get-orders-ID.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/backend/taler-merchant-httpd_get-orders-ID.c')
-rw-r--r--src/backend/taler-merchant-httpd_get-orders-ID.c654
1 files changed, 654 insertions, 0 deletions
diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c
new file mode 100644
index 00000000..e86e4e4b
--- /dev/null
+++ b/src/backend/taler-merchant-httpd_get-orders-ID.c
@@ -0,0 +1,654 @@
+/*
+ This file is part of TALER
+ (C) 2014-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 <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file backend/taler-merchant-httpd_refund_lookup.c
+ * @brief refund handling logic
+ * @author Marcello Stanisci
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <taler/taler_signatures.h>
+#include <taler/taler_json_lib.h>
+#include <taler/taler_exchange_service.h>
+#include "taler-merchant-httpd.h"
+#include "taler-merchant-httpd_exchanges.h"
+#include "taler-merchant-httpd_refund.h"
+
+/**
+ * How often do we retry DB transactions on serialization failures?
+ */
+#define MAX_RETRIES 5
+
+/**
+ * Information we keep for each coin to be refunded.
+ */
+struct CoinRefund
+{
+
+ /**
+ * Kept in a DLL.
+ */
+ struct CoinRefund *next;
+
+ /**
+ * Kept in a DLL.
+ */
+ struct CoinRefund *prev;
+
+ /**
+ * Request to connect to the target exchange.
+ */
+ struct TMH_EXCHANGES_FindOperation *fo;
+
+ /**
+ * Handle for the refund operation with the exchange.
+ */
+ struct TALER_EXCHANGE_RefundHandle *rh;
+
+ /**
+ * PRD this operation is part of.
+ */
+ struct ProcessRefundData *prd;
+
+ /**
+ * URL of the exchange for this @e coin_pub.
+ */
+ char *exchange_url;
+
+ /**
+ * Coin to refund.
+ */
+ struct TALER_CoinSpendPublicKeyP coin_pub;
+
+ /**
+ * Refund transaction ID to use.
+ */
+ uint64_t rtransaction_id;
+
+ /**
+ * Amount to refund.
+ */
+ struct TALER_Amount refund_amount;
+
+ /**
+ * Applicable refund transaction fee.
+ */
+ struct TALER_Amount refund_fee;
+
+ /**
+ * Public key of the exchange affirming the refund.
+ */
+ struct TALER_ExchangePublicKeyP exchange_pub;
+
+ /**
+ * Signature of the exchange affirming the refund.
+ */
+ struct TALER_ExchangeSignatureP exchange_sig;
+
+ /**
+ * HTTP status from the exchange, #MHD_HTTP_OK if
+ * @a exchange_pub and @a exchange_sig are valid.
+ */
+ unsigned int exchange_status;
+
+ /**
+ * HTTP error code from the exchange.
+ */
+ enum TALER_ErrorCode exchange_code;
+
+ /**
+ * Fully reply from the exchange, only possibly set if
+ * we got a JSON reply and a non-#MHD_HTTP_OK error code
+ */
+ json_t *exchange_reply;
+
+};
+
+
+/**
+ * Closure for #process_refunds_cb.
+ */
+struct ProcessRefundData
+{
+ /**
+ * Must be first for #handle_mhd_completion_callback() cleanup
+ * logic to work.
+ */
+ struct TM_HandlerContext hc;
+
+ /**
+ * Hashed version of contract terms.
+ */
+ struct GNUNET_HashCode h_contract_terms;
+
+ /**
+ * DLL of (suspended) requests.
+ */
+ struct ProcessRefundData *next;
+
+ /**
+ * DLL of (suspended) requests.
+ */
+ struct ProcessRefundData *prev;
+
+ /**
+ * Head of DLL of coin refunds for this request.
+ */
+ struct CoinRefund *cr_head;
+
+ /**
+ * Tail of DLL of coin refunds for this request.
+ */
+ struct CoinRefund *cr_tail;
+
+ /**
+ * Both public and private key are needed by the callback
+ */
+ const struct MerchantInstance *merchant;
+
+ /**
+ * Connection we are handling.
+ */
+ struct MHD_Connection *connection;
+
+ /**
+ * Did we suspend @a connection?
+ */
+ int suspended;
+
+ /**
+ * Return code: #TALER_EC_NONE if successful.
+ */
+ enum TALER_ErrorCode ec;
+};
+
+
+/**
+ * HEad of DLL of (suspended) requests.
+ */
+static struct ProcessRefundData *prd_head;
+
+/**
+ * Tail of DLL of (suspended) requests.
+ */
+static struct ProcessRefundData *prd_tail;
+
+
+/**
+ * Clean up memory in @a cls, the connection was closed.
+ *
+ * @param cls a `struct ProcessRefundData` to clean up.
+ */
+static void
+cleanup_prd (struct TM_HandlerContext *cls)
+{
+ struct ProcessRefundData *prd = (struct ProcessRefundData *) cls;
+ struct CoinRefund *cr;
+
+ while (NULL != (cr = prd->cr_head))
+ {
+ GNUNET_CONTAINER_DLL_remove (prd->cr_head,
+ prd->cr_tail,
+ cr);
+ if (NULL != cr->fo)
+ {
+ TMH_EXCHANGES_find_exchange_cancel (cr->fo);
+ cr->fo = NULL;
+ }
+ if (NULL != cr->rh)
+ {
+ TALER_EXCHANGE_refund_cancel (cr->rh);
+ cr->rh = NULL;
+ }
+ if (NULL != cr->exchange_reply)
+ {
+ json_decref (cr->exchange_reply);
+ cr->exchange_reply = NULL;
+ }
+ GNUNET_free (cr->exchange_url);
+ GNUNET_free (cr);
+ }
+ GNUNET_free (prd);
+}
+
+
+/**
+ * Check if @a prd has sub-activities still pending.
+ *
+ * @param prd request to check
+ * @return #GNUNET_YES if activities are still pending
+ */
+static int
+prd_pending (struct ProcessRefundData *prd)
+{
+ int pending = GNUNET_NO;
+
+ for (struct CoinRefund *cr = prd->cr_head;
+ NULL != cr;
+ cr = cr->next)
+ {
+ if ( (NULL != cr->fo) ||
+ (NULL != cr->rh) )
+ {
+ pending = GNUNET_YES;
+ break;
+ }
+ }
+ return pending;
+}
+
+
+/**
+ * Check if @a prd is ready to be resumed, and if so, do it.
+ *
+ * @param prd refund request to be possibly ready
+ */
+static void
+check_resume_prd (struct ProcessRefundData *prd)
+{
+ if (prd_pending (prd))
+ return;
+ GNUNET_CONTAINER_DLL_remove (prd_head,
+ prd_tail,
+ prd);
+ GNUNET_assert (prd->suspended);
+ prd->suspended = GNUNET_NO;
+ MHD_resume_connection (prd->connection);
+ TMH_trigger_daemon ();
+}
+
+
+/**
+ * Callbacks of this type are used to serve the result of submitting a
+ * refund request to an exchange.
+ *
+ * @param cls a `struct CoinRefund`
+ * @param hr HTTP response data
+ * @param exchange_pub exchange key used to sign refund confirmation
+ * @param exchange_sig exchange's signature over refund
+ */
+static void
+refund_cb (void *cls,
+ const struct TALER_EXCHANGE_HttpResponse *hr,
+ const struct TALER_ExchangePublicKeyP *exchange_pub,
+ const struct TALER_ExchangeSignatureP *exchange_sig)
+{
+ struct CoinRefund *cr = cls;
+
+ cr->rh = NULL;
+ cr->exchange_status = hr->http_status;
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Exchange refund status for coin %s is %u\n",
+ TALER_B2S (&cr->coin_pub),
+ hr->http_status);
+ if (MHD_HTTP_OK != hr->http_status)
+ {
+ cr->exchange_code = hr->ec;
+ cr->exchange_reply = json_incref ((json_t*) hr->reply);
+ }
+ else
+ {
+ enum GNUNET_DB_QueryStatus qs;
+
+ cr->exchange_pub = *exchange_pub;
+ cr->exchange_sig = *exchange_sig;
+ qs = db->put_refund_proof (db->cls,
+ &cr->prd->merchant->pubkey,
+ &cr->prd->h_contract_terms,
+ &cr->coin_pub,
+ cr->rtransaction_id,
+ exchange_pub,
+ exchange_sig);
+ if (0 >= qs)
+ {
+ /* generally, this is relatively harmless for the merchant, but let's at
+ least log this. */
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Failed to persist exchange response to /refund in database: %d\n",
+ qs);
+ }
+ }
+ check_resume_prd (cr->prd);
+}
+
+
+/**
+ * Function called with the result of a #TMH_EXCHANGES_find_exchange()
+ * operation.
+ *
+ * @param cls a `struct CoinRefund *`
+ * @param hr HTTP response details
+ * @param eh handle to the exchange context
+ * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available
+ * @param exchange_trusted #GNUNET_YES 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 struct TALER_Amount *wire_fee,
+ int exchange_trusted)
+{
+ struct CoinRefund *cr = cls;
+
+ cr->fo = NULL;
+ if (TALER_EC_NONE == hr->ec)
+ {
+ cr->rh = TALER_EXCHANGE_refund (eh,
+ &cr->refund_amount,
+ &cr->refund_fee,
+ &cr->prd->h_contract_terms,
+ &cr->coin_pub,
+ cr->rtransaction_id,
+ &cr->prd->merchant->privkey,
+ &refund_cb,
+ cr);
+ return;
+ }
+ cr->exchange_status = hr->http_status;
+ cr->exchange_code = hr->ec;
+ cr->exchange_reply = json_incref ((json_t*) hr->reply);
+ check_resume_prd (cr->prd);
+}
+
+
+/**
+ * Function called with information about a refund.
+ * It is responsible for packing up the data to return.
+ *
+ * @param cls closure
+ * @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 refund_fee cost of this refund operation
+ */
+static void
+process_refunds_cb (void *cls,
+ const struct TALER_CoinSpendPublicKeyP *coin_pub,
+ const char *exchange_url,
+ uint64_t rtransaction_id,
+ const char *reason,
+ const struct TALER_Amount *refund_amount,
+ const struct TALER_Amount *refund_fee)
+{
+ struct ProcessRefundData *prd = cls;
+ struct CoinRefund *cr;
+
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Found refund of %s for coin %s with reason `%s' in database\n",
+ TALER_B2S (coin_pub),
+ TALER_amount2s (refund_amount),
+ reason);
+ cr = GNUNET_new (struct CoinRefund);
+ cr->exchange_url = GNUNET_strdup (exchange_url);
+ cr->prd = prd;
+ cr->coin_pub = *coin_pub;
+ cr->rtransaction_id = rtransaction_id;
+ cr->refund_amount = *refund_amount;
+ cr->refund_fee = *refund_fee;
+ GNUNET_CONTAINER_DLL_insert (prd->cr_head,
+ prd->cr_tail,
+ cr);
+}
+
+
+/**
+ * Force resuming all suspended refund lookups, needed during shutdown.
+ */
+void
+MH_force_refund_resume (void)
+{
+ struct ProcessRefundData *prd;
+
+ while (NULL != (prd = prd_head))
+ {
+ GNUNET_CONTAINER_DLL_remove (prd_head,
+ prd_tail,
+ prd);
+ GNUNET_assert (prd->suspended);
+ prd->suspended = GNUNET_NO;
+ MHD_resume_connection (prd->connection);
+ }
+}
+
+
+/**
+ * Return refund situation about a contract.
+ *
+ * @param rh context of the handler
+ * @param connection the MHD connection to handle
+ * @param[in,out] connection_cls the connection's closure (can be updated)
+ * @param upload_data upload data
+ * @param[in,out] upload_data_size number of bytes (left) in @a upload_data
+ * @param mi merchant backend instance, never NULL
+ * @return MHD result code
+ */
+MHD_RESULT
+MH_handler_refund_lookup (struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ void **connection_cls,
+ const char *upload_data,
+ size_t *upload_data_size,
+ struct MerchantInstance *mi)
+{
+ struct ProcessRefundData *prd;
+ const char *order_id;
+ json_t *contract_terms;
+ enum GNUNET_DB_QueryStatus qs;
+
+ prd = *connection_cls;
+ if (NULL == prd)
+ {
+ order_id = MHD_lookup_connection_value (connection,
+ MHD_GET_ARGUMENT_KIND,
+ "order_id");
+ if (NULL == order_id)
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_PARAMETER_MISSING,
+ "order_id");
+ }
+
+ /* Convert order id to h_contract_terms */
+ contract_terms = NULL;
+ db->preflight (db->cls);
+ qs = db->find_contract_terms (db->cls,
+ &contract_terms,
+ order_id,
+ &mi->pubkey);
+ 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_REFUND_LOOKUP_DB_ERROR,
+ "database error looking up order_id from merchant_contract_terms table");
+ }
+
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Unknown order id given: `%s'\n",
+ order_id);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_REFUND_ORDER_ID_UNKNOWN,
+ "order_id not found in database");
+ }
+
+ prd = GNUNET_new (struct ProcessRefundData);
+ if (GNUNET_OK !=
+ TALER_JSON_hash (contract_terms,
+ &prd->h_contract_terms))
+ {
+ GNUNET_break (0);
+ json_decref (contract_terms);
+ GNUNET_free (prd);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_INTERNAL_LOGIC_ERROR,
+ "Could not hash contract terms");
+ }
+ json_decref (contract_terms);
+ prd->hc.cc = &cleanup_prd;
+ prd->merchant = mi;
+ prd->ec = TALER_EC_NONE;
+ prd->connection = connection;
+ *connection_cls = prd;
+
+ for (unsigned int i = 0; i<MAX_RETRIES; i++)
+ {
+ qs = db->get_refunds_from_contract_terms_hash (db->cls,
+ &mi->pubkey,
+ &prd->h_contract_terms,
+ &process_refunds_cb,
+ prd);
+ if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
+ break;
+ }
+ if (0 > qs)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Database hard error on refunds_from_contract_terms_hash lookup: %s\n",
+ GNUNET_h2s (&prd->h_contract_terms));
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_REFUND_LOOKUP_DB_ERROR,
+ "Failed to lookup refunds for contract");
+ }
+
+ /* Now launch exchange interactions, unless we already have the
+ response in the database! */
+ for (struct CoinRefund *cr = prd->cr_head;
+ NULL != cr;
+ cr = cr->next)
+ {
+ enum GNUNET_DB_QueryStatus qs;
+
+ qs = db->get_refund_proof (db->cls,
+ &cr->prd->merchant->pubkey,
+ &cr->prd->h_contract_terms,
+ &cr->coin_pub,
+ cr->rtransaction_id,
+ &cr->exchange_pub,
+ &cr->exchange_sig);
+ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
+ {
+ /* We need to talk to the exchange */
+ cr->fo = TMH_EXCHANGES_find_exchange (cr->exchange_url,
+ NULL,
+ GNUNET_NO,
+ &exchange_found_cb,
+ cr);
+ }
+ }
+ }
+
+ /* Check if there are still exchange operations pending */
+ if (GNUNET_YES == prd_pending (prd))
+ {
+ if (! prd->suspended)
+ {
+ prd->suspended = GNUNET_YES;
+ MHD_suspend_connection (connection);
+ GNUNET_CONTAINER_DLL_insert (prd_head,
+ prd_tail,
+ prd);
+ }
+ return MHD_YES; /* we're still talking to the exchange */
+ }
+
+ /* All operations done, build final response */
+ if (NULL == prd->cr_head)
+ {
+ /* There ARE no refunds scheduled, bitch */
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_REFUND_LOOKUP_NO_REFUND,
+ "This contract is not currently eligible for refunds");
+ }
+
+ {
+ json_t *ra;
+
+ ra = json_array ();
+ GNUNET_assert (NULL != ra);
+ for (struct CoinRefund *cr = prd->cr_head;
+ NULL != cr;
+ cr = cr->next)
+ {
+ GNUNET_assert (
+ 0 ==
+ json_array_append_new (
+ ra,
+ (MHD_HTTP_OK != cr->exchange_status)
+ ? json_pack ((NULL != cr->exchange_reply)
+ ? "{s:o,s:o,s:o,s:I,s:I,s:I,s:O}"
+ : "{s:o,s:o,s:o,s:I,s:I:s:I}",
+ "coin_pub",
+ GNUNET_JSON_from_data_auto (&cr->coin_pub),
+ "refund_amount",
+ TALER_JSON_from_amount (&cr->refund_amount),
+ "refund_fee",
+ TALER_JSON_from_amount (&cr->refund_fee),
+ "exchange_http_status",
+ (json_int_t) cr->exchange_status,
+ "rtransaction_id",
+ (json_int_t) cr->rtransaction_id,
+ "exchange_code",
+ (json_int_t) cr->exchange_code,
+ "exchange_reply",
+ cr->exchange_reply)
+ : json_pack ("{s:o,s:o,s:o,s:I,s:I,s:o,s:o}",
+ "coin_pub",
+ GNUNET_JSON_from_data_auto (&cr->coin_pub),
+ "refund_amount",
+ TALER_JSON_from_amount (&cr->refund_amount),
+ "refund_fee",
+ TALER_JSON_from_amount (&cr->refund_fee),
+ "exchange_http_status",
+ (json_int_t) cr->exchange_status,
+ "rtransaction_id",
+ (json_int_t) cr->rtransaction_id,
+ "exchange_pub",
+ GNUNET_JSON_from_data_auto (&cr->exchange_pub),
+ "exchange_sig",
+ GNUNET_JSON_from_data_auto (&cr->exchange_sig)
+ )));
+ }
+ return TALER_MHD_reply_json_pack (
+ connection,
+ MHD_HTTP_OK,
+ "{s:o, s:o, s:o}",
+ "refunds",
+ ra,
+ "merchant_pub",
+ GNUNET_JSON_from_data_auto (&mi->pubkey),
+ "h_contract_terms",
+ GNUNET_JSON_from_data_auto (&prd->h_contract_terms));
+ }
+}
+
+
+/* end of taler-merchant-httpd_refund_lookup.c */