summaryrefslogtreecommitdiff
path: root/src/backend
diff options
context:
space:
mode:
authorChristian Grothoff <christian@grothoff.org>2024-01-03 11:10:18 +0100
committerChristian Grothoff <christian@grothoff.org>2024-01-03 11:27:40 +0100
commit5284c114cfc2a0ba5cebf133228d338befae6d1a (patch)
tree9a0be4dd9acc7eb9d740dbd9c7f906212e4edeb2 /src/backend
parent0c9c3ea5e5eb3b2b08aa165dfad4b03b35ca16df (diff)
downloadmerchant-5284c114cfc2a0ba5cebf133228d338befae6d1a.tar.gz
merchant-5284c114cfc2a0ba5cebf133228d338befae6d1a.tar.bz2
merchant-5284c114cfc2a0ba5cebf133228d338befae6d1a.zip
first steps towards cleaning up GET /private/orders/ request handling logic
Diffstat (limited to 'src/backend')
-rw-r--r--src/backend/taler-merchant-httpd_private-get-orders-ID.c1879
1 files changed, 1151 insertions, 728 deletions
diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c
index 98bf2ab8..cd26d378 100644
--- a/src/backend/taler-merchant-httpd_private-get-orders-ID.c
+++ b/src/backend/taler-merchant-httpd_private-get-orders-ID.c
@@ -109,12 +109,108 @@ struct TransferQuery
/**
+ * Phases of order processing.
+ */
+enum GetOrderPhase
+{
+ /**
+ * Initialization.
+ */
+ GOP_INIT = 0,
+
+ /**
+ * Obtain contract terms from database.
+ */
+ GOP_FETCH_CONTRACT = 1,
+
+ /**
+ * Parse the contract terms.
+ */
+ GOP_PARSE_CONTRACT = 2,
+
+ /**
+ * Check if the contract was fully paid.
+ */
+ GOP_CHECK_PAID = 3,
+
+ /**
+ * Check if the wallet may have purchased an equivalent
+ * order before and we need to redirect the wallet to
+ * an existing paid order.
+ */
+ GOP_CHECK_REPURCHASE = 4,
+
+ /**
+ * Terminate processing of unpaid orders, either by
+ * suspending until payment or by returning the
+ * unpaid order status.
+ */
+ GOP_UNPAID_FINISH = 5,
+
+ /**
+ * Check if the (paid) order was refunded.
+ */
+ GOP_CHECK_REFUNDS = 6,
+
+ /**
+ * Check if the exchange transferred the funds to
+ * the merchant.
+ */
+ GOP_CHECK_EXCHANGE_TRANSFERS = 7,
+
+ /**
+ * We are suspended awaiting a response from the
+ * exchange.
+ */
+ GOP_SUSPENDED_ON_EXCHANGE = 8,
+
+ /**
+ * Check local records for transfers of funds to
+ * the merchant.
+ */
+ GOP_CHECK_LOCAL_TRANSFERS = 9,
+
+ /**
+ * Generate final comprehensive result.
+ */
+ GOP_REPLY_RESULT = 10,
+
+ /**
+ * End with the HTTP status and error code in
+ * wire_hc and wire_ec.
+ */
+ GOP_ERROR = 11,
+
+ /**
+ * We are suspended awaiting payment.
+ */
+ GOP_SUSPENDED_ON_UNPAID = 12,
+
+ /**
+ * Processing is done, return #MHD_YES.
+ */
+ GOP_END_YES = 13,
+
+ /**
+ * Processing is done, return #MHD_NO.
+ */
+ GOP_END_NO = 14
+
+};
+
+
+/**
* Data structure we keep for a check payment request.
*/
struct GetOrderRequestContext
{
/**
+ * Processing phase we are in.
+ */
+ enum GetOrderPhase phase;
+
+ /**
* Entry in the #resume_timeout_heap for this check payment, if we are
* suspended.
*/
@@ -181,6 +277,21 @@ struct GetOrderRequestContext
json_t *contract_terms;
/**
+ * Claim token of the order.
+ */
+ struct TALER_ClaimTokenP claim_token;
+
+ /**
+ * Timestamp from the @e contract_terms.
+ */
+ struct GNUNET_TIME_Timestamp timestamp;
+
+ /**
+ * Order summary. Pointer into @e contract_terms.
+ */
+ const char *summary;
+
+ /**
* Wire details for the payment, to be returned in the reply. NULL
* if not available.
*/
@@ -274,6 +385,22 @@ struct GetOrderRequestContext
bool refunded;
/**
+ * True if the order was paid.
+ */
+ bool paid;
+
+ /**
+ * True if the exchange wired the money to the merchant.
+ */
+ bool wired;
+
+ /**
+ * True if the order remains unclaimed.
+ */
+ bool order_only;
+
+
+ /**
* Set to true if this payment has been refunded and
* some refunds remain to be picked up by the wallet.
*/
@@ -334,13 +461,9 @@ TMH_force_gorc_resume (void)
* 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)
+gorc_resume (struct GetOrderRequestContext *gorc)
{
struct TransferQuery *tq;
@@ -362,8 +485,6 @@ gorc_resume (struct GetOrderRequestContext *gorc,
tq->dgh = NULL;
}
}
- gorc->wire_hc = http_status;
- gorc->wire_ec = ec;
GNUNET_assert (GNUNET_YES == gorc->suspended);
GNUNET_CONTAINER_DLL_remove (gorc_head,
gorc_tail,
@@ -375,6 +496,26 @@ gorc_resume (struct GetOrderRequestContext *gorc,
/**
+ * 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_error (struct GetOrderRequestContext *gorc,
+ unsigned int http_status,
+ enum TALER_ErrorCode ec)
+{
+ gorc->wire_hc = http_status;
+ gorc->wire_ec = ec;
+ gorc->phase = GOP_ERROR;
+ gorc_resume (gorc);
+}
+
+
+/**
* We have received a trigger from the database
* that we should (possibly) resume the request.
*
@@ -397,6 +538,7 @@ resume_by_event (void *cls,
if (GNUNET_NO == gorc->suspended)
return; /* duplicate event is possible */
gorc->suspended = GNUNET_NO;
+ gorc->phase = GOP_PARSE_CONTRACT;
GNUNET_CONTAINER_DLL_remove (gorc_head,
gorc_tail,
gorc);
@@ -451,9 +593,676 @@ exchange_timeout_cb (void *cls)
struct GetOrderRequestContext *gorc = cls;
gorc->tt = NULL;
- gorc_resume (gorc,
- MHD_HTTP_REQUEST_TIMEOUT,
- TALER_EC_GENERIC_TIMEOUT);
+ gorc_resume_error (gorc,
+ MHD_HTTP_REQUEST_TIMEOUT,
+ TALER_EC_GENERIC_TIMEOUT);
+}
+
+
+/**
+ * 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);
+ if (NULL != gorc->tt)
+ {
+ GNUNET_SCHEDULER_cancel (gorc->tt);
+ gorc->tt = NULL;
+ }
+ 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);
+}
+
+
+/**
+ * Processing the request @a gorc is finished, set the
+ * final return value in phase based on @a mret.
+ *
+ * @param[in,out] gorc order context to initialize
+ * @param mret MHD HTTP response status to return
+ */
+static void
+phase_end (struct GetOrderRequestContext *gorc,
+ MHD_RESULT mret)
+{
+ gorc->phase = (MHD_YES == mret)
+ ? GOP_END_YES
+ : GOP_END_NO;
+}
+
+
+/**
+ * Initialize event callbacks for the order processing.
+ *
+ * @param[in,out] gorc order context to initialize
+ */
+static void
+phase_init (struct GetOrderRequestContext *gorc)
+{
+ struct TMH_HandlerContext *hc = gorc->hc;
+ 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
+ };
+
+ if (! GNUNET_TIME_absolute_is_future (gorc->sc.long_poll_timeout))
+ {
+ gorc->phase++;
+ return;
+ }
+
+ GNUNET_CRYPTO_hash (hc->infix,
+ strlen (hc->infix),
+ &pay_eh.h_order_id);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Subscribing to payment triggers for %p\n",
+ gorc);
+ gorc->eh = TMH_db->event_listen (
+ TMH_db->cls,
+ &pay_eh.header,
+ GNUNET_TIME_absolute_get_remaining (gorc->sc.long_poll_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_log (GNUNET_ERROR_TYPE_INFO,
+ "Subscribing to session triggers for %p\n",
+ gorc);
+ 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,
+ GNUNET_TIME_absolute_get_remaining (gorc->sc.long_poll_timeout),
+ &resume_by_event,
+ gorc);
+ }
+ gorc->phase++;
+}
+
+
+/**
+ * Obtain latest contract terms from the database.
+ *
+ * @param[in,out] gorc order context to update
+ */
+static void
+phase_fetch_contract (struct GetOrderRequestContext *gorc)
+{
+ struct TMH_HandlerContext *hc = gorc->hc;
+ enum GNUNET_DB_QueryStatus qs;
+
+ 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;
+ gorc->summary = 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,
+ &gorc->paid,
+ &gorc->claim_token);
+ 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);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "contract terms"));
+ return;
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ gorc->order_only = true;
+ }
+ /* FIXME: what is the point of doing the lookup_order
+ below if order_only is false (qs == 1 above)?
+ Seems we could just return here, or not? */
+ {
+ struct TALER_MerchantPostDataHashP 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,
+ &gorc->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);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "order"));
+ return;
+ }
+ if (gorc->order_only &&
+ (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) )
+ {
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
+ hc->infix));
+ return;
+ }
+ if (gorc->order_only)
+ {
+ gorc->contract_terms = ct;
+ }
+ else if (NULL != ct)
+ {
+ json_decref (ct);
+ }
+ }
+ gorc->phase++;
+}
+
+
+/**
+ * Obtain parse contract terms of the order. Extracts the fulfillment URL,
+ * total amount, summary and timestamp from the contract terms!
+ *
+ * @param[in,out] gorc order context to update
+ */
+static void
+phase_parse_contract (struct GetOrderRequestContext *gorc)
+{
+ struct TMH_HandlerContext *hc = gorc->hc;
+ struct GNUNET_JSON_Specification spec[] = {
+ TALER_JSON_spec_amount_any ("amount",
+ &gorc->contract_amount),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("fulfillment_url",
+ &gorc->fulfillment_url),
+ NULL),
+ GNUNET_JSON_spec_string ("summary",
+ &gorc->summary),
+ GNUNET_JSON_spec_timestamp ("timestamp",
+ &gorc->timestamp),
+ GNUNET_JSON_spec_end ()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (gorc->contract_terms,
+ spec,
+ NULL, NULL))
+ {
+ GNUNET_break (0);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (
+ gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
+ hc->infix));
+ return;
+ }
+ if (! gorc->order_only)
+ {
+ if (GNUNET_OK !=
+ TALER_JSON_contract_hash (gorc->contract_terms,
+ &gorc->h_contract_terms))
+ {
+ GNUNET_break (0);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
+ NULL));
+ return;
+ }
+ }
+ GNUNET_assert (NULL != gorc->contract_terms);
+ gorc->phase++;
+}
+
+
+/**
+ * Check payment status of the order.
+ *
+ * @param[in,out] gorc order context to update
+ */
+static void
+phase_check_paid (struct GetOrderRequestContext *gorc)
+{
+ struct TMH_HandlerContext *hc = gorc->hc;
+ enum GNUNET_DB_QueryStatus qs;
+
+ if (gorc->order_only)
+ {
+ gorc->paid = false;
+ gorc->wired = false;
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Order %s unclaimed, no need to lookup payment status\n",
+ hc->infix);
+ gorc->phase++;
+ return;
+ }
+ /* FIXME: why do another DB lookup here, we got 'paid' before already, could
+ have likely gotten 'wired' just as well! */
+ TMH_db->preflight (TMH_db->cls);
+ qs = TMH_db->lookup_payment_status (TMH_db->cls,
+ gorc->order_serial,
+ gorc->session_id,
+ &gorc->paid,
+ &gorc->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);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "payment status"));
+ return;
+ }
+ gorc->phase++;
+}
+
+
+/**
+ * Check if re-purchase detection applies to the order.
+ *
+ * @param[in,out] gorc order context to update
+ */
+static void
+phase_check_repurchase (struct GetOrderRequestContext *gorc)
+{
+ struct TMH_HandlerContext *hc = gorc->hc;
+ char *already_paid_order_id = NULL;
+ enum GNUNET_DB_QueryStatus qs;
+ char *taler_pay_uri;
+ char *order_status_url;
+ MHD_RESULT ret;
+
+ if ( (gorc->paid) ||
+ (NULL == gorc->fulfillment_url) ||
+ (NULL == gorc->session_id) )
+ {
+ /* Repurchase cannot apply */
+ gorc->phase++;
+ return;
+ }
+ 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);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "order by fulfillment"));
+ return;
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "No already paid order for %s/%s\n",
+ gorc->session_id,
+ gorc->fulfillment_url);
+ gorc->phase++;
+ return;
+ }
+
+ /* User did pay for this order, but under a different session; ask wallet
+ to switch order ID */
+
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Found already paid order %s\n",
+ already_paid_order_id);
+ taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con,
+ hc->infix,
+ gorc->session_id,
+ hc->instance->settings.id,
+ &gorc->claim_token);
+ order_status_url = TMH_make_order_status_url (gorc->sc.con,
+ hc->infix,
+ gorc->session_id,
+ hc->instance->settings.id,
+ &gorc->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);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
+ "host"));
+ return;
+ }
+ ret = TALER_MHD_REPLY_JSON_PACK (
+ gorc->sc.con,
+ 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",
+ gorc->summary),
+ GNUNET_JSON_pack_timestamp ("creation_time",
+ gorc->timestamp));
+ GNUNET_free (taler_pay_uri);
+ GNUNET_free (already_paid_order_id);
+ phase_end (gorc,
+ ret);
+}
+
+
+/**
+ * Check if we should suspend until the order is paid.
+ *
+ * @param[in,out] gorc order context to update
+ */
+static void
+phase_unpaid_finish (struct GetOrderRequestContext *gorc)
+{
+ struct TMH_HandlerContext *hc = gorc->hc;
+ char *taler_pay_uri;
+ char *order_status_url;
+ MHD_RESULT ret;
+
+ if (gorc->paid)
+ {
+ gorc->phase++;
+ return;
+ }
+ /* User never paid for this order, suspend waiting
+ on payment or return details. */
+
+ if (! gorc->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->phase = GOP_SUSPENDED_ON_UNPAID;
+ gorc->suspended = GNUNET_YES;
+ MHD_suspend_connection (gorc->sc.con);
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Order %s claimed but not paid yet\n",
+ hc->infix);
+ phase_end (gorc,
+ TALER_MHD_REPLY_JSON_PACK (
+ gorc->sc.con,
+ MHD_HTTP_OK,
+ GNUNET_JSON_pack_object_incref ("contract_terms",
+ gorc->contract_terms),
+ GNUNET_JSON_pack_string ("order_status",
+ "claimed")));
+ return;
+ }
+
+ /* FIXME: too similar to logic above! */
+ 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_assert (GNUNET_NO == gorc->suspended);
+ GNUNET_CONTAINER_DLL_insert (gorc_head,
+ gorc_tail,
+ gorc);
+ gorc->suspended = GNUNET_YES;
+ gorc->phase = GOP_SUSPENDED_ON_UNPAID;
+ MHD_suspend_connection (gorc->sc.con);
+ return;
+ }
+ taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con,
+ hc->infix,
+ gorc->session_id,
+ hc->instance->settings.id,
+ &gorc->claim_token);
+ order_status_url = TMH_make_order_status_url (gorc->sc.con,
+ hc->infix,
+ gorc->session_id,
+ hc->instance->settings.id,
+ &gorc->claim_token,
+ NULL);
+ ret = TALER_MHD_REPLY_JSON_PACK (
+ gorc->sc.con,
+ 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",
+ gorc->summary),
+ GNUNET_JSON_pack_timestamp ("creation_time",
+ gorc->timestamp));
+ GNUNET_free (taler_pay_uri);
+ GNUNET_free (order_status_url);
+ phase_end (gorc,
+ ret);
+
+}
+
+
+/**
+ * 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_Timestamp 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_log (GNUNET_ERROR_TYPE_INFO,
+ "Found refund %llu over %s for reason %s\n",
+ (unsigned long long) rtransaction_id,
+ TALER_amount2s (refund_amount),
+ reason);
+ GNUNET_assert (0 ==
+ json_array_append_new (
+ gorc->refund_details,
+ GNUNET_JSON_PACK (
+ TALER_JSON_pack_amount ("amount",
+ refund_amount),
+ GNUNET_JSON_pack_bool ("pending",
+ pending),
+ GNUNET_JSON_pack_timestamp ("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))
+ {
+ if (GNUNET_OK !=
+ TALER_amount_cmp_currency (
+ &gorc->deposit_fees_total,
+ &tq->deposit_fee))
+ {
+ gorc->refund_currency_mismatch = true;
+ return;
+ }
+
+ GNUNET_assert (0 <=
+ TALER_amount_subtract (&gorc->deposit_fees_total,
+ &gorc->deposit_fees_total,
+ &tq->deposit_fee));
+ }
+ }
+ if (GNUNET_OK !=
+ TALER_amount_cmp_currency (
+ &gorc->refund_amount,
+ refund_amount))
+ {
+ gorc->refund_currency_mismatch = true;
+ return;
+ }
+ GNUNET_assert (0 <=
+ TALER_amount_add (&gorc->refund_amount,
+ &gorc->refund_amount,
+ refund_amount));
+ gorc->refunded = true;
+ gorc->refund_pending |= pending;
+}
+
+
+/**
+ * Check refund status for the order.
+ *
+ * @param[in,out] gorc order context to update
+ */
+static void
+phase_check_refunds (struct GetOrderRequestContext *gorc)
+{
+ struct TMH_HandlerContext *hc = gorc->hc;
+ enum GNUNET_DB_QueryStatus qs;
+
+ GNUNET_assert (! gorc->order_only);
+ GNUNET_assert (gorc->paid);
+ /* Accumulate refunds, if any. */
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (gorc->contract_amount.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);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "detailed refunds"));
+ return;
+ }
+ if (gorc->refund_currency_mismatch)
+ {
+ GNUNET_break (0);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "refunds in different currency than original order price"));
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Total refunds are %s\n",
+ TALER_amount2s (&gorc->refund_amount));
+ gorc->phase++;
}
@@ -479,6 +1288,10 @@ deposit_get_cb (void *cls,
{
enum GNUNET_DB_QueryStatus qs;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Exchange returned wire transfer over %s for deposited coin %s\n",
+ TALER_amount2s (&dr->details.ok.coin_contribution),
+ TALER_B2S (&tq->coin_pub));
qs = TMH_db->insert_deposit_to_transfer (TMH_db->cls,
tq->deposit_serial,
&dr->details.ok);
@@ -491,9 +1304,10 @@ deposit_get_cb (void *cls,
GNUNET_free (tq->exchange_url);
GNUNET_free (tq);
if (NULL == gorc->tq_head)
- gorc_resume (gorc,
- 0,
- TALER_EC_NONE);
+ {
+ gorc->phase++;
+ gorc_resume (gorc);
+ }
return;
}
/* Compute total amount *wired* */
@@ -515,9 +1329,10 @@ deposit_get_cb (void *cls,
GNUNET_free (tq->exchange_url);
GNUNET_free (tq);
if (NULL == gorc->tq_head)
- gorc_resume (gorc,
- 0,
- TALER_EC_NONE);
+ {
+ gorc->phase++;
+ gorc_resume (gorc);
+ }
return;
}
if (0 >
@@ -532,9 +1347,10 @@ deposit_get_cb (void *cls,
GNUNET_free (tq->exchange_url);
GNUNET_free (tq);
if (NULL == gorc->tq_head)
- gorc_resume (gorc,
- 0,
- TALER_EC_NONE);
+ {
+ gorc->phase++;
+ gorc_resume (gorc);
+ }
return;
}
if (0 >
@@ -549,9 +1365,10 @@ deposit_get_cb (void *cls,
GNUNET_free (tq->exchange_url);
GNUNET_free (tq);
if (NULL == gorc->tq_head)
- gorc_resume (gorc,
- 0,
- TALER_EC_NONE);
+ {
+ gorc->phase++;
+ gorc_resume (gorc);
+ }
return;
}
break;
@@ -563,6 +1380,11 @@ deposit_get_cb (void *cls,
enum GNUNET_DB_QueryStatus qs;
struct GNUNET_TIME_Timestamp now;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Exchange returned KYC requirement (%d/%d) for deposited coin %s\n",
+ dr->details.accepted.kyc_ok,
+ dr->details.accepted.aml_decision,
+ TALER_B2S (&tq->coin_pub));
now = GNUNET_TIME_timestamp_get ();
qs = TMH_db->account_kyc_set_status (
TMH_db->cls,
@@ -584,9 +1406,10 @@ deposit_get_cb (void *cls,
GNUNET_free (tq->exchange_url);
GNUNET_free (tq);
if (NULL == gorc->tq_head)
- gorc_resume (gorc,
- 0,
- TALER_EC_NONE);
+ {
+ gorc->phase++;
+ gorc_resume (gorc);
+ }
return;
}
gorc_report (gorc,
@@ -597,6 +1420,9 @@ deposit_get_cb (void *cls,
}
default:
{
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Exchange returned tracking failure for deposited coin %s\n",
+ TALER_B2S (&tq->coin_pub));
gorc_report (gorc,
TALER_EC_MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE,
&tq->coin_pub,
@@ -604,9 +1430,10 @@ deposit_get_cb (void *cls,
GNUNET_free (tq->exchange_url);
GNUNET_free (tq);
if (NULL == gorc->tq_head)
- gorc_resume (gorc,
- 0,
- TALER_EC_NONE);
+ {
+ gorc->phase++;
+ gorc_resume (gorc);
+ }
return;
}
} /* end switch */
@@ -615,9 +1442,8 @@ deposit_get_cb (void *cls,
if (NULL != gorc->tq_head)
return;
/* *all* are done, resume! */
- gorc_resume (gorc,
- 0,
- TALER_EC_NONE);
+ gorc->phase++;
+ gorc_resume (gorc);
}
@@ -647,9 +1473,9 @@ exchange_found_cb (void *cls,
tq);
GNUNET_free (tq->exchange_url);
GNUNET_free (tq);
- gorc_resume (gorc,
- MHD_HTTP_GATEWAY_TIMEOUT,
- TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT);
+ gorc_resume_error (gorc,
+ MHD_HTTP_GATEWAY_TIMEOUT,
+ TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT);
return;
}
tq->dgh = TALER_EXCHANGE_deposits_get (
@@ -670,9 +1496,9 @@ exchange_found_cb (void *cls,
tq);
GNUNET_free (tq->exchange_url);
GNUNET_free (tq);
- gorc_resume (gorc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE);
+ gorc_resume_error (gorc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE);
}
}
@@ -705,6 +1531,10 @@ deposit_cb (void *cls,
struct GetOrderRequestContext *gorc = cls;
struct TransferQuery *tq;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Checking deposit status for coin %s (over %s)\n",
+ TALER_B2S (coin_pub),
+ TALER_amount2s (amount_with_fee));
tq = GNUNET_new (struct TransferQuery);
tq->gorc = gorc;
tq->exchange_url = GNUNET_strdup (exchange_url);
@@ -722,124 +1552,55 @@ deposit_cb (void *cls,
tq);
if (NULL == tq->fo)
{
- gorc_resume (gorc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE);
+ gorc_resume_error (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.
+ * Check wire transfer status for the order at the exchange.
*
- * @param cls closure, must be a `struct GetOrderRequestContext *`
+ * @param[in,out] gorc order context to update
*/
static void
-gorc_cleanup (void *cls)
+phase_check_exchange_transfers (struct GetOrderRequestContext *gorc)
{
- 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)
+ if (gorc->wired ||
+ (! gorc->transfer_status_requested) )
{
- 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_Timestamp 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_bool ("pending",
- pending),
- GNUNET_JSON_pack_timestamp ("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))
- {
- if (GNUNET_OK !=
- TALER_amount_cmp_currency (
- &gorc->deposit_fees_total,
- &tq->deposit_fee))
- {
- gorc->refund_currency_mismatch = true;
- return;
- }
-
- GNUNET_assert (0 <=
- TALER_amount_subtract (&gorc->deposit_fees_total,
- &gorc->deposit_fees_total,
- &tq->deposit_fee));
- }
+ gorc->phase = GOP_CHECK_LOCAL_TRANSFERS;
+ return;
}
- if (GNUNET_OK !=
- TALER_amount_cmp_currency (
- &gorc->refund_amount,
- refund_amount))
+ /* 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 (gorc->contract_amount.currency,
+ &gorc->deposits_total));
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (gorc->contract_amount.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)
{
- gorc->refund_currency_mismatch = true;
+ /* No deposits found for paid order. This is strange... */
+ GNUNET_break (0);
+ gorc->phase = GOP_CHECK_LOCAL_TRANSFERS;
return;
}
- GNUNET_assert (0 <=
- TALER_amount_add (&gorc->refund_amount,
- &gorc->refund_amount,
- refund_amount));
- gorc->refunded = true;
- gorc->refund_pending |= pending;
+ gorc->phase++;
+ GNUNET_CONTAINER_DLL_insert (gorc_head,
+ gorc_tail,
+ gorc);
+ gorc->suspended = GNUNET_YES;
+ MHD_suspend_connection (gorc->sc.con);
+ gorc->tt = GNUNET_SCHEDULER_add_delayed (EXCHANGE_TIMEOUT,
+ &exchange_timeout_cb,
+ gorc);
}
@@ -881,7 +1642,7 @@ process_transfer_details (
gorc->deposit_currency_mismatch = true;
return;
}
-
+
/* Compute total amount *wired* */
GNUNET_assert (0 <
TALER_amount_add (&gorc->deposits_total,
@@ -913,19 +1674,202 @@ process_transfer_details (
}
+/**
+ * Check transfer status in local database.
+ *
+ * @param[in,out] gorc order context to update
+ */
+static void
+phase_check_local_transfers (struct GetOrderRequestContext *gorc)
+{
+ struct TMH_HandlerContext *hc = gorc->hc;
+ enum GNUNET_DB_QueryStatus qs;
+
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (gorc->contract_amount.currency,
+ &gorc->deposits_total));
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (gorc->contract_amount.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);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "transfer details"));
+ return;
+ }
+ if (gorc->deposit_currency_mismatch)
+ {
+ GNUNET_break (0);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "deposits in different currency than original order price"));
+ return;
+ }
+
+ if (! gorc->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);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (
+ gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
+ "refund exceeds contract value"));
+ return;
+ }
+ if (0 >
+ TALER_amount_subtract (&expect_total,
+ &expect_total,
+ &gorc->deposit_fees_total))
+ {
+ GNUNET_break (0);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (
+ gorc->sc.con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
+ "deposit fees exceed total minus refunds"));
+ return;
+ }
+ if (0 >=
+ TALER_amount_cmp (&expect_total,
+ &gorc->deposits_total))
+ {
+ /* expect_total <= gorc->deposits_total: good: we got the wire transfer */
+ gorc->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,
+ gorc->timestamp,
+ gorc->order_serial);
+ }
+ }
+ gorc->phase++;
+}
+
+
+/**
+ * Generate final result for the status request.
+ *
+ * @param[in,out] gorc order context to update
+ */
+static void
+phase_reply_result (struct GetOrderRequestContext *gorc)
+{
+ struct TMH_HandlerContext *hc = gorc->hc;
+ MHD_RESULT ret;
+ char *order_status_url;
+
+ {
+ struct TALER_PrivateContractHashP *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 (gorc->sc.con,
+ hc->infix,
+ gorc->session_id,
+ hc->instance->settings.id,
+ &gorc->claim_token,
+ h_contract);
+ }
+
+ ret = TALER_MHD_REPLY_JSON_PACK (
+ gorc->sc.con,
+ 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",
+ gorc->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;
+ phase_end (gorc,
+ ret);
+}
+
+
+/**
+ * End with error status in wire_hc and wire_ec.
+ *
+ * @param[in,out] gorc order context to update
+ */
+static void
+phase_error (struct GetOrderRequestContext *gorc)
+{
+ GNUNET_assert (TALER_EC_NONE != gorc->wire_ec);
+ phase_end (gorc,
+ TALER_MHD_reply_with_error (gorc->sc.con,
+ gorc->wire_hc,
+ gorc->wire_ec,
+ NULL));
+}
+
+
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_Timestamp timestamp;
if (NULL == gorc)
{
@@ -960,590 +1904,69 @@ TMH_private_get_orders_ID (const struct TMH_RequestHandler *rh,
TALER_MHD_parse_request_timeout (connection,
&gorc->sc.long_poll_timeout);
- if (GNUNET_TIME_absolute_is_future (gorc->sc.long_poll_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);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Subscribing to payment triggers for %p\n",
- gorc);
- gorc->eh = TMH_db->event_listen (
- TMH_db->cls,
- &pay_eh.header,
- GNUNET_TIME_absolute_get_remaining (gorc->sc.long_poll_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_log (GNUNET_ERROR_TYPE_INFO,
- "Subscribing to session triggers for %p\n",
- gorc);
- 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,
- GNUNET_TIME_absolute_get_remaining (gorc->sc.long_poll_timeout),
- &resume_by_event,
- gorc);
- }
- }
- } /* end first-time per-request initialization */
-
- if (GNUNET_SYSERR == gorc->suspended)
- return MHD_NO; /* we are in shutdown */
-
- 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);
- {
- bool paid = false;
-
- qs = TMH_db->lookup_contract_terms (TMH_db->cls,
- hc->instance->settings.id,
- hc->infix,
- &gorc->contract_terms,
- &gorc->order_serial,
- &paid,
- 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 TALER_MerchantPostDataHashP 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_any ("amount",
- &gorc->contract_amount),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_string ("fulfillment_url",
- &gorc->fulfillment_url),
- NULL),
- GNUNET_JSON_spec_string ("summary",
- &summary),
- GNUNET_JSON_spec_timestamp ("timestamp",
- &timestamp),
- 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");
- }
+ "Starting GET /private/orders/%s processing with timeout %s\n",
+ hc->infix,
+ GNUNET_STRINGS_absolute_time_to_string (
+ gorc->sc.long_poll_timeout));
}
- 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_timestamp ("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_SYSERR == gorc->suspended)
+ return MHD_NO; /* we are in shutdown */
+ while (1)
{
- if (GNUNET_TIME_absolute_is_future (gorc->sc.long_poll_timeout))
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Processing order %s in phase %d\n",
+ hc->infix,
+ (int) gorc->phase);
+ switch (gorc->phase)
{
- 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 = GNUNET_YES;
- MHD_suspend_connection (gorc->sc.con);
+ case GOP_INIT:
+ phase_init (gorc);
+ break;
+ case GOP_FETCH_CONTRACT:
+ phase_fetch_contract (gorc);
+ break;
+ case GOP_PARSE_CONTRACT:
+ phase_parse_contract (gorc);
+ break;
+ case GOP_CHECK_PAID:
+ phase_check_paid (gorc);
+ break;
+ case GOP_CHECK_REPURCHASE:
+ phase_check_repurchase (gorc);
+ break;
+ case GOP_UNPAID_FINISH:
+ phase_unpaid_finish (gorc);
+ break;
+ case GOP_CHECK_REFUNDS:
+ phase_check_refunds (gorc);
+ break;
+ case GOP_CHECK_EXCHANGE_TRANSFERS:
+ phase_check_exchange_transfers (gorc);
+ break;
+ case GOP_CHECK_LOCAL_TRANSFERS:
+ phase_check_local_transfers (gorc);
+ break;
+ case GOP_REPLY_RESULT:
+ phase_reply_result (gorc);
+ break;
+ case GOP_ERROR:
+ phase_error (gorc);
+ break;
+ case GOP_SUSPENDED_ON_UNPAID:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Suspending order request awaiting payment\n");
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 (gorc->contract_amount.currency,
- &gorc->deposits_total));
- GNUNET_assert (GNUNET_OK ==
- TALER_amount_set_zero (gorc->contract_amount.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 = GNUNET_YES;
- MHD_suspend_connection (connection);
- gorc->tt = GNUNET_SCHEDULER_add_delayed (EXCHANGE_TIMEOUT,
- &exchange_timeout_cb,
- gorc);
+ case GOP_SUSPENDED_ON_EXCHANGE:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Suspending order request awaiting answer from exchange\n");
return MHD_YES;
+ case GOP_END_YES:
+ return MHD_YES;
+ case GOP_END_NO:
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Closing connection, no response generated\n");
+ return MHD_NO;
}
- }
-
- 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 (GNUNET_NO == gorc->suspended);
- GNUNET_CONTAINER_DLL_insert (gorc_head,
- gorc_tail,
- gorc);
- gorc->suspended = GNUNET_YES;
- 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_timestamp ("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 (gorc->contract_amount.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");
- }
- if (gorc->refund_currency_mismatch)
- {
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "refunds in different currency than original order price");
- }
-
- /* 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 (gorc->contract_amount.currency,
- &gorc->deposits_total));
- GNUNET_assert (GNUNET_OK ==
- TALER_amount_set_zero (gorc->contract_amount.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 (gorc->deposit_currency_mismatch)
- {
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "deposits in different currency than original order price");
- }
-
- 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 TALER_PrivateContractHashP *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;
- }
+ } /* end first-time per-request initialization */
}