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

            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 2.0-1