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


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