LCOV - code coverage report
Current view: top level - backend - taler-merchant-httpd_private-post-orders-ID-refund.c (source / functions) Hit Total Coverage
Test: GNU Taler merchant coverage report Lines: 93 124 75.0 %
Date: 2021-08-30 06:54:17 Functions: 3 3 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /*
       2             :   This file is part of TALER
       3             :   (C) 2014-2021 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             : 
      30             : 
      31             : /**
      32             :  * How often do we retry the non-trivial refund INSERT database
      33             :  * transaction?
      34             :  */
      35             : #define MAX_RETRIES 5
      36             : 
      37             : 
      38             : /**
      39             :  * Use database to notify other clients about the
      40             :  * @a order_id being refunded
      41             :  *
      42             :  * @param hc handler context we operate in
      43             :  * @param amount the (total) refunded amount
      44             :  */
      45             : static void
      46           4 : trigger_refund_notification (struct TMH_HandlerContext *hc,
      47             :                              const struct TALER_Amount *amount)
      48             : {
      49             :   const char *as;
      50           4 :   struct TMH_OrderRefundEventP refund_eh = {
      51           4 :     .header.size = htons (sizeof (refund_eh)),
      52           4 :     .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND),
      53           4 :     .merchant_pub = hc->instance->merchant_pub
      54             :   };
      55             : 
      56             :   /* Resume clients that may wait for this refund */
      57           4 :   as = TALER_amount2s (amount);
      58           4 :   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
      59             :               "Awakening clients on %s waiting for refund of no more than %s\n",
      60             :               hc->infix,
      61             :               as);
      62           4 :   GNUNET_CRYPTO_hash (hc->infix,
      63           4 :                       strlen (hc->infix),
      64             :                       &refund_eh.h_order_id);
      65           4 :   TMH_db->event_notify (TMH_db->cls,
      66             :                         &refund_eh.header,
      67             :                         as,
      68             :                         strlen (as));
      69           4 : }
      70             : 
      71             : 
      72             : /**
      73             :  * Make a taler://refund URI
      74             :  *
      75             :  * @param connection MHD connection to take host and path from
      76             :  * @param instance_id merchant's instance ID, must not be NULL
      77             :  * @param order_id order ID to show a refund for, must not be NULL
      78             :  * @returns the URI, must be freed with #GNUNET_free
      79             :  */
      80             : static char *
      81           4 : make_taler_refund_uri (struct MHD_Connection *connection,
      82             :                        const char *instance_id,
      83             :                        const char *order_id)
      84             : {
      85             :   const char *host;
      86             :   const char *forwarded_host;
      87             :   const char *uri_path;
      88           4 :   struct GNUNET_Buffer buf = { 0 };
      89             : 
      90           4 :   GNUNET_assert (NULL != instance_id);
      91           4 :   GNUNET_assert (NULL != order_id);
      92           4 :   host = MHD_lookup_connection_value (connection,
      93             :                                       MHD_HEADER_KIND,
      94             :                                       "Host");
      95           4 :   forwarded_host = MHD_lookup_connection_value (connection,
      96             :                                                 MHD_HEADER_KIND,
      97             :                                                 "X-Forwarded-Host");
      98           4 :   uri_path = MHD_lookup_connection_value (connection,
      99             :                                           MHD_HEADER_KIND,
     100             :                                           "X-Forwarded-Prefix");
     101           4 :   if (NULL != forwarded_host)
     102           0 :     host = forwarded_host;
     103           4 :   if (NULL == host)
     104             :   {
     105             :     /* Should never happen, at least the host header should be defined */
     106           0 :     GNUNET_break (0);
     107           0 :     return NULL;
     108             :   }
     109           4 :   GNUNET_buffer_write_str (&buf, "taler");
     110           4 :   if (GNUNET_NO == TALER_mhd_is_https (connection))
     111           4 :     GNUNET_buffer_write_str (&buf, "+http");
     112           4 :   GNUNET_buffer_write_str (&buf, "://refund/");
     113           4 :   GNUNET_buffer_write_str (&buf, host);
     114           4 :   if (NULL != uri_path)
     115           0 :     GNUNET_buffer_write_path (&buf, uri_path);
     116           4 :   if (0 != strcmp ("default", instance_id))
     117             :   {
     118           0 :     GNUNET_buffer_write_path (&buf, "instances");
     119           0 :     GNUNET_buffer_write_path (&buf, instance_id);
     120             :   }
     121           4 :   GNUNET_buffer_write_path (&buf, order_id);
     122           4 :   GNUNET_buffer_write_path (&buf,
     123             :                             ""); /* Trailing slash */
     124           4 :   return GNUNET_buffer_reap_str (&buf);
     125             : }
     126             : 
     127             : 
     128             : /**
     129             :  * Handle request for increasing the refund associated with
     130             :  * a contract.
     131             :  *
     132             :  * @param rh context of the handler
     133             :  * @param connection the MHD connection to handle
     134             :  * @param[in,out] hc context with further information about the request
     135             :  * @return MHD result code
     136             :  */
     137             : MHD_RESULT
     138           6 : TMH_private_post_orders_ID_refund (const struct TMH_RequestHandler *rh,
     139             :                                    struct MHD_Connection *connection,
     140             :                                    struct TMH_HandlerContext *hc)
     141             : {
     142             :   struct TALER_Amount refund;
     143             :   const char *reason;
     144             :   struct GNUNET_JSON_Specification spec[] = {
     145           6 :     TALER_JSON_spec_amount ("refund",
     146             :                             TMH_currency,
     147             :                             &refund),
     148           6 :     GNUNET_JSON_spec_string ("reason",
     149             :                              &reason),
     150           6 :     GNUNET_JSON_spec_end ()
     151             :   };
     152             :   enum TALER_MERCHANTDB_RefundStatus rs;
     153             :   struct GNUNET_HashCode h_contract;
     154             : 
     155             :   {
     156             :     enum GNUNET_DB_QueryStatus qs;
     157             :     json_t *contract_terms;
     158             :     uint64_t order_serial;
     159             :     struct GNUNET_TIME_Absolute refund_deadline;
     160             :     struct GNUNET_TIME_Absolute timestamp;
     161             : 
     162           6 :     qs = TMH_db->lookup_contract_terms (TMH_db->cls,
     163           6 :                                         hc->instance->settings.id,
     164           6 :                                         hc->infix,
     165             :                                         &contract_terms,
     166             :                                         &order_serial,
     167             :                                         NULL);
     168           6 :     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
     169             :     {
     170             :       struct GNUNET_JSON_Specification spec[] = {
     171           5 :         TALER_JSON_spec_absolute_time ("refund_deadline",
     172             :                                        &refund_deadline),
     173           5 :         TALER_JSON_spec_absolute_time ("timestamp",
     174             :                                        &timestamp),
     175           5 :         GNUNET_JSON_spec_end ()
     176             :       };
     177             : 
     178           5 :       if (GNUNET_YES !=
     179           5 :           GNUNET_JSON_parse (contract_terms,
     180             :                              spec,
     181             :                              NULL, NULL))
     182             :       {
     183           0 :         GNUNET_break (0);
     184           0 :         GNUNET_JSON_parse_free (spec);
     185           0 :         json_decref (contract_terms);
     186           0 :         return TALER_MHD_reply_with_error (
     187             :           connection,
     188             :           MHD_HTTP_INTERNAL_SERVER_ERROR,
     189             :           TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
     190             :           "mandatory fields missing");
     191             :       }
     192           5 :       json_decref (contract_terms);
     193           5 :       if (timestamp.abs_value_us == refund_deadline.abs_value_us)
     194             :       {
     195             :         /* refund was never allowed, so we should refuse hard */
     196           0 :         return TALER_MHD_reply_with_error (
     197             :           connection,
     198             :           MHD_HTTP_FORBIDDEN,
     199             :           TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT,
     200             :           NULL);
     201             :       }
     202           5 :       if (GNUNET_TIME_absolute_is_past (refund_deadline))
     203             :       {
     204             :         /* it is too late for refunds */
     205             :         /* NOTE: We MAY still be lucky that the exchange did not yet
     206             :            wire the funds, so we will try to give the refund anyway */
     207             :       }
     208             :     }
     209             :     else
     210             :     {
     211           1 :       return TALER_MHD_reply_with_error (connection,
     212             :                                          MHD_HTTP_NOT_FOUND,
     213             :                                          TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
     214           1 :                                          hc->infix);
     215             :     }
     216             :   }
     217             : 
     218             :   {
     219             :     enum GNUNET_GenericReturnValue res;
     220             : 
     221           5 :     res = TALER_MHD_parse_json_data (connection,
     222           5 :                                      hc->request_body,
     223             :                                      spec);
     224           5 :     if (GNUNET_OK != res)
     225             :       return (GNUNET_NO == res)
     226             :              ? MHD_YES
     227           0 :              : MHD_NO;
     228             :   }
     229             : 
     230           5 :   TMH_db->preflight (TMH_db->cls);
     231           5 :   for (unsigned int i = 0; i<MAX_RETRIES; i++)
     232             :   {
     233           5 :     if (GNUNET_OK !=
     234           5 :         TMH_db->start (TMH_db->cls,
     235             :                        "increase refund"))
     236             :     {
     237           0 :       GNUNET_break (0);
     238           0 :       return TALER_MHD_reply_with_error (connection,
     239             :                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
     240             :                                          TALER_EC_GENERIC_DB_START_FAILED,
     241             :                                          NULL);
     242             :     }
     243           5 :     rs = TMH_db->increase_refund (TMH_db->cls,
     244           5 :                                   hc->instance->settings.id,
     245           5 :                                   hc->infix,
     246             :                                   &refund,
     247             :                                   reason);
     248           5 :     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
     249             :                 "increase refund returned %d\n",
     250             :                 rs);
     251           5 :     if (TALER_MERCHANTDB_RS_SUCCESS != rs)
     252           1 :       TMH_db->rollback (TMH_db->cls);
     253           5 :     if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs)
     254           0 :       continue;
     255           5 :     if (TALER_MERCHANTDB_RS_SUCCESS == rs)
     256             :     {
     257             :       enum GNUNET_DB_QueryStatus qs;
     258             : 
     259           4 :       qs = TMH_db->commit (TMH_db->cls);
     260           4 :       if (GNUNET_DB_STATUS_HARD_ERROR == qs)
     261             :       {
     262           0 :         GNUNET_break (0);
     263           0 :         rs = TALER_MERCHANTDB_RS_HARD_ERROR;
     264           0 :         break;
     265             :       }
     266           4 :       if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
     267           0 :         continue;
     268           4 :       trigger_refund_notification (hc,
     269             :                                    &refund);
     270             :     }
     271           5 :     break;
     272             :   } /* retries loop */
     273             : 
     274           5 :   switch (rs)
     275             :   {
     276           0 :   case TALER_MERCHANTDB_RS_TOO_HIGH:
     277           0 :     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
     278             :                 "Refusing refund amount %s that is larger than original payment\n",
     279             :                 TALER_amount2s (&refund));
     280           0 :     return TALER_MHD_reply_with_error (connection,
     281             :                                        MHD_HTTP_CONFLICT,
     282             :                                        TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT,
     283             :                                        "Amount above payment");
     284           0 :   case TALER_MERCHANTDB_RS_SOFT_ERROR:
     285             :   case TALER_MERCHANTDB_RS_HARD_ERROR:
     286           0 :     return TALER_MHD_reply_with_error (connection,
     287             :                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
     288             :                                        TALER_EC_GENERIC_DB_COMMIT_FAILED,
     289             :                                        NULL);
     290           1 :   case TALER_MERCHANTDB_RS_NO_SUCH_ORDER:
     291             :     {
     292             :       /* We know the order exists from the
     293             :          "lookup_contract_terms" at the beginning;
     294             :          so if we get 'no such order' here, it
     295             :          must be read as "no PAID order" */
     296           1 :       return TALER_MHD_reply_with_error (
     297             :         connection,
     298             :         MHD_HTTP_CONFLICT,
     299             :         TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID,
     300           1 :         hc->infix);
     301             :     }
     302           4 :   case TALER_MERCHANTDB_RS_SUCCESS:
     303             :     {
     304             :       enum GNUNET_DB_QueryStatus qs;
     305             :       json_t *contract_terms;
     306             :       uint64_t order_serial;
     307             : 
     308           4 :       qs = TMH_db->lookup_contract_terms (TMH_db->cls,
     309           4 :                                           hc->instance->settings.id,
     310           4 :                                           hc->infix,
     311             :                                           &contract_terms,
     312             :                                           &order_serial,
     313             :                                           NULL);
     314           4 :       if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
     315             :       {
     316           0 :         return TALER_MHD_reply_with_error (connection,
     317             :                                            MHD_HTTP_NOT_FOUND,
     318             :                                            TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
     319           0 :                                            hc->infix);
     320             :       }
     321           4 :       if (GNUNET_OK !=
     322           4 :           TALER_JSON_contract_hash (contract_terms,
     323             :                                     &h_contract))
     324             :       {
     325           0 :         GNUNET_break (0);
     326           0 :         json_decref (contract_terms);
     327           0 :         return TALER_MHD_reply_with_error (connection,
     328             :                                            MHD_HTTP_INTERNAL_SERVER_ERROR,
     329             :                                            TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
     330             :                                            "Could not hash contract terms");
     331             :       }
     332           4 :       json_decref (contract_terms);
     333             :     }
     334           4 :     break;
     335             :   }
     336             : 
     337           4 :   {
     338             :     struct GNUNET_TIME_Absolute timestamp;
     339             :     uint64_t order_serial;
     340             :     enum GNUNET_DB_QueryStatus qs;
     341             : 
     342           4 :     qs = TMH_db->lookup_order_summary (TMH_db->cls,
     343           4 :                                        hc->instance->settings.id,
     344           4 :                                        hc->infix,
     345             :                                        &timestamp,
     346             :                                        &order_serial);
     347           4 :     if (0 >= qs)
     348             :     {
     349           0 :       GNUNET_break (0);
     350           0 :       return TALER_MHD_reply_with_error (connection,
     351             :                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
     352             :                                          TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
     353             :                                          NULL);
     354             :     }
     355           4 :     TMH_notify_order_change (hc->instance,
     356             :                              TMH_OSF_PAID
     357             :                              | TMH_OSF_REFUNDED,
     358             :                              timestamp,
     359             :                              order_serial);
     360             :   }
     361             :   {
     362             :     MHD_RESULT ret;
     363             :     char *taler_refund_uri;
     364             : 
     365           4 :     taler_refund_uri = make_taler_refund_uri (connection,
     366           4 :                                               hc->instance->settings.id,
     367           4 :                                               hc->infix);
     368           4 :     ret = TALER_MHD_REPLY_JSON_PACK (
     369             :       connection,
     370             :       MHD_HTTP_OK,
     371             :       GNUNET_JSON_pack_string ("taler_refund_uri",
     372             :                                taler_refund_uri),
     373             :       GNUNET_JSON_pack_data_auto ("h_contract",
     374             :                                   &h_contract));
     375           4 :     GNUNET_free (taler_refund_uri);
     376           4 :     return ret;
     377             :   }
     378             : }
     379             : 
     380             : 
     381             : /* end of taler-merchant-httpd_private-post-orders-ID-refund.c */

Generated by: LCOV version 1.14