/* This file is part of TALER (C) 2014-2020 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-merchant-httpd_private-post-orders-ID-refund.c * @brief Handle request to increase the refund for an order * @author Marcello Stanisci * @author Christian Grothoff */ #include "platform.h" #include #include #include #include "taler-merchant-httpd_private-post-orders-ID-refund.h" #include "taler-merchant-httpd_private-get-orders.h" /** * How often do we retry the non-trivial refund INSERT database * transaction? */ #define MAX_RETRIES 5 /** * Make a taler://refund URI * * @param connection MHD connection to take host and path from * @param instance_id merchant's instance ID, must not be NULL * @param order_id order ID to show a refund for, must not be NULL * @returns the URI, must be freed with #GNUNET_free */ static char * make_taler_refund_uri (struct MHD_Connection *connection, const char *instance_id, const char *order_id) { const char *host; const char *forwarded_host; const char *uri_path; struct GNUNET_Buffer buf = { 0 }; GNUNET_assert (NULL != instance_id); GNUNET_assert (NULL != order_id); host = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, "Host"); forwarded_host = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, "X-Forwarded-Host"); uri_path = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, "X-Forwarded-Prefix"); if (NULL != forwarded_host) host = forwarded_host; if (NULL == host) { /* Should never happen, at least the host header should be defined */ GNUNET_break (0); return NULL; } GNUNET_buffer_write_str (&buf, "taler"); if (GNUNET_NO == TALER_mhd_is_https (connection)) GNUNET_buffer_write_str (&buf, "+http"); GNUNET_buffer_write_str (&buf, "://refund/"); GNUNET_buffer_write_str (&buf, host); if (NULL != uri_path) GNUNET_buffer_write_path (&buf, uri_path); if (0 != strcmp ("default", instance_id)) { GNUNET_buffer_write_path (&buf, "instances"); GNUNET_buffer_write_path (&buf, instance_id); } GNUNET_buffer_write_path (&buf, order_id); GNUNET_buffer_write_path (&buf, ""); // Trailing slash return GNUNET_buffer_reap_str (&buf); } /** * Handle request for increasing the refund associated with * a contract. * * @param rh context of the handler * @param connection the MHD connection to handle * @param[in,out] hc context with further information about the request * @return MHD result code */ MHD_RESULT TMH_private_post_orders_ID_refund (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct TALER_Amount refund; const char *reason; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount ("refund", &refund), GNUNET_JSON_spec_string ("reason", &reason), GNUNET_JSON_spec_end () }; enum TALER_MERCHANTDB_RefundStatus rs; struct GNUNET_HashCode h_contract; { enum GNUNET_DB_QueryStatus qs; json_t *contract_terms; uint64_t order_serial; struct GNUNET_TIME_Absolute refund_deadline; struct GNUNET_TIME_Absolute timestamp; qs = TMH_db->lookup_contract_terms (TMH_db->cls, hc->instance->settings.id, hc->infix, &contract_terms, &order_serial); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_absolute_time ("refund_deadline", &refund_deadline), TALER_JSON_spec_absolute_time ("timestamp", ×tamp), GNUNET_JSON_spec_end () }; if (GNUNET_YES != GNUNET_JSON_parse (contract_terms, spec, NULL, NULL)) { GNUNET_break (0); GNUNET_JSON_parse_free (spec); json_decref (contract_terms); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, "mandatory fields missing"); } json_decref (contract_terms); if (timestamp.abs_value_us == refund_deadline.abs_value_us) { /* refund was never allowed, so we should refuse hard */ return TALER_MHD_reply_with_error (connection, MHD_HTTP_FORBIDDEN, #ifndef TALER_EC_MERCHANT_REFUND_NOT_ALLOWED_BY_CONTRACT 2609, /* remove post 0.8.0 release */ #else TALER_EC_MERCHANT_REFUND_NOT_ALLOWED_BY_CONTRACT, #endif NULL); } if (0 == GNUNET_TIME_absolute_get_remaining ( refund_deadline).rel_value_us) { /* it is too late for refunds */ /* NOTE: We MAY still be lucky that the exchange did not yet wire the funds, so we will try to give the refund anyway */ } } else { return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, hc->infix); } } { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, hc->request_body, spec); if (GNUNET_OK != res) return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } TMH_db->preflight (TMH_db->cls); for (unsigned int i = 0; istart (TMH_db->cls, "increase refund")) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_START_FAILED, NULL); } rs = TMH_db->increase_refund (TMH_db->cls, hc->instance->settings.id, hc->infix, &refund, reason); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "increase refund returned %d\n", rs); if (TALER_MERCHANTDB_RS_SUCCESS != rs) TMH_db->rollback (TMH_db->cls); if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs) continue; if (TALER_MERCHANTDB_RS_SUCCESS == rs) { enum GNUNET_DB_QueryStatus qs; qs = TMH_db->commit (TMH_db->cls); if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); rs = TALER_MERCHANTDB_RS_HARD_ERROR; break; } if (GNUNET_DB_STATUS_SOFT_ERROR == qs) continue; } break; } /* retries loop */ switch (rs) { case TALER_MERCHANTDB_RS_TOO_HIGH: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Refusing refund amount %s that is larger than original payment\n", TALER_amount2s (&refund)); return TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, "Amount above payment"); case TALER_MERCHANTDB_RS_SOFT_ERROR: case TALER_MERCHANTDB_RS_HARD_ERROR: return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); case TALER_MERCHANTDB_RS_NO_SUCH_ORDER: { /* We know the order exists from the "lookup_contract_terms" at the beginning; so if we get 'no such order' here, it must be read as "no PAID order" */ return TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID, hc->infix); } case TALER_MERCHANTDB_RS_SUCCESS: { enum GNUNET_DB_QueryStatus qs; json_t *contract_terms; uint64_t order_serial; qs = TMH_db->lookup_contract_terms (TMH_db->cls, hc->instance->settings.id, hc->infix, &contract_terms, &order_serial); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, hc->infix); } if (GNUNET_OK != TALER_JSON_contract_hash (contract_terms, &h_contract)) { GNUNET_break (0); json_decref (contract_terms); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, "Could not hash contract terms"); } json_decref (contract_terms); } break; } /* Resume clients that may wait for this refund */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Awakeing clients on %s waiting for refund of less than %s\n", hc->infix, TALER_amount2s (&refund)); TMH_long_poll_resume (hc->infix, hc->instance, &refund, false); { struct GNUNET_TIME_Absolute timestamp; uint64_t order_serial; enum GNUNET_DB_QueryStatus qs; qs = TMH_db->lookup_order_summary (TMH_db->cls, hc->instance->settings.id, hc->infix, ×tamp, &order_serial); if (0 >= qs) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_INVARIANT_FAILURE, NULL); } TMH_notify_order_change (hc->instance, hc->infix, true, /* paid */ true, /* refunded */ false, /* wired, cannot be if we could still do refunds */ timestamp, order_serial); } { MHD_RESULT ret; char *taler_refund_uri; taler_refund_uri = make_taler_refund_uri (connection, hc->instance->settings.id, hc->infix); ret = TALER_MHD_reply_json_pack (connection, MHD_HTTP_OK, "{s:s, s:o}", "taler_refund_uri", taler_refund_uri, "h_contract", GNUNET_JSON_from_data_auto (&h_contract)); GNUNET_free (taler_refund_uri); return ret; } } /* end of taler-merchant-httpd_private-post-orders-ID-refund.c */