summaryrefslogtreecommitdiff
path: root/src/backend/taler-merchant-httpd_post-orders-ID-refund.c
diff options
context:
space:
mode:
authorJonathan Buchanan <jonathan.russ.buchanan@gmail.com>2020-08-16 02:42:03 -0400
committerJonathan Buchanan <jonathan.russ.buchanan@gmail.com>2020-08-16 02:42:49 -0400
commit84d79e5c8eda85d4dc2af6528de19a35350ad60e (patch)
tree38d109bda1d7f1b9fed8ca44a7834aa6fc205bcf /src/backend/taler-merchant-httpd_post-orders-ID-refund.c
parent5e9a041c084f70c7bb80d13b960402d30cd5e6fe (diff)
downloadmerchant-84d79e5c8eda85d4dc2af6528de19a35350ad60e.tar.gz
merchant-84d79e5c8eda85d4dc2af6528de19a35350ad60e.tar.bz2
merchant-84d79e5c8eda85d4dc2af6528de19a35350ad60e.zip
early stages of implementing POST /orders/$ORDER_ID/refund
Diffstat (limited to 'src/backend/taler-merchant-httpd_post-orders-ID-refund.c')
-rw-r--r--src/backend/taler-merchant-httpd_post-orders-ID-refund.c704
1 files changed, 704 insertions, 0 deletions
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-refund.c b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c
new file mode 100644
index 00000000..bfdb6ca2
--- /dev/null
+++ b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c
@@ -0,0 +1,704 @@
+/*
+ This file is part of TALER
+ (C) 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_post-orders-ID-refund.c
+ * @brief handling of POST /orders/$ID/refund requests
+ * @author Jonathan Buchanan
+ */
+#include "platform.h"
+#include <taler/taler_signatures.h>
+#include <taler/taler_json_lib.h>
+#include <taler/taler_exchange_service.h>
+#include "taler-merchant-httpd_auditors.h"
+#include "taler-merchant-httpd_exchanges.h"
+#include "taler-merchant-httpd_post-orders-ID-refund.h"
+
+
+/**
+ * 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;
+
+ /**
+ * Request this operation is part of.
+ */
+ struct PostRefundData *prd;
+
+ /**
+ * URL of the exchange for this @e coin_pub.
+ */
+ char *exchange_url;
+
+ /**
+ * 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;
+
+ /**
+ * When did the merchant grant the refund. To be used to group events
+ * in the wallet.
+ */
+ struct GNUNET_TIME_Absolute execution_time;
+
+ /**
+ * Coin to refund.
+ */
+ struct TALER_CoinSpendPublicKeyP coin_pub;
+
+ /**
+ * Refund transaction ID to use.
+ */
+ uint64_t rtransaction_id;
+
+ /**
+ * Unique serial number identifying the refund.
+ */
+ uint64_t refund_serial;
+
+ /**
+ * Amount to refund.
+ */
+ struct TALER_Amount refund_amount;
+
+ /**
+ * 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;
+
+};
+
+
+/**
+ * Context for the operation.
+ */
+struct PostRefundData
+{
+
+ /**
+ * Hashed version of contract terms. All zeros if
+ * not provided.
+ */
+ struct GNUNET_HashCode h_contract_terms;
+
+ /**
+ * DLL of (suspended) requests.
+ */
+ struct PostRefundData *next;
+
+ /**
+ * DLL of (suspended) requests.
+ */
+ struct PostRefundData *prev;
+
+ /**
+ * Refunds for this order. Head of DLL.
+ */
+ struct CoinRefund *cr_head;
+
+ /**
+ * Refunds for this order. Tail of DLL.
+ */
+ struct CoinRefund *cr_tail;
+
+ /**
+ * Context of the request.
+ */
+ struct TMH_HandlerContext *hc;
+
+ /**
+ * 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 MerchantInstance *mi;
+
+ /**
+ * order ID for the payment
+ */
+ const char *order_id;
+
+ /**
+ * Where to get the contract
+ */
+ const char *contract_url;
+
+ /**
+ * fulfillment URL of the contract (valid as long as
+ * @e contract_terms is valid).
+ */
+ const char *fulfillment_url;
+
+ /**
+ * session of the client
+ */
+ const char *session_id;
+
+ /**
+ * Contract terms of the payment we are checking. NULL when they
+ * are not (yet) known.
+ */
+ json_t *contract_terms;
+
+ /**
+ * Total refunds granted for this payment. Only initialized
+ * if @e refunded is set to true.
+ */
+ struct TALER_Amount refund_amount;
+
+ /**
+ * Did we suspend @a connection?
+ */
+ bool suspended;
+
+ /**
+ * Return code: #TALER_EC_NONE if successful.
+ */
+ enum TALER_ErrorCode ec;
+
+ /**
+ * Set to true if we are dealing with an unclaimed order
+ * (and thus @e h_contract_terms is not set, and certain
+ * DB queries will not work).
+ */
+ bool unclaimed;
+
+ /**
+ * Set to true if this payment has been refunded and
+ * @e refund_amount is initialized.
+ */
+ bool refunded;
+
+ /**
+ * Set to true if a refund is still available for the
+ * wallet for this payment.
+ */
+ bool refund_available;
+
+ /**
+ * Set to true if the client requested HTML, otherwise
+ * we generate JSON.
+ */
+ bool generate_html;
+
+};
+
+
+/**
+ * Head of DLL of (suspended) requests.
+ */
+static struct PostRefundData *prd_head;
+
+/**
+ * Tail of DLL of (suspended) requests.
+ */
+static struct PostRefundData *prd_tail;
+
+
+/* FIXME: Handle shutdown and other events that require ending all requests */
+
+
+/**
+ * Check if @a prd has exchange requests still pending.
+ *
+ * @param prd state to check
+ * @return true if activities are still pending
+ */
+static bool
+exchange_operations_pending (struct PostRefundData *prd)
+{
+ for (struct CoinRefund *cr = prd->cr_head;
+ NULL != cr;
+ cr = cr->next)
+ {
+ if ( (NULL != cr->fo) ||
+ (NULL != cr->rh) )
+ return true;
+ }
+ return false;
+}
+
+
+/**
+ * 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 PostRefundData *prd)
+{
+ if (exchange_operations_pending (prd))
+ return;
+ GNUNET_CONTAINER_DLL_remove (prd_head,
+ prd_tail,
+ prd);
+ GNUNET_assert (prd->suspended);
+ prd->suspended = false;
+ MHD_resume_connection (prd->sc.con);
+ 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 = TMH_db->insert_refund_proof (TMH_db->cls,
+ cr->refund_serial,
+ exchange_sig,
+ exchange_pub);
+ 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 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 CoinRefund *cr = cls;
+
+ (void) payto_uri;
+ cr->fo = NULL;
+ if (TALER_EC_NONE == hr->ec)
+ {
+ cr->rh = TALER_EXCHANGE_refund (eh,
+ &cr->refund_amount,
+ &cr->prd->h_contract_terms,
+ &cr->coin_pub,
+ cr->rtransaction_id,
+ &cr->prd->hc->instance->merchant_priv,
+ &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 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 PostRefundData *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_amount2s (refund_amount),
+ TALER_B2S (coin_pub),
+ reason);
+ cr = GNUNET_new (struct CoinRefund);
+ cr->refund_serial = refund_serial;
+ 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->execution_time = timestamp;
+ GNUNET_CONTAINER_DLL_insert (prd->cr_head,
+ prd->cr_tail,
+ cr);
+ if (prd->refunded)
+ {
+ GNUNET_assert (0 <=
+ TALER_amount_add (&prd->refund_amount,
+ &prd->refund_amount,
+ refund_amount));
+ return;
+ }
+ prd->refund_amount = *refund_amount;
+ prd->refunded = true;
+ prd->refund_available |= pending;
+}
+
+
+/**
+ * Obtain refunds for an order.
+ *
+ * @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_post_orders_ID_refund (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ struct PostRefundData *prd = hc->ctx;
+ enum GNUNET_DB_QueryStatus qs;
+
+ if (NULL == prd)
+ {
+ prd = GNUNET_new (struct PostRefundData);
+ prd->sc.con = connection;
+ prd->hc = hc;
+ prd->order_id = hc->infix;
+ {
+ enum GNUNET_GenericReturnValue res;
+
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_fixed_auto ("h_contract", &prd->h_contract_terms),
+ GNUNET_JSON_spec_end ()
+ };
+ res = TALER_MHD_parse_json_data (connection,
+ hc->request_body,
+ spec);
+ if (GNUNET_OK != res)
+ return (GNUNET_NO == res)
+ ? MHD_YES
+ : MHD_NO;
+ }
+
+ TMH_db->preflight (TMH_db->cls);
+ {
+ json_t *contract_terms;
+ uint64_t order_serial;
+ qs = TMH_db->lookup_contract_terms (TMH_db->cls,
+ hc->instance->settings.id,
+ hc->infix,
+ &contract_terms,
+ &order_serial);
+ 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,
+ "db error fetching contract terms");
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ json_decref (contract_terms);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_GET_ORDERS_ORDER_NOT_FOUND,
+ "Did not find contract terms for order in DB");
+ }
+ {
+ struct GNUNET_HashCode h_contract_terms;
+ if (GNUNET_OK !=
+ TALER_JSON_contract_hash (contract_terms,
+ &h_contract_terms))
+ {
+ GNUNET_break (0);
+ json_decref (contract_terms);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GET_ORDERS_FAILED_COMPUTE_PROPOSAL_HASH,
+ "Failed to hash contract terms");
+ }
+ json_decref (contract_terms);
+ if (0 != GNUNET_memcmp (&h_contract_terms,
+ &prd->h_contract_terms))
+ {
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_GET_ORDERS_FAILED_COMPUTE_PROPOSAL_HASH,
+ "");
+ }
+ }
+ }
+ }
+ {
+ GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TMH_currency,
+ &prd->refund_amount));
+ qs = TMH_db->lookup_refunds_detailed (TMH_db->cls,
+ hc->instance->settings.id,
+ &prd->h_contract_terms,
+ &process_refunds_cb,
+ prd);
+ if (0 > qs)
+ {
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GET_ORDERS_DB_LOOKUP_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 = TMH_db->lookup_refund_proof (TMH_db->cls,
+ cr->refund_serial,
+ &cr->exchange_sig,
+ &cr->exchange_pub);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GET_ORDERS_DB_LOOKUP_ERROR,
+ "Merchant database error");
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ /* We need to talk to the exchange */
+ /* FIXME: notify clients polling for this to happen */
+ cr->fo = TMH_EXCHANGES_find_exchange (cr->exchange_url,
+ NULL,
+ GNUNET_NO,
+ &exchange_found_cb,
+ cr);
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* We got a reply earlier, set status accordingly */
+ cr->exchange_status = MHD_HTTP_OK;
+ break;
+ }
+ }
+
+ /* Check if there are still exchange operations pending */
+ if (exchange_operations_pending (prd))
+ {
+ if (! prd->suspended)
+ {
+ prd->suspended = true;
+ MHD_suspend_connection (connection);
+ GNUNET_CONTAINER_DLL_insert (prd_head,
+ prd_tail,
+ prd);
+ }
+ return MHD_YES; /* we're still talking to the exchange */
+ }
+
+ {
+ json_t *ra;
+
+ ra = json_array ();
+ GNUNET_assert (NULL != ra);
+ for (struct CoinRefund *cr = prd->cr_head;
+ NULL != cr;
+ cr = cr->next)
+ {
+ json_t *refund;
+
+ if (MHD_HTTP_OK != cr->exchange_status)
+ {
+ if (NULL == cr->exchange_reply)
+ {
+ refund = json_pack ("{s:s, s:I,s:I,s:o,s:o,s:o}"
+ "type",
+ "failure",
+ "exchange_status",
+ (json_int_t) cr->exchange_status,
+ "rtransaction_id",
+ (json_int_t) cr->rtransaction_id,
+ "coin_pub",
+ GNUNET_JSON_from_data_auto (&cr->coin_pub),
+ "refund_amount",
+ TALER_JSON_from_amount (&cr->refund_amount),
+ "execution_time",
+ GNUNET_JSON_from_time_abs (cr->execution_time));
+ }
+ else
+ {
+ refund = json_pack ("{s:s,s:I,s:I,s:O,s:I,s:o,s:o,s:o}"
+ "type",
+ "failure",
+ "exchange_status",
+ (json_int_t) cr->exchange_status,
+ "exchange_code",
+ (json_int_t) cr->exchange_code,
+ "exchange_reply",
+ cr->exchange_reply,
+ "rtransaction_id",
+ (json_int_t) cr->rtransaction_id,
+ "coin_pub",
+ GNUNET_JSON_from_data_auto (&cr->coin_pub),
+ "refund_amount",
+ TALER_JSON_from_amount (&cr->refund_amount),
+ "execution_time",
+ GNUNET_JSON_from_time_abs (cr->execution_time));
+ }
+ }
+ else
+ {
+ refund = json_pack ("{s:s,s:I,s:o,s:o,s:I,s:o,s:o,s:o}",
+ "type",
+ "success",
+ "exchange_status",
+ (json_int_t) cr->exchange_status,
+ "exchange_sig",
+ GNUNET_JSON_from_data_auto (&cr->exchange_sig),
+ "exchange_pub",
+ GNUNET_JSON_from_data_auto (&cr->exchange_pub),
+ "rtransaction_id",
+ (json_int_t) cr->rtransaction_id,
+ "coin_pub",
+ GNUNET_JSON_from_data_auto (&cr->coin_pub),
+ "refund_amount",
+ TALER_JSON_from_amount (&cr->refund_amount),
+ "execution_time",
+ GNUNET_JSON_from_time_abs (cr->execution_time));
+ }
+ GNUNET_assert (
+ 0 ==
+ json_array_append_new (ra,
+ refund));
+ }
+
+ return TALER_MHD_reply_json_pack (
+ connection,
+ MHD_HTTP_OK,
+ "{s:o, s:o, s:o}",
+ "refund_amount",
+ TALER_JSON_from_amount (&prd->refund_amount),
+ "refunds",
+ ra,
+ "merchant_pub",
+ GNUNET_JSON_from_data_auto (&hc->instance->merchant_pub));
+ }
+
+ return MHD_YES;
+}
+
+
+/* end of taler-merchant-httpd_post-orders-ID-refund.c */