merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

taler-merchant-httpd_private-post-orders-ID-refund.c (14523B)


      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 trigger_refund_notification (
     49   struct TMH_HandlerContext *hc,
     50   const struct TALER_Amount *amount)
     51 {
     52   const char *as;
     53   struct TMH_OrderRefundEventP refund_eh = {
     54     .header.size = htons (sizeof (refund_eh)),
     55     .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND),
     56     .merchant_pub = hc->instance->merchant_pub
     57   };
     58 
     59   /* Resume clients that may wait for this refund */
     60   as = TALER_amount2s (amount);
     61   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   GNUNET_CRYPTO_hash (hc->infix,
     66                       strlen (hc->infix),
     67                       &refund_eh.h_order_id);
     68   TMH_db->event_notify (TMH_db->cls,
     69                         &refund_eh.header,
     70                         as,
     71                         strlen (as));
     72 }
     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 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   GNUNET_assert (NULL != instance_id);
     91   GNUNET_assert (NULL != order_id);
     92   if (GNUNET_OK !=
     93       TMH_taler_uri_by_connection (connection,
     94                                    "refund",
     95                                    instance_id,
     96                                    &buf))
     97   {
     98     GNUNET_break (0);
     99     return NULL;
    100   }
    101   GNUNET_buffer_write_path (&buf,
    102                             order_id);
    103   GNUNET_buffer_write_path (&buf,
    104                             ""); /* Trailing slash */
    105   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 get_refund_limit (void *cls,
    121                   const char *exchange_url,
    122                   struct TALER_Amount *amount)
    123 {
    124   (void) cls;
    125   TMH_EXCHANGES_get_limit (exchange_url,
    126                            TALER_KYCLOGIC_KYC_TRIGGER_REFUND,
    127                            amount);
    128 }
    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 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     TALER_JSON_spec_amount_any ("refund",
    150                                 &refund),
    151     GNUNET_JSON_spec_string ("reason",
    152                              &reason),
    153     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     res = TALER_MHD_parse_json_data (connection,
    164                                      hc->request_body,
    165                                      spec);
    166     if (GNUNET_OK != res)
    167     {
    168       return (GNUNET_NO == res)
    169              ? MHD_YES
    170              : 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     qs = TMH_db->lookup_contract_terms (TMH_db->cls,
    181                                         hc->instance->settings.id,
    182                                         hc->infix,
    183                                         &contract_terms,
    184                                         &order_serial,
    185                                         NULL);
    186     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    187     {
    188       if (qs < 0)
    189       {
    190         GNUNET_break (0);
    191         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       return TALER_MHD_reply_with_error (
    198         connection,
    199         MHD_HTTP_NOT_FOUND,
    200         TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
    201         hc->infix);
    202     }
    203     if (GNUNET_OK !=
    204         TALER_JSON_contract_hash (contract_terms,
    205                                   &h_contract))
    206     {
    207       GNUNET_break (0);
    208       json_decref (contract_terms);
    209       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         GNUNET_JSON_spec_timestamp ("refund_deadline",
    218                                     &refund_deadline),
    219         GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
    220                                     &wire_deadline),
    221         GNUNET_JSON_spec_timestamp ("timestamp",
    222                                     &timestamp),
    223         GNUNET_JSON_spec_end ()
    224       };
    225 
    226       if (GNUNET_YES !=
    227           GNUNET_JSON_parse (contract_terms,
    228                              cspec,
    229                              NULL, NULL))
    230       {
    231         GNUNET_break (0);
    232         json_decref (contract_terms);
    233         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       if (GNUNET_TIME_timestamp_cmp (timestamp,
    240                                      ==,
    241                                      refund_deadline))
    242       {
    243         /* refund was never allowed, so we should refuse hard */
    244         json_decref (contract_terms);
    245         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       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       if (GNUNET_TIME_absolute_is_past (wire_deadline.abs_time))
    258       {
    259         /* it is *really* too late for refunds */
    260         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   TMH_db->preflight (TMH_db->cls);
    270   for (unsigned int i = 0; i<MAX_RETRIES; i++)
    271   {
    272     if (GNUNET_OK !=
    273         TMH_db->start (TMH_db->cls,
    274                        "increase refund"))
    275     {
    276       GNUNET_break (0);
    277       json_decref (contract_terms);
    278       return TALER_MHD_reply_with_error (connection,
    279                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    280                                          TALER_EC_GENERIC_DB_START_FAILED,
    281                                          NULL);
    282     }
    283     rs = TMH_db->increase_refund (TMH_db->cls,
    284                                   hc->instance->settings.id,
    285                                   hc->infix,
    286                                   &refund,
    287                                   &get_refund_limit,
    288                                   NULL,
    289                                   reason);
    290     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    291                 "increase refund returned %d\n",
    292                 rs);
    293     if (TALER_MERCHANTDB_RS_SUCCESS != rs)
    294       TMH_db->rollback (TMH_db->cls);
    295     if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs)
    296       continue;
    297     if (TALER_MERCHANTDB_RS_SUCCESS == rs)
    298     {
    299       enum GNUNET_DB_QueryStatus qs;
    300       json_t *rargs;
    301 
    302       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       GNUNET_assert (NULL != rargs);
    315       qs = TMH_trigger_webhook (
    316         hc->instance->settings.id,
    317         "refund",
    318         rargs);
    319       json_decref (rargs);
    320       switch (qs)
    321       {
    322       case GNUNET_DB_STATUS_HARD_ERROR:
    323         GNUNET_break (0);
    324         TMH_db->rollback (TMH_db->cls);
    325         rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    326         break;
    327       case GNUNET_DB_STATUS_SOFT_ERROR:
    328         TMH_db->rollback (TMH_db->cls);
    329         continue;
    330       case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    331       case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    332         qs = TMH_db->commit (TMH_db->cls);
    333         break;
    334       }
    335       if (GNUNET_DB_STATUS_HARD_ERROR == qs)
    336       {
    337         GNUNET_break (0);
    338         rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    339         break;
    340       }
    341       if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
    342         continue;
    343       trigger_refund_notification (hc,
    344                                    &refund);
    345     }
    346     break;
    347   } /* retries loop */
    348   json_decref (contract_terms);
    349 
    350   switch (rs)
    351   {
    352   case TALER_MERCHANTDB_RS_LEGAL_FAILURE:
    353     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    354                 "Refund amount %s exceeded legal limits of the exchanges involved\n",
    355                 TALER_amount2s (&refund));
    356     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   case TALER_MERCHANTDB_RS_BAD_CURRENCY:
    362     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     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   case TALER_MERCHANTDB_RS_TOO_HIGH:
    371     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    372                 "Refusing refund amount %s that is larger than original payment\n",
    373                 TALER_amount2s (&refund));
    374     return TALER_MHD_reply_with_error (
    375       connection,
    376       MHD_HTTP_CONFLICT,
    377       TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT,
    378       "Amount above payment");
    379   case TALER_MERCHANTDB_RS_SOFT_ERROR:
    380   case TALER_MERCHANTDB_RS_HARD_ERROR:
    381     return TALER_MHD_reply_with_error (
    382       connection,
    383       MHD_HTTP_INTERNAL_SERVER_ERROR,
    384       TALER_EC_GENERIC_DB_COMMIT_FAILED,
    385       NULL);
    386   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     return TALER_MHD_reply_with_error (
    392       connection,
    393       MHD_HTTP_CONFLICT,
    394       TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID,
    395       hc->infix);
    396   case TALER_MERCHANTDB_RS_SUCCESS:
    397     /* continued below */
    398     break;
    399   } /* end switch */
    400 
    401   {
    402     uint64_t order_serial;
    403     enum GNUNET_DB_QueryStatus qs;
    404 
    405     qs = TMH_db->lookup_order_summary (TMH_db->cls,
    406                                        hc->instance->settings.id,
    407                                        hc->infix,
    408                                        &timestamp,
    409                                        &order_serial);
    410     if (0 >= qs)
    411     {
    412       GNUNET_break (0);
    413       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     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     taler_refund_uri = make_taler_refund_uri (connection,
    431                                               hc->instance->settings.id,
    432                                               hc->infix);
    433     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     GNUNET_free (taler_refund_uri);
    441     return ret;
    442   }
    443 }
    444 
    445 
    446 /* end of taler-merchant-httpd_private-post-orders-ID-refund.c */