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.4 % 133 91
Test Date: 2025-12-16 19:21:50 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              :     struct GNUNET_TIME_Timestamp wire_deadline;
     179              : 
     180           10 :     qs = TMH_db->lookup_contract_terms (TMH_db->cls,
     181           10 :                                         hc->instance->settings.id,
     182           10 :                                         hc->infix,
     183              :                                         &contract_terms,
     184              :                                         &order_serial,
     185              :                                         NULL);
     186           10 :     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
     187              :     {
     188            2 :       if (qs < 0)
     189              :       {
     190            0 :         GNUNET_break (0);
     191            2 :         return TALER_MHD_reply_with_error (
     192              :           connection,
     193              :           MHD_HTTP_INTERNAL_SERVER_ERROR,
     194              :           TALER_EC_GENERIC_DB_FETCH_FAILED,
     195              :           "lookup_contract_terms");
     196              :       }
     197            2 :       return TALER_MHD_reply_with_error (
     198              :         connection,
     199              :         MHD_HTTP_NOT_FOUND,
     200              :         TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
     201            2 :         hc->infix);
     202              :     }
     203            8 :     if (GNUNET_OK !=
     204            8 :         TALER_JSON_contract_hash (contract_terms,
     205              :                                   &h_contract))
     206              :     {
     207            0 :       GNUNET_break (0);
     208            0 :       json_decref (contract_terms);
     209            0 :       return TALER_MHD_reply_with_error (
     210              :         connection,
     211              :         MHD_HTTP_INTERNAL_SERVER_ERROR,
     212              :         TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
     213              :         "Could not hash contract terms");
     214              :     }
     215              :     {
     216              :       struct GNUNET_JSON_Specification cspec[] = {
     217            8 :         GNUNET_JSON_spec_timestamp ("refund_deadline",
     218              :                                     &refund_deadline),
     219            8 :         GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
     220              :                                     &wire_deadline),
     221            8 :         GNUNET_JSON_spec_timestamp ("timestamp",
     222              :                                     &timestamp),
     223            8 :         GNUNET_JSON_spec_end ()
     224              :       };
     225              : 
     226            8 :       if (GNUNET_YES !=
     227            8 :           GNUNET_JSON_parse (contract_terms,
     228              :                              cspec,
     229              :                              NULL, NULL))
     230              :       {
     231            0 :         GNUNET_break (0);
     232            0 :         json_decref (contract_terms);
     233            0 :         return TALER_MHD_reply_with_error (
     234              :           connection,
     235              :           MHD_HTTP_INTERNAL_SERVER_ERROR,
     236              :           TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
     237              :           "mandatory fields missing");
     238              :       }
     239            8 :       if (GNUNET_TIME_timestamp_cmp (timestamp,
     240              :                                      ==,
     241              :                                      refund_deadline))
     242              :       {
     243              :         /* refund was never allowed, so we should refuse hard */
     244            0 :         json_decref (contract_terms);
     245            0 :         return TALER_MHD_reply_with_error (
     246              :           connection,
     247              :           MHD_HTTP_FORBIDDEN,
     248              :           TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT,
     249              :           NULL);
     250              :       }
     251            8 :       if (GNUNET_TIME_absolute_is_past (refund_deadline.abs_time))
     252              :       {
     253              :         /* it is too late for refunds */
     254              :         /* NOTE: We MAY still be lucky that the exchange did not yet
     255              :            wire the funds, so we will try to give the refund anyway */
     256              :       }
     257            8 :       if (GNUNET_TIME_absolute_is_past (wire_deadline.abs_time))
     258              :       {
     259              :         /* it is *really* too late for refunds */
     260            0 :         return TALER_MHD_reply_with_error (
     261              :           connection,
     262              :           MHD_HTTP_GONE,
     263              :           TALER_EC_MERCHANT_PRIVATE_POST_REFUND_AFTER_WIRE_DEADLINE,
     264              :           NULL);
     265              :       }
     266              :     }
     267              :   }
     268              : 
     269            8 :   TMH_db->preflight (TMH_db->cls);
     270            8 :   for (unsigned int i = 0; i<MAX_RETRIES; i++)
     271              :   {
     272            8 :     if (GNUNET_OK !=
     273            8 :         TMH_db->start (TMH_db->cls,
     274              :                        "increase refund"))
     275              :     {
     276            0 :       GNUNET_break (0);
     277            0 :       json_decref (contract_terms);
     278            0 :       return TALER_MHD_reply_with_error (connection,
     279              :                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
     280              :                                          TALER_EC_GENERIC_DB_START_FAILED,
     281              :                                          NULL);
     282              :     }
     283            8 :     rs = TMH_db->increase_refund (TMH_db->cls,
     284            8 :                                   hc->instance->settings.id,
     285            8 :                                   hc->infix,
     286              :                                   &refund,
     287              :                                   &get_refund_limit,
     288              :                                   NULL,
     289              :                                   reason);
     290            8 :     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
     291              :                 "increase refund returned %d\n",
     292              :                 rs);
     293            8 :     if (TALER_MERCHANTDB_RS_SUCCESS != rs)
     294            2 :       TMH_db->rollback (TMH_db->cls);
     295            8 :     if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs)
     296            0 :       continue;
     297            8 :     if (TALER_MERCHANTDB_RS_SUCCESS == rs)
     298              :     {
     299              :       enum GNUNET_DB_QueryStatus qs;
     300              :       json_t *rargs;
     301              : 
     302            6 :       rargs = GNUNET_JSON_PACK (
     303              :         GNUNET_JSON_pack_timestamp ("timestamp",
     304              :                                     timestamp),
     305              :         GNUNET_JSON_pack_string ("order_id",
     306              :                                  hc->infix),
     307              :         GNUNET_JSON_pack_object_incref ("contract_terms",
     308              :                                         contract_terms),
     309              :         TALER_JSON_pack_amount ("refund_amount",
     310              :                                 &refund),
     311              :         GNUNET_JSON_pack_string ("reason",
     312              :                                  reason)
     313              :         );
     314            6 :       GNUNET_assert (NULL != rargs);
     315            6 :       qs = TMH_trigger_webhook (
     316            6 :         hc->instance->settings.id,
     317              :         "refund",
     318              :         rargs);
     319            6 :       json_decref (rargs);
     320            6 :       switch (qs)
     321              :       {
     322            0 :       case GNUNET_DB_STATUS_HARD_ERROR:
     323            0 :         GNUNET_break (0);
     324            0 :         TMH_db->rollback (TMH_db->cls);
     325            0 :         rs = TALER_MERCHANTDB_RS_HARD_ERROR;
     326            0 :         break;
     327            0 :       case GNUNET_DB_STATUS_SOFT_ERROR:
     328            0 :         TMH_db->rollback (TMH_db->cls);
     329            0 :         continue;
     330            6 :       case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     331              :       case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     332            6 :         qs = TMH_db->commit (TMH_db->cls);
     333            6 :         break;
     334              :       }
     335            6 :       if (GNUNET_DB_STATUS_HARD_ERROR == qs)
     336              :       {
     337            0 :         GNUNET_break (0);
     338            0 :         rs = TALER_MERCHANTDB_RS_HARD_ERROR;
     339            0 :         break;
     340              :       }
     341            6 :       if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
     342            0 :         continue;
     343            6 :       trigger_refund_notification (hc,
     344              :                                    &refund);
     345              :     }
     346            8 :     break;
     347              :   } /* retries loop */
     348            8 :   json_decref (contract_terms);
     349              : 
     350            8 :   switch (rs)
     351              :   {
     352            0 :   case TALER_MERCHANTDB_RS_LEGAL_FAILURE:
     353            0 :     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
     354              :                 "Refund amount %s exceeded legal limits of the exchanges involved\n",
     355              :                 TALER_amount2s (&refund));
     356            0 :     return TALER_MHD_reply_with_error (
     357              :       connection,
     358              :       MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
     359              :       TALER_EC_MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION,
     360              :       NULL);
     361            0 :   case TALER_MERCHANTDB_RS_BAD_CURRENCY:
     362            0 :     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
     363              :                 "Refund amount %s is not in the currency of the original payment\n",
     364              :                 TALER_amount2s (&refund));
     365            0 :     return TALER_MHD_reply_with_error (
     366              :       connection,
     367              :       MHD_HTTP_CONFLICT,
     368              :       TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH,
     369              :       "Order was paid in a different currency");
     370            0 :   case TALER_MERCHANTDB_RS_TOO_HIGH:
     371            0 :     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
     372              :                 "Refusing refund amount %s that is larger than original payment\n",
     373              :                 TALER_amount2s (&refund));
     374            0 :     return TALER_MHD_reply_with_error (
     375              :       connection,
     376              :       MHD_HTTP_CONFLICT,
     377              :       TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT,
     378              :       "Amount above payment");
     379            0 :   case TALER_MERCHANTDB_RS_SOFT_ERROR:
     380              :   case TALER_MERCHANTDB_RS_HARD_ERROR:
     381            0 :     return TALER_MHD_reply_with_error (
     382              :       connection,
     383              :       MHD_HTTP_INTERNAL_SERVER_ERROR,
     384              :       TALER_EC_GENERIC_DB_COMMIT_FAILED,
     385              :       NULL);
     386            2 :   case TALER_MERCHANTDB_RS_NO_SUCH_ORDER:
     387              :     /* We know the order exists from the
     388              :        "lookup_contract_terms" at the beginning;
     389              :        so if we get 'no such order' here, it
     390              :        must be read as "no PAID order" */
     391            2 :     return TALER_MHD_reply_with_error (
     392              :       connection,
     393              :       MHD_HTTP_CONFLICT,
     394              :       TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID,
     395            2 :       hc->infix);
     396            6 :   case TALER_MERCHANTDB_RS_SUCCESS:
     397              :     /* continued below */
     398            6 :     break;
     399              :   } /* end switch */
     400              : 
     401              :   {
     402              :     uint64_t order_serial;
     403              :     enum GNUNET_DB_QueryStatus qs;
     404              : 
     405            6 :     qs = TMH_db->lookup_order_summary (TMH_db->cls,
     406            6 :                                        hc->instance->settings.id,
     407            6 :                                        hc->infix,
     408              :                                        &timestamp,
     409              :                                        &order_serial);
     410            6 :     if (0 >= qs)
     411              :     {
     412            0 :       GNUNET_break (0);
     413            0 :       return TALER_MHD_reply_with_error (
     414              :         connection,
     415              :         MHD_HTTP_INTERNAL_SERVER_ERROR,
     416              :         TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
     417              :         NULL);
     418              :     }
     419            6 :     TMH_notify_order_change (hc->instance,
     420              :                              TMH_OSF_CLAIMED
     421              :                              | TMH_OSF_PAID
     422              :                              | TMH_OSF_REFUNDED,
     423              :                              timestamp,
     424              :                              order_serial);
     425              :   }
     426              :   {
     427              :     MHD_RESULT ret;
     428              :     char *taler_refund_uri;
     429              : 
     430            6 :     taler_refund_uri = make_taler_refund_uri (connection,
     431            6 :                                               hc->instance->settings.id,
     432            6 :                                               hc->infix);
     433            6 :     ret = TALER_MHD_REPLY_JSON_PACK (
     434              :       connection,
     435              :       MHD_HTTP_OK,
     436              :       GNUNET_JSON_pack_string ("taler_refund_uri",
     437              :                                taler_refund_uri),
     438              :       GNUNET_JSON_pack_data_auto ("h_contract",
     439              :                                   &h_contract));
     440            6 :     GNUNET_free (taler_refund_uri);
     441            6 :     return ret;
     442              :   }
     443              : }
     444              : 
     445              : 
     446              : /* end of taler-merchant-httpd_private-post-orders-ID-refund.c */
        

Generated by: LCOV version 2.0-1