merchant

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

taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c (15273B)


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