diff options
author | Christian Grothoff <christian@grothoff.org> | 2024-01-03 11:10:18 +0100 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2024-01-03 11:27:40 +0100 |
commit | 5284c114cfc2a0ba5cebf133228d338befae6d1a (patch) | |
tree | 9a0be4dd9acc7eb9d740dbd9c7f906212e4edeb2 /src/backend | |
parent | 0c9c3ea5e5eb3b2b08aa165dfad4b03b35ca16df (diff) | |
download | merchant-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.c | 1879 |
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", - ×tamp), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (gorc->contract_terms, - spec, - NULL, NULL)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - hc->infix); - } - } - if (! order_only) - { - if (GNUNET_OK != - TALER_JSON_contract_hash (gorc->contract_terms, - &gorc->h_contract_terms)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - NULL); - } - } - if (TALER_EC_NONE != gorc->wire_ec) - { - return TALER_MHD_reply_with_error (connection, - gorc->wire_hc, - gorc->wire_ec, - NULL); - } - - GNUNET_assert (NULL != gorc->contract_terms); - - TMH_db->preflight (TMH_db->cls); - if (order_only) - { - paid = false; - wired = false; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Order %s unclaimed, no need to lookup payment status\n", - hc->infix); - } - else - { - qs = TMH_db->lookup_payment_status (TMH_db->cls, - gorc->order_serial, - gorc->session_id, - &paid, - &wired); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems, and the entry should exist as per above */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "payment status"); - } + "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 */ } |