LCOV - code coverage report
Current view: top level - backend - taler-merchant-httpd_private-post-orders-ID-refund.c (source / functions) Hit Total Coverage
Test: coverage.info Lines: 89 130 68.5 %
Date: 2025-06-23 16:22:09 Functions: 4 4 100.0 %

          Line data    Source code
       1             : /*
       2             :   This file is part of TALER
       3             :   (C) 2014-2024 Taler Systems SA
       4             : 
       5             :   TALER is free software; you can redistribute it and/or modify it under the
       6             :   terms of the GNU Affero General Public License as published by the Free Software
       7             :   Foundation; either version 3, or (at your option) any later version.
       8             : 
       9             :   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
      10             :   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
      11             :   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
      12             : 
      13             :   You should have received a copy of the GNU General Public License along with
      14             :   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
      15             : */
      16             : /**
      17             :  * @file taler-merchant-httpd_private-post-orders-ID-refund.c
      18             :  * @brief Handle request to increase the refund for an order
      19             :  * @author Marcello Stanisci
      20             :  * @author Christian Grothoff
      21             :  */
      22             : #include "platform.h"
      23             : #include <jansson.h>
      24             : #include <taler/taler_dbevents.h>
      25             : #include <taler/taler_signatures.h>
      26             : #include <taler/taler_json_lib.h>
      27             : #include "taler-merchant-httpd_private-post-orders-ID-refund.h"
      28             : #include "taler-merchant-httpd_private-get-orders.h"
      29             : #include "taler-merchant-httpd_helper.h"
      30             : #include "taler-merchant-httpd_exchanges.h"
      31             : 
      32             : 
      33             : /**
      34             :  * How often do we retry the non-trivial refund INSERT database
      35             :  * transaction?
      36             :  */
      37             : #define MAX_RETRIES 5
      38             : 
      39             : 
      40             : /**
      41             :  * Use database to notify other clients about the
      42             :  * @a order_id being refunded
      43             :  *
      44             :  * @param hc handler context we operate in
      45             :  * @param amount the (total) refunded amount
      46             :  */
      47             : static void
      48           6 : trigger_refund_notification (
      49             :   struct TMH_HandlerContext *hc,
      50             :   const struct TALER_Amount *amount)
      51             : {
      52             :   const char *as;
      53           6 :   struct TMH_OrderRefundEventP refund_eh = {
      54           6 :     .header.size = htons (sizeof (refund_eh)),
      55           6 :     .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND),
      56           6 :     .merchant_pub = hc->instance->merchant_pub
      57             :   };
      58             : 
      59             :   /* Resume clients that may wait for this refund */
      60           6 :   as = TALER_amount2s (amount);
      61           6 :   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
      62             :               "Awakening clients on %s waiting for refund of no more than %s\n",
      63             :               hc->infix,
      64             :               as);
      65           6 :   GNUNET_CRYPTO_hash (hc->infix,
      66           6 :                       strlen (hc->infix),
      67             :                       &refund_eh.h_order_id);
      68           6 :   TMH_db->event_notify (TMH_db->cls,
      69             :                         &refund_eh.header,
      70             :                         as,
      71             :                         strlen (as));
      72           6 : }
      73             : 
      74             : 
      75             : /**
      76             :  * Make a taler://refund URI
      77             :  *
      78             :  * @param connection MHD connection to take host and path from
      79             :  * @param instance_id merchant's instance ID, must not be NULL
      80             :  * @param order_id order ID to show a refund for, must not be NULL
      81             :  * @returns the URI, must be freed with #GNUNET_free
      82             :  */
      83             : static char *
      84           6 : make_taler_refund_uri (struct MHD_Connection *connection,
      85             :                        const char *instance_id,
      86             :                        const char *order_id)
      87             : {
      88             :   struct GNUNET_Buffer buf;
      89             : 
      90           6 :   GNUNET_assert (NULL != instance_id);
      91           6 :   GNUNET_assert (NULL != order_id);
      92           6 :   if (GNUNET_OK !=
      93           6 :       TMH_taler_uri_by_connection (connection,
      94             :                                    "refund",
      95             :                                    instance_id,
      96             :                                    &buf))
      97             :   {
      98           0 :     GNUNET_break (0);
      99           0 :     return NULL;
     100             :   }
     101           6 :   GNUNET_buffer_write_path (&buf,
     102             :                             order_id);
     103           6 :   GNUNET_buffer_write_path (&buf,
     104             :                             ""); /* Trailing slash */
     105           6 :   return GNUNET_buffer_reap_str (&buf);
     106             : }
     107             : 
     108             : 
     109             : /**
     110             :  * Wrapper around #TMH_EXCHANGES_get_limit() that
     111             :  * determines the refund limit for a given @a exchange_url
     112             :  *
     113             :  * @param cls unused
     114             :  * @param exchange_url base URL of the exchange to get
     115             :  *   the refund limit for
     116             :  * @param[in,out] amount lowered to the maximum refund
     117             :  *   allowed at the exchange
     118             :  */
     119             : static void
     120           6 : get_refund_limit (void *cls,
     121             :                   const char *exchange_url,
     122             :                   struct TALER_Amount *amount)
     123             : {
     124             :   (void) cls;
     125           6 :   TMH_EXCHANGES_get_limit (exchange_url,
     126             :                            TALER_KYCLOGIC_KYC_TRIGGER_REFUND,
     127             :                            amount);
     128           6 : }
     129             : 
     130             : 
     131             : /**
     132             :  * Handle request for increasing the refund associated with
     133             :  * a contract.
     134             :  *
     135             :  * @param rh context of the handler
     136             :  * @param connection the MHD connection to handle
     137             :  * @param[in,out] hc context with further information about the request
     138             :  * @return MHD result code
     139             :  */
     140             : MHD_RESULT
     141          10 : TMH_private_post_orders_ID_refund (
     142             :   const struct TMH_RequestHandler *rh,
     143             :   struct MHD_Connection *connection,
     144             :   struct TMH_HandlerContext *hc)
     145             : {
     146             :   struct TALER_Amount refund;
     147             :   const char *reason;
     148             :   struct GNUNET_JSON_Specification spec[] = {
     149          10 :     TALER_JSON_spec_amount_any ("refund",
     150             :                                 &refund),
     151          10 :     GNUNET_JSON_spec_string ("reason",
     152             :                              &reason),
     153          10 :     GNUNET_JSON_spec_end ()
     154             :   };
     155             :   enum TALER_MERCHANTDB_RefundStatus rs;
     156             :   struct TALER_PrivateContractHashP h_contract;
     157             :   json_t *contract_terms;
     158             :   struct GNUNET_TIME_Timestamp timestamp;
     159             : 
     160             :   {
     161             :     enum GNUNET_GenericReturnValue res;
     162             : 
     163          10 :     res = TALER_MHD_parse_json_data (connection,
     164          10 :                                      hc->request_body,
     165             :                                      spec);
     166          10 :     if (GNUNET_OK != res)
     167             :     {
     168             :       return (GNUNET_NO == res)
     169             :              ? MHD_YES
     170           0 :              : MHD_NO;
     171             :     }
     172             :   }
     173             : 
     174             :   {
     175             :     enum GNUNET_DB_QueryStatus qs;
     176             :     uint64_t order_serial;
     177             :     struct GNUNET_TIME_Timestamp refund_deadline;
     178             : 
     179          10 :     qs = TMH_db->lookup_contract_terms (TMH_db->cls,
     180          10 :                                         hc->instance->settings.id,
     181          10 :                                         hc->infix,
     182             :                                         &contract_terms,
     183             :                                         &order_serial,
     184             :                                         NULL);
     185          10 :     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
     186             :     {
     187           2 :       if (qs < 0)
     188             :       {
     189           0 :         GNUNET_break (0);
     190           2 :         return TALER_MHD_reply_with_error (
     191             :           connection,
     192             :           MHD_HTTP_INTERNAL_SERVER_ERROR,
     193             :           TALER_EC_GENERIC_DB_FETCH_FAILED,
     194             :           "lookup_contract_terms");
     195             :       }
     196           2 :       return TALER_MHD_reply_with_error (
     197             :         connection,
     198             :         MHD_HTTP_NOT_FOUND,
     199             :         TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
     200           2 :         hc->infix);
     201             :     }
     202           8 :     if (GNUNET_OK !=
     203           8 :         TALER_JSON_contract_hash (contract_terms,
     204             :                                   &h_contract))
     205             :     {
     206           0 :       GNUNET_break (0);
     207           0 :       json_decref (contract_terms);
     208           0 :       return TALER_MHD_reply_with_error (
     209             :         connection,
     210             :         MHD_HTTP_INTERNAL_SERVER_ERROR,
     211             :         TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
     212             :         "Could not hash contract terms");
     213             :     }
     214             :     {
     215             :       struct GNUNET_JSON_Specification cspec[] = {
     216           8 :         GNUNET_JSON_spec_timestamp ("refund_deadline",
     217             :                                     &refund_deadline),
     218           8 :         GNUNET_JSON_spec_timestamp ("timestamp",
     219             :                                     &timestamp),
     220           8 :         GNUNET_JSON_spec_end ()
     221             :       };
     222             : 
     223           8 :       if (GNUNET_YES !=
     224           8 :           GNUNET_JSON_parse (contract_terms,
     225             :                              cspec,
     226             :                              NULL, NULL))
     227             :       {
     228           0 :         GNUNET_break (0);
     229           0 :         json_decref (contract_terms);
     230           0 :         return TALER_MHD_reply_with_error (
     231             :           connection,
     232             :           MHD_HTTP_INTERNAL_SERVER_ERROR,
     233             :           TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
     234             :           "mandatory fields missing");
     235             :       }
     236           8 :       if (GNUNET_TIME_timestamp_cmp (timestamp,
     237             :                                      ==,
     238             :                                      refund_deadline))
     239             :       {
     240             :         /* refund was never allowed, so we should refuse hard */
     241           0 :         json_decref (contract_terms);
     242           0 :         return TALER_MHD_reply_with_error (
     243             :           connection,
     244             :           MHD_HTTP_FORBIDDEN,
     245             :           TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT,
     246             :           NULL);
     247             :       }
     248           8 :       if (GNUNET_TIME_absolute_is_past (refund_deadline.abs_time))
     249             :       {
     250             :         /* it is too late for refunds */
     251             :         /* NOTE: We MAY still be lucky that the exchange did not yet
     252             :            wire the funds, so we will try to give the refund anyway */
     253             :       }
     254             :     }
     255             :   }
     256             : 
     257           8 :   TMH_db->preflight (TMH_db->cls);
     258           8 :   for (unsigned int i = 0; i<MAX_RETRIES; i++)
     259             :   {
     260           8 :     if (GNUNET_OK !=
     261           8 :         TMH_db->start (TMH_db->cls,
     262             :                        "increase refund"))
     263             :     {
     264           0 :       GNUNET_break (0);
     265           0 :       json_decref (contract_terms);
     266           0 :       return TALER_MHD_reply_with_error (connection,
     267             :                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
     268             :                                          TALER_EC_GENERIC_DB_START_FAILED,
     269             :                                          NULL);
     270             :     }
     271           8 :     rs = TMH_db->increase_refund (TMH_db->cls,
     272           8 :                                   hc->instance->settings.id,
     273           8 :                                   hc->infix,
     274             :                                   &refund,
     275             :                                   &get_refund_limit,
     276             :                                   NULL,
     277             :                                   reason);
     278           8 :     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
     279             :                 "increase refund returned %d\n",
     280             :                 rs);
     281           8 :     if (TALER_MERCHANTDB_RS_SUCCESS != rs)
     282           2 :       TMH_db->rollback (TMH_db->cls);
     283           8 :     if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs)
     284           0 :       continue;
     285           8 :     if (TALER_MERCHANTDB_RS_SUCCESS == rs)
     286             :     {
     287             :       enum GNUNET_DB_QueryStatus qs;
     288             :       json_t *rargs;
     289             : 
     290           6 :       rargs = GNUNET_JSON_PACK (
     291             :         GNUNET_JSON_pack_timestamp ("timestamp",
     292             :                                     timestamp),
     293             :         GNUNET_JSON_pack_string ("order_id",
     294             :                                  hc->infix),
     295             :         GNUNET_JSON_pack_object_incref ("contract_terms",
     296             :                                         contract_terms),
     297             :         TALER_JSON_pack_amount ("refund_amount",
     298             :                                 &refund),
     299             :         GNUNET_JSON_pack_string ("reason",
     300             :                                  reason)
     301             :         );
     302           6 :       GNUNET_assert (NULL != rargs);
     303           6 :       qs = TMH_trigger_webhook (
     304           6 :         hc->instance->settings.id,
     305             :         "refund",
     306             :         rargs);
     307           6 :       json_decref (rargs);
     308           6 :       switch (qs)
     309             :       {
     310           0 :       case GNUNET_DB_STATUS_HARD_ERROR:
     311           0 :         GNUNET_break (0);
     312           0 :         TMH_db->rollback (TMH_db->cls);
     313           0 :         rs = TALER_MERCHANTDB_RS_HARD_ERROR;
     314           0 :         break;
     315           0 :       case GNUNET_DB_STATUS_SOFT_ERROR:
     316           0 :         TMH_db->rollback (TMH_db->cls);
     317           0 :         continue;
     318           6 :       case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     319             :       case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     320           6 :         qs = TMH_db->commit (TMH_db->cls);
     321           6 :         break;
     322             :       }
     323           6 :       if (GNUNET_DB_STATUS_HARD_ERROR == qs)
     324             :       {
     325           0 :         GNUNET_break (0);
     326           0 :         rs = TALER_MERCHANTDB_RS_HARD_ERROR;
     327           0 :         break;
     328             :       }
     329           6 :       if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
     330           0 :         continue;
     331           6 :       trigger_refund_notification (hc,
     332             :                                    &refund);
     333             :     }
     334           8 :     break;
     335             :   } /* retries loop */
     336           8 :   json_decref (contract_terms);
     337             : 
     338           8 :   switch (rs)
     339             :   {
     340           0 :   case TALER_MERCHANTDB_RS_LEGAL_FAILURE:
     341           0 :     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
     342             :                 "Refund amount %s exceeded legal limits of the exchanges involved\n",
     343             :                 TALER_amount2s (&refund));
     344           0 :     return TALER_MHD_reply_with_error (
     345             :       connection,
     346             :       MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
     347             :       TALER_EC_MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION,
     348             :       NULL);
     349           0 :   case TALER_MERCHANTDB_RS_BAD_CURRENCY:
     350           0 :     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
     351             :                 "Refund amount %s is not in the currency of the original payment\n",
     352             :                 TALER_amount2s (&refund));
     353           0 :     return TALER_MHD_reply_with_error (
     354             :       connection,
     355             :       MHD_HTTP_CONFLICT,
     356             :       TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH,
     357             :       "Order was paid in a different currency");
     358           0 :   case TALER_MERCHANTDB_RS_TOO_HIGH:
     359           0 :     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
     360             :                 "Refusing refund amount %s that is larger than original payment\n",
     361             :                 TALER_amount2s (&refund));
     362           0 :     return TALER_MHD_reply_with_error (
     363             :       connection,
     364             :       MHD_HTTP_CONFLICT,
     365             :       TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT,
     366             :       "Amount above payment");
     367           0 :   case TALER_MERCHANTDB_RS_SOFT_ERROR:
     368             :   case TALER_MERCHANTDB_RS_HARD_ERROR:
     369           0 :     return TALER_MHD_reply_with_error (
     370             :       connection,
     371             :       MHD_HTTP_INTERNAL_SERVER_ERROR,
     372             :       TALER_EC_GENERIC_DB_COMMIT_FAILED,
     373             :       NULL);
     374           2 :   case TALER_MERCHANTDB_RS_NO_SUCH_ORDER:
     375             :     /* We know the order exists from the
     376             :        "lookup_contract_terms" at the beginning;
     377             :        so if we get 'no such order' here, it
     378             :        must be read as "no PAID order" */
     379           2 :     return TALER_MHD_reply_with_error (
     380             :       connection,
     381             :       MHD_HTTP_CONFLICT,
     382             :       TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID,
     383           2 :       hc->infix);
     384           6 :   case TALER_MERCHANTDB_RS_SUCCESS:
     385             :     /* continued below */
     386           6 :     break;
     387             :   } /* end switch */
     388             : 
     389             :   {
     390             :     uint64_t order_serial;
     391             :     enum GNUNET_DB_QueryStatus qs;
     392             : 
     393           6 :     qs = TMH_db->lookup_order_summary (TMH_db->cls,
     394           6 :                                        hc->instance->settings.id,
     395           6 :                                        hc->infix,
     396             :                                        &timestamp,
     397             :                                        &order_serial);
     398           6 :     if (0 >= qs)
     399             :     {
     400           0 :       GNUNET_break (0);
     401           0 :       return TALER_MHD_reply_with_error (
     402             :         connection,
     403             :         MHD_HTTP_INTERNAL_SERVER_ERROR,
     404             :         TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
     405             :         NULL);
     406             :     }
     407           6 :     TMH_notify_order_change (hc->instance,
     408             :                              TMH_OSF_CLAIMED
     409             :                              | TMH_OSF_PAID
     410             :                              | TMH_OSF_REFUNDED,
     411             :                              timestamp,
     412             :                              order_serial);
     413             :   }
     414             :   {
     415             :     MHD_RESULT ret;
     416             :     char *taler_refund_uri;
     417             : 
     418           6 :     taler_refund_uri = make_taler_refund_uri (connection,
     419           6 :                                               hc->instance->settings.id,
     420           6 :                                               hc->infix);
     421           6 :     ret = TALER_MHD_REPLY_JSON_PACK (
     422             :       connection,
     423             :       MHD_HTTP_OK,
     424             :       GNUNET_JSON_pack_string ("taler_refund_uri",
     425             :                                taler_refund_uri),
     426             :       GNUNET_JSON_pack_data_auto ("h_contract",
     427             :                                   &h_contract));
     428           6 :     GNUNET_free (taler_refund_uri);
     429           6 :     return ret;
     430             :   }
     431             : }
     432             : 
     433             : 
     434             : /* end of taler-merchant-httpd_private-post-orders-ID-refund.c */

Generated by: LCOV version 1.16