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 (15804B)


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