From 5284c114cfc2a0ba5cebf133228d338befae6d1a Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Wed, 3 Jan 2024 11:10:18 +0100 Subject: first steps towards cleaning up GET /private/orders/ request handling logic --- .../taler-merchant-httpd_private-get-orders-ID.c | 2273 ++++++++++++-------- 1 file changed, 1348 insertions(+), 925 deletions(-) (limited to 'src/backend') 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 @@ -108,12 +108,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. @@ -180,6 +276,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. @@ -273,6 +384,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, @@ -374,6 +495,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,404 +593,1023 @@ 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); } /** - * Function called with detailed wire transfer data. + * Clean up the session state for a GET /private/order/ID request. * - * @param cls closure with a `struct TransferQuery *` - * @param dr HTTP response data + * @param cls closure, must be a `struct GetOrderRequestContext *` */ static void -deposit_get_cb (void *cls, - const struct TALER_EXCHANGE_GetDepositResponse *dr) +gorc_cleanup (void *cls) { - struct TransferQuery *tq = cls; - struct GetOrderRequestContext *gorc = tq->gorc; + struct GetOrderRequestContext *gorc = cls; - GNUNET_CONTAINER_DLL_remove (gorc->tq_head, - gorc->tq_tail, - tq); - switch (dr->hr.http_status) + 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) { - case MHD_HTTP_OK: - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->insert_deposit_to_transfer (TMH_db->cls, - tq->deposit_serial, - &dr->details.ok); - if (qs < 0) - { - gorc_report (gorc, - TALER_EC_GENERIC_DB_STORE_FAILED, - &tq->coin_pub, - NULL); - GNUNET_free (tq->exchange_url); - GNUNET_free (tq); - if (NULL == gorc->tq_head) - gorc_resume (gorc, - 0, - TALER_EC_NONE); - return; - } - /* Compute total amount *wired* */ - if ( (GNUNET_OK != - TALER_amount_cmp_currency ( - &gorc->deposits_total, - &dr->details.ok.coin_contribution)) || - (GNUNET_OK != - TALER_amount_cmp_currency ( - &gorc->deposit_fees_total, - &tq->deposit_fee)) ) - { - /* something very wrong in our database ... */ - GNUNET_break (0); - gorc_report (gorc, - TALER_EC_GENERIC_DB_FETCH_FAILED, - &tq->coin_pub, - NULL); - GNUNET_free (tq->exchange_url); - GNUNET_free (tq); - if (NULL == gorc->tq_head) - gorc_resume (gorc, - 0, - TALER_EC_NONE); - return; - } - if (0 > - TALER_amount_add (&gorc->deposits_total, - &gorc->deposits_total, - &dr->details.ok.coin_contribution)) - { - gorc_report (gorc, - TALER_EC_MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE, - &tq->coin_pub, - NULL); - GNUNET_free (tq->exchange_url); - GNUNET_free (tq); - if (NULL == gorc->tq_head) - gorc_resume (gorc, - 0, - TALER_EC_NONE); - return; - } - if (0 > - TALER_amount_add (&gorc->deposit_fees_total, - &gorc->deposit_fees_total, - &tq->deposit_fee)) - { - gorc_report (gorc, - TALER_EC_MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE, - &tq->coin_pub, - NULL); - GNUNET_free (tq->exchange_url); - GNUNET_free (tq); - if (NULL == gorc->tq_head) - gorc_resume (gorc, - 0, - TALER_EC_NONE); - return; - } - break; - } - case MHD_HTTP_ACCEPTED: - { - /* got a 'preliminary' reply from the exchange, - remember our target UUID */ - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); - qs = TMH_db->account_kyc_set_status ( - TMH_db->cls, - gorc->hc->instance->settings.id, - &tq->h_wire, - tq->exchange_url, - dr->details.accepted.requirement_row, - NULL, - NULL, - now, - dr->details.accepted.kyc_ok, - dr->details.accepted.aml_decision); - if (qs < 0) - { - gorc_report (gorc, - TALER_EC_GENERIC_DB_STORE_FAILED, - &tq->coin_pub, - NULL); - GNUNET_free (tq->exchange_url); - GNUNET_free (tq); - if (NULL == gorc->tq_head) - gorc_resume (gorc, - 0, - TALER_EC_NONE); - return; - } - gorc_report (gorc, - TALER_EC_NONE, - &tq->coin_pub, - &dr->hr); - break; - } - default: - { - gorc_report (gorc, - TALER_EC_MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE, - &tq->coin_pub, - &dr->hr); - GNUNET_free (tq->exchange_url); - GNUNET_free (tq); - if (NULL == gorc->tq_head) - gorc_resume (gorc, - 0, - TALER_EC_NONE); - return; - } - } /* end switch */ - GNUNET_free (tq->exchange_url); - GNUNET_free (tq); - if (NULL != gorc->tq_head) - return; - /* *all* are done, resume! */ - gorc_resume (gorc, - 0, - TALER_EC_NONE); + 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); } /** - * Function called with the result of a #TMH_EXCHANGES_keys4exchange() - * operation. + * Processing the request @a gorc is finished, set the + * final return value in phase based on @a mret. * - * @param cls closure with a `struct GetOrderRequestContext *` - * @param keys keys of the exchange - * @param exchange representation of the exchange + * @param[in,out] gorc order context to initialize + * @param mret MHD HTTP response status to return */ static void -exchange_found_cb (void *cls, - struct TALER_EXCHANGE_Keys *keys, - struct TMH_Exchange *exchange) +phase_end (struct GetOrderRequestContext *gorc, + MHD_RESULT mret) { - struct TransferQuery *tq = cls; - struct GetOrderRequestContext *gorc = tq->gorc; + gorc->phase = (MHD_YES == mret) + ? GOP_END_YES + : GOP_END_NO; +} - (void) exchange; - tq->fo = NULL; - if (NULL == keys) + +/** + * 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)) { - /* failed */ - GNUNET_CONTAINER_DLL_remove (gorc->tq_head, - gorc->tq_tail, - tq); - GNUNET_free (tq->exchange_url); - GNUNET_free (tq); - gorc_resume (gorc, - MHD_HTTP_GATEWAY_TIMEOUT, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT); + gorc->phase++; return; } - tq->dgh = TALER_EXCHANGE_deposits_get ( - TMH_curl_ctx, - tq->exchange_url, - keys, - &gorc->hc->instance->merchant_priv, - &tq->h_wire, - &gorc->h_contract_terms, - &tq->coin_pub, - GNUNET_TIME_UNIT_ZERO, - &deposit_get_cb, - tq); - if (NULL == tq->dgh) + + 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) ) { - GNUNET_CONTAINER_DLL_remove (gorc->tq_head, - gorc->tq_tail, - 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); + 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++; } /** - * Function called with each @a coin_pub that was deposited into the - * @a h_wire account of the merchant for the @a deposit_serial as part - * of the payment for the order identified by @a cls. + * Obtain latest contract terms from the database. * - * Queries the exchange for the payment status associated with the - * given coin. - * - * @param cls a `struct GetOrderRequestContext` - * @param deposit_serial identifies the deposit operation - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param amount_with_fee amount the exchange will deposit for this coin - * @param deposit_fee fee the exchange will charge for this coin - * @param h_wire hash of the merchant's wire account into which the deposit was made - * @param coin_pub public key of the deposited coin + * @param[in,out] gorc order context to update */ static void -deposit_cb (void *cls, - uint64_t deposit_serial, - const char *exchange_url, - const struct TALER_MerchantWireHashP *h_wire, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_CoinSpendPublicKeyP *coin_pub) +phase_fetch_contract (struct GetOrderRequestContext *gorc) { - struct GetOrderRequestContext *gorc = cls; - struct TransferQuery *tq; + struct TMH_HandlerContext *hc = gorc->hc; + enum GNUNET_DB_QueryStatus qs; - tq = GNUNET_new (struct TransferQuery); - tq->gorc = gorc; - tq->exchange_url = GNUNET_strdup (exchange_url); - tq->deposit_serial = deposit_serial; - GNUNET_CONTAINER_DLL_insert (gorc->tq_head, - gorc->tq_tail, - tq); - tq->coin_pub = *coin_pub; - tq->h_wire = *h_wire; - tq->amount_with_fee = *amount_with_fee; - tq->deposit_fee = *deposit_fee; - tq->fo = TMH_EXCHANGES_keys4exchange (exchange_url, - false, - &exchange_found_cb, - tq); - if (NULL == tq->fo) + if (NULL != gorc->contract_terms) { - gorc_resume (gorc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE); + /* 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++; } /** - * Clean up the session state for a GET /private/order/ID request. + * Obtain parse contract terms of the order. Extracts the fulfillment URL, + * total amount, summary and timestamp from the contract terms! * - * @param cls closure, must be a `struct GetOrderRequestContext *` + * @param[in,out] gorc order context to update */ static void -gorc_cleanup (void *cls) +phase_parse_contract (struct GetOrderRequestContext *gorc) { - struct GetOrderRequestContext *gorc = cls; + 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 (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 (GNUNET_OK != + GNUNET_JSON_parse (gorc->contract_terms, + spec, + NULL, NULL)) { - TMH_db->event_listen_cancel (gorc->eh); - gorc->eh = 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 (NULL != gorc->session_eh) + if (! gorc->order_only) { - TMH_db->event_listen_cancel (gorc->session_eh); - gorc->session_eh = NULL; + 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_free (gorc); + GNUNET_assert (NULL != gorc->contract_terms); + gorc->phase++; } /** - * Function called with information about a refund. - * It is responsible for summing up the refund amount. + * Check payment status of the order. * - * @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 + * @param[in,out] gorc order context to update */ 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) +phase_check_paid (struct GetOrderRequestContext *gorc) { - struct GetOrderRequestContext *gorc = cls; + struct TMH_HandlerContext *hc = gorc->hc; + enum GNUNET_DB_QueryStatus qs; - 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 (gorc->order_only) { - 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->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; } - if (GNUNET_OK != - TALER_amount_cmp_currency ( - &gorc->refund_amount, - refund_amount)) + /* 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) { - gorc->refund_currency_mismatch = true; + /* 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; } - GNUNET_assert (0 <= - TALER_amount_add (&gorc->refund_amount, - &gorc->refund_amount, - refund_amount)); - gorc->refunded = true; - gorc->refund_pending |= pending; + gorc->phase++; } /** - * Function called with available wire details, to be added to - * the response. + * Check if re-purchase detection applies to the order. * - * @param cls a `struct GetOrderRequestContext` - * @param wtid wire transfer subject of the wire transfer for the coin + * @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++; +} + + +/** + * Function called with detailed wire transfer data. + * + * @param cls closure with a `struct TransferQuery *` + * @param dr HTTP response data + */ +static void +deposit_get_cb (void *cls, + const struct TALER_EXCHANGE_GetDepositResponse *dr) +{ + struct TransferQuery *tq = cls; + struct GetOrderRequestContext *gorc = tq->gorc; + + GNUNET_CONTAINER_DLL_remove (gorc->tq_head, + gorc->tq_tail, + tq); + switch (dr->hr.http_status) + { + case MHD_HTTP_OK: + { + 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); + if (qs < 0) + { + gorc_report (gorc, + TALER_EC_GENERIC_DB_STORE_FAILED, + &tq->coin_pub, + NULL); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + if (NULL == gorc->tq_head) + { + gorc->phase++; + gorc_resume (gorc); + } + return; + } + /* Compute total amount *wired* */ + if ( (GNUNET_OK != + TALER_amount_cmp_currency ( + &gorc->deposits_total, + &dr->details.ok.coin_contribution)) || + (GNUNET_OK != + TALER_amount_cmp_currency ( + &gorc->deposit_fees_total, + &tq->deposit_fee)) ) + { + /* something very wrong in our database ... */ + GNUNET_break (0); + gorc_report (gorc, + TALER_EC_GENERIC_DB_FETCH_FAILED, + &tq->coin_pub, + NULL); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + if (NULL == gorc->tq_head) + { + gorc->phase++; + gorc_resume (gorc); + } + return; + } + if (0 > + TALER_amount_add (&gorc->deposits_total, + &gorc->deposits_total, + &dr->details.ok.coin_contribution)) + { + gorc_report (gorc, + TALER_EC_MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE, + &tq->coin_pub, + NULL); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + if (NULL == gorc->tq_head) + { + gorc->phase++; + gorc_resume (gorc); + } + return; + } + if (0 > + TALER_amount_add (&gorc->deposit_fees_total, + &gorc->deposit_fees_total, + &tq->deposit_fee)) + { + gorc_report (gorc, + TALER_EC_MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE, + &tq->coin_pub, + NULL); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + if (NULL == gorc->tq_head) + { + gorc->phase++; + gorc_resume (gorc); + } + return; + } + break; + } + case MHD_HTTP_ACCEPTED: + { + /* got a 'preliminary' reply from the exchange, + remember our target UUID */ + 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, + gorc->hc->instance->settings.id, + &tq->h_wire, + tq->exchange_url, + dr->details.accepted.requirement_row, + NULL, + NULL, + now, + dr->details.accepted.kyc_ok, + dr->details.accepted.aml_decision); + if (qs < 0) + { + gorc_report (gorc, + TALER_EC_GENERIC_DB_STORE_FAILED, + &tq->coin_pub, + NULL); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + if (NULL == gorc->tq_head) + { + gorc->phase++; + gorc_resume (gorc); + } + return; + } + gorc_report (gorc, + TALER_EC_NONE, + &tq->coin_pub, + &dr->hr); + break; + } + 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, + &dr->hr); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + if (NULL == gorc->tq_head) + { + gorc->phase++; + gorc_resume (gorc); + } + return; + } + } /* end switch */ + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + if (NULL != gorc->tq_head) + return; + /* *all* are done, resume! */ + gorc->phase++; + gorc_resume (gorc); +} + + +/** + * Function called with the result of a #TMH_EXCHANGES_keys4exchange() + * operation. + * + * @param cls closure with a `struct GetOrderRequestContext *` + * @param keys keys of the exchange + * @param exchange representation of the exchange + */ +static void +exchange_found_cb (void *cls, + struct TALER_EXCHANGE_Keys *keys, + struct TMH_Exchange *exchange) +{ + struct TransferQuery *tq = cls; + struct GetOrderRequestContext *gorc = tq->gorc; + + (void) exchange; + tq->fo = NULL; + if (NULL == keys) + { + /* failed */ + GNUNET_CONTAINER_DLL_remove (gorc->tq_head, + gorc->tq_tail, + tq); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + gorc_resume_error (gorc, + MHD_HTTP_GATEWAY_TIMEOUT, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT); + return; + } + tq->dgh = TALER_EXCHANGE_deposits_get ( + TMH_curl_ctx, + tq->exchange_url, + keys, + &gorc->hc->instance->merchant_priv, + &tq->h_wire, + &gorc->h_contract_terms, + &tq->coin_pub, + GNUNET_TIME_UNIT_ZERO, + &deposit_get_cb, + tq); + if (NULL == tq->dgh) + { + GNUNET_CONTAINER_DLL_remove (gorc->tq_head, + gorc->tq_tail, + tq); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + gorc_resume_error (gorc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE); + } +} + + +/** + * Function called with each @a coin_pub that was deposited into the + * @a h_wire account of the merchant for the @a deposit_serial as part + * of the payment for the order identified by @a cls. + * + * Queries the exchange for the payment status associated with the + * given coin. + * + * @param cls a `struct GetOrderRequestContext` + * @param deposit_serial identifies the deposit operation + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param amount_with_fee amount the exchange will deposit for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param h_wire hash of the merchant's wire account into which the deposit was made + * @param coin_pub public key of the deposited coin + */ +static void +deposit_cb (void *cls, + uint64_t deposit_serial, + const char *exchange_url, + const struct TALER_MerchantWireHashP *h_wire, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_CoinSpendPublicKeyP *coin_pub) +{ + 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); + tq->deposit_serial = deposit_serial; + GNUNET_CONTAINER_DLL_insert (gorc->tq_head, + gorc->tq_tail, + tq); + tq->coin_pub = *coin_pub; + tq->h_wire = *h_wire; + tq->amount_with_fee = *amount_with_fee; + tq->deposit_fee = *deposit_fee; + tq->fo = TMH_EXCHANGES_keys4exchange (exchange_url, + false, + &exchange_found_cb, + tq); + if (NULL == tq->fo) + { + gorc_resume_error (gorc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE); + } +} + + +/** + * Check wire transfer status for the order at the exchange. + * + * @param[in,out] gorc order context to update + */ +static void +phase_check_exchange_transfers (struct GetOrderRequestContext *gorc) +{ + if (gorc->wired || + (! gorc->transfer_status_requested) ) + { + gorc->phase = GOP_CHECK_LOCAL_TRANSFERS; + return; + } + /* 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) + { + /* No deposits found for paid order. This is strange... */ + GNUNET_break (0); + gorc->phase = GOP_CHECK_LOCAL_TRANSFERS; + return; + } + 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); +} + + +/** + * Function called with available wire details, to be added to + * the response. + * + * @param cls a `struct GetOrderRequestContext` + * @param wtid wire transfer subject of the wire transfer for the coin * @param exchange_url base URL of the exchange that made the payment * @param execution_time when was the payment made * @param deposit_value contribution of the coin to the total wire transfer value @@ -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"); - } - } - 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); + "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) && - (! 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 */ } -- cgit v1.2.3