/* This file is part of TALER (C) 2014-2023 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 #include "taler-merchant-httpd_private-post-orders-ID-refund.h" #include "taler-merchant-httpd_private-get-orders.h" #include "taler-merchant-httpd_helper.h" /** * How often do we retry the non-trivial refund INSERT database * transaction? */ #define MAX_RETRIES 5 /** * Use database to notify other clients about the * @a order_id being refunded * * @param hc handler context we operate in * @param amount the (total) refunded amount */ static void trigger_refund_notification (struct TMH_HandlerContext *hc, const struct TALER_Amount *amount) { const char *as; struct TMH_OrderRefundEventP refund_eh = { .header.size = htons (sizeof (refund_eh)), .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND), .merchant_pub = hc->instance->merchant_pub }; /* Resume clients that may wait for this refund */ as = TALER_amount2s (amount); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Awakening clients on %s waiting for refund of no more than %s\n", hc->infix, as); GNUNET_CRYPTO_hash (hc->infix, strlen (hc->infix), &refund_eh.h_order_id); TMH_db->event_notify (TMH_db->cls, &refund_eh.header, as, strlen (as)); } /** * 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) { struct GNUNET_Buffer buf; GNUNET_assert (NULL != instance_id); GNUNET_assert (NULL != order_id); if (GNUNET_OK != TMH_taler_uri_by_connection (connection, "refund", instance_id, &buf)) { GNUNET_break (0); return NULL; } 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_any ("refund", &refund), GNUNET_JSON_spec_string ("reason", &reason), GNUNET_JSON_spec_end () }; enum TALER_MERCHANTDB_RefundStatus rs; struct TALER_PrivateContractHashP h_contract; json_t *contract_terms; struct GNUNET_TIME_Timestamp timestamp; { 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; } } { enum GNUNET_DB_QueryStatus qs; uint64_t order_serial; struct GNUNET_TIME_Timestamp refund_deadline; bool paid = false; qs = TMH_db->lookup_contract_terms (TMH_db->cls, hc->instance->settings.id, hc->infix, &contract_terms, &order_serial, &paid, NULL); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { if (qs < 0) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup_contract_terms"); } 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"); } { struct GNUNET_JSON_Specification cspec[] = { GNUNET_JSON_spec_timestamp ("refund_deadline", &refund_deadline), GNUNET_JSON_spec_timestamp ("timestamp", ×tamp), GNUNET_JSON_spec_end () }; if (GNUNET_YES != GNUNET_JSON_parse (contract_terms, cspec, NULL, NULL)) { GNUNET_break (0); 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"); } if (GNUNET_TIME_timestamp_cmp (timestamp, ==, refund_deadline)) { /* refund was never allowed, so we should refuse hard */ json_decref (contract_terms); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_FORBIDDEN, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT, NULL); } if (GNUNET_TIME_absolute_is_past (refund_deadline.abs_time)) { /* 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 */ } } } TMH_db->preflight (TMH_db->cls); for (unsigned int i = 0; istart (TMH_db->cls, "increase refund")) { GNUNET_break (0); json_decref (contract_terms); 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; json_t *rargs; rargs = GNUNET_JSON_PACK ( GNUNET_JSON_pack_timestamp ("timestamp", timestamp), GNUNET_JSON_pack_string ("order_id", hc->infix), GNUNET_JSON_pack_object_incref ("contract_terms", contract_terms), TALER_JSON_pack_amount ("refund_amount", &refund), GNUNET_JSON_pack_string ("reason", reason) ); GNUNET_assert (NULL != rargs); qs = TMH_trigger_webhook ( hc->instance->settings.id, "refund", rargs); json_decref (rargs); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_db->rollback (TMH_db->cls); rs = TALER_MERCHANTDB_RS_HARD_ERROR; break; case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); continue; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: qs = TMH_db->commit (TMH_db->cls); break; } 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; trigger_refund_notification (hc, &refund); } break; } /* retries loop */ json_decref (contract_terms); switch (rs) { case TALER_MERCHANTDB_RS_BAD_CURRENCY: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Refund amount %s is not in the currency of the original payment\n", TALER_amount2s (&refund)); return TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, "Order was paid in a different currency"); 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: /* continued below */ break; } /* end switch */ { struct GNUNET_TIME_Timestamp 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, TMH_OSF_CLAIMED | TMH_OSF_PAID | TMH_OSF_REFUNDED, 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, GNUNET_JSON_pack_string ("taler_refund_uri", taler_refund_uri), GNUNET_JSON_pack_data_auto ("h_contract", &h_contract)); GNUNET_free (taler_refund_uri); return ret; } } /* end of taler-merchant-httpd_private-post-orders-ID-refund.c */