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


      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_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   struct TALER_Amount order_total;
    186   int16_t choice_index;
    187 
    188   {
    189     enum GNUNET_GenericReturnValue res;
    190 
    191     res = TALER_MHD_parse_json_data (connection,
    192                                      hc->request_body,
    193                                      spec);
    194     if (GNUNET_OK != res)
    195     {
    196       return (GNUNET_NO == res)
    197              ? MHD_YES
    198              : MHD_NO;
    199     }
    200   }
    201 
    202   {
    203     enum GNUNET_DB_QueryStatus qs;
    204     uint64_t order_serial;
    205     bool paid;
    206     bool wired;
    207     bool session_matches;
    208     struct GNUNET_TIME_Timestamp refund_deadline;
    209     struct GNUNET_TIME_Timestamp wire_deadline;
    210 
    211     qs = TALER_MERCHANTDB_get_contract_terms_status (TMH_db,
    212                                                      hc->instance->settings.id,
    213                                                      hc->infix,
    214                                                      NULL,
    215                                                      &contract_terms,
    216                                                      &order_serial,
    217                                                      &paid,
    218                                                      &wired,
    219                                                      &session_matches,
    220                                                      NULL,
    221                                                      &choice_index);
    222     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    223     {
    224       if (qs < 0)
    225       {
    226         GNUNET_break (0);
    227         return TALER_MHD_reply_with_error (
    228           connection,
    229           MHD_HTTP_INTERNAL_SERVER_ERROR,
    230           TALER_EC_GENERIC_DB_FETCH_FAILED,
    231           "get_contract_terms");
    232       }
    233       return TALER_MHD_reply_with_error (
    234         connection,
    235         MHD_HTTP_NOT_FOUND,
    236         TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
    237         hc->infix);
    238     }
    239     if (GNUNET_OK !=
    240         TALER_JSON_contract_hash (contract_terms,
    241                                   &h_contract))
    242     {
    243       GNUNET_break (0);
    244       json_decref (contract_terms);
    245       return TALER_MHD_reply_with_error (
    246         connection,
    247         MHD_HTTP_INTERNAL_SERVER_ERROR,
    248         TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
    249         "Could not hash contract terms");
    250     }
    251     {
    252       struct GNUNET_JSON_Specification cspec[] = {
    253         GNUNET_JSON_spec_timestamp ("refund_deadline",
    254                                     &refund_deadline),
    255         GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
    256                                     &wire_deadline),
    257         GNUNET_JSON_spec_timestamp ("timestamp",
    258                                     &timestamp),
    259         GNUNET_JSON_spec_end ()
    260       };
    261 
    262       if (GNUNET_YES !=
    263           GNUNET_JSON_parse (contract_terms,
    264                              cspec,
    265                              NULL, NULL))
    266       {
    267         GNUNET_break (0);
    268         json_decref (contract_terms);
    269         return TALER_MHD_reply_with_error (
    270           connection,
    271           MHD_HTTP_INTERNAL_SERVER_ERROR,
    272           TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
    273           "mandatory fields missing");
    274       }
    275       if (GNUNET_TIME_timestamp_cmp (timestamp,
    276                                      ==,
    277                                      refund_deadline))
    278       {
    279         /* refund was never allowed, so we should refuse hard */
    280         json_decref (contract_terms);
    281         return TALER_MHD_reply_with_error (
    282           connection,
    283           MHD_HTTP_FORBIDDEN,
    284           TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT,
    285           NULL);
    286       }
    287       if (GNUNET_TIME_absolute_is_past (refund_deadline.abs_time))
    288       {
    289         /* it is too late for refunds */
    290         /* NOTE: We MAY still be lucky that the exchange did not yet
    291            wire the funds, so we will try to give the refund anyway */
    292       }
    293       if (GNUNET_TIME_absolute_is_past (wire_deadline.abs_time))
    294       {
    295         /* it is *really* too late for refunds */
    296         return TALER_MHD_reply_with_error (
    297           connection,
    298           MHD_HTTP_GONE,
    299           TALER_EC_MERCHANT_PRIVATE_POST_REFUND_AFTER_WIRE_DEADLINE,
    300           NULL);
    301       }
    302     }
    303   }
    304 
    305   TALER_MERCHANTDB_preflight (TMH_db);
    306   for (unsigned int i = 0; i<MAX_RETRIES; i++)
    307   {
    308     struct TALER_Amount external_total = {0};
    309     bool external_mismatch = false;
    310 
    311     if (GNUNET_OK !=
    312         TALER_MERCHANTDB_start (TMH_db,
    313                                 "increase refund"))
    314     {
    315       GNUNET_break (0);
    316       json_decref (contract_terms);
    317       return TALER_MHD_reply_with_error (connection,
    318                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    319                                          TALER_EC_GENERIC_DB_START_FAILED,
    320                                          NULL);
    321     }
    322     {
    323       enum GNUNET_DB_QueryStatus eqs;
    324 
    325       eqs = TALER_MERCHANTDB_get_external_refunds_total (
    326         TMH_db,
    327         hc->instance->settings.id,
    328         hc->infix,
    329         &external_total,
    330         &external_mismatch);
    331       if (0 > eqs)
    332       {
    333         TALER_MERCHANTDB_rollback (TMH_db);
    334         if (GNUNET_DB_STATUS_SOFT_ERROR == eqs)
    335           continue;
    336         json_decref (contract_terms);
    337         return TALER_MHD_reply_with_error (connection,
    338                                            MHD_HTTP_INTERNAL_SERVER_ERROR,
    339                                            TALER_EC_GENERIC_DB_FETCH_FAILED,
    340                                            "select external refunds");
    341       }
    342     }
    343     if (external_mismatch)
    344     {
    345       GNUNET_break (0);
    346       TALER_MERCHANTDB_rollback (TMH_db);
    347       json_decref (contract_terms);
    348       return TALER_MHD_reply_with_error (
    349         connection,
    350         MHD_HTTP_INTERNAL_SERVER_ERROR,
    351         TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
    352         "external refund currencies do not match");
    353     }
    354     if (TALER_amount_is_valid (&external_total))
    355     {
    356       struct TALER_Amount remaining;
    357 
    358       /* External refunds only exist for paid orders, so at this point a
    359          choice was selected and the order total is well-defined. */
    360       if (GNUNET_OK !=
    361           TMH_compute_order_total (contract_terms,
    362                                    choice_index,
    363                                    &order_total))
    364       {
    365         TALER_MERCHANTDB_rollback (TMH_db);
    366         json_decref (contract_terms);
    367         return TALER_MHD_reply_with_error (
    368           connection,
    369           MHD_HTTP_INTERNAL_SERVER_ERROR,
    370           TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
    371           "amount");
    372       }
    373       if (0 >
    374           TALER_amount_subtract (&remaining,
    375                                  &order_total,
    376                                  &external_total))
    377       {
    378         GNUNET_break (0);
    379         TALER_MERCHANTDB_rollback (TMH_db);
    380         json_decref (contract_terms);
    381         return TALER_MHD_reply_with_error (
    382           connection,
    383           MHD_HTTP_INTERNAL_SERVER_ERROR,
    384           TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
    385           "external refunds exceed order total");
    386       }
    387       if (1 ==
    388           TALER_amount_cmp (&refund,
    389                             &remaining))
    390       {
    391         TALER_MERCHANTDB_rollback (TMH_db);
    392         GNUNET_log (
    393           GNUNET_ERROR_TYPE_WARNING,
    394           "Refusing Taler refund of %s that would exceed remaining refundable amount of %s\n",
    395           TALER_amount2s (&refund),
    396           TALER_amount2s (&remaining));
    397         json_decref (contract_terms);
    398         return TALER_MHD_reply_with_error (
    399           connection,
    400           MHD_HTTP_CONFLICT,
    401           TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT,
    402           "Taler and external refunds would exceed the order total");
    403       }
    404     }
    405     rs = TALER_MERCHANTDB_do_increase_refund (TMH_db,
    406                                               hc->instance->settings.id,
    407                                               hc->infix,
    408                                               &refund,
    409                                               &get_refund_limit,
    410                                               NULL,
    411                                               reason);
    412     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    413                 "increase refund returned %d\n",
    414                 rs);
    415     if (TALER_MERCHANTDB_RS_SUCCESS != rs)
    416       TALER_MERCHANTDB_rollback (TMH_db);
    417     if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs)
    418       continue;
    419     if (TALER_MERCHANTDB_RS_SUCCESS == rs)
    420     {
    421       enum GNUNET_DB_QueryStatus qs;
    422       json_t *rargs;
    423 
    424       rargs = GNUNET_JSON_PACK (
    425         GNUNET_JSON_pack_timestamp ("timestamp",
    426                                     timestamp),
    427         GNUNET_JSON_pack_string ("order_id",
    428                                  hc->infix),
    429         GNUNET_JSON_pack_object_incref ("contract_terms",
    430                                         contract_terms),
    431         TALER_JSON_pack_amount ("refund_amount",
    432                                 &refund),
    433         GNUNET_JSON_pack_string ("reason",
    434                                  reason)
    435         );
    436       GNUNET_assert (NULL != rargs);
    437       qs = TMH_trigger_webhook (
    438         hc->instance->settings.id,
    439         "refund",
    440         rargs);
    441       json_decref (rargs);
    442       switch (qs)
    443       {
    444       case GNUNET_DB_STATUS_HARD_ERROR:
    445         GNUNET_break (0);
    446         TALER_MERCHANTDB_rollback (TMH_db);
    447         rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    448         break;
    449       case GNUNET_DB_STATUS_SOFT_ERROR:
    450         TALER_MERCHANTDB_rollback (TMH_db);
    451         continue;
    452       case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    453       case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    454         qs = TALER_MERCHANTDB_commit (TMH_db);
    455         break;
    456       }
    457       if (GNUNET_DB_STATUS_HARD_ERROR == qs)
    458       {
    459         GNUNET_break (0);
    460         rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    461         break;
    462       }
    463       if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
    464         continue;
    465       trigger_refund_notification (hc,
    466                                    &refund);
    467     }
    468     break;
    469   } /* retries loop */
    470   json_decref (contract_terms);
    471 
    472   switch (rs)
    473   {
    474   case TALER_MERCHANTDB_RS_LEGAL_FAILURE:
    475     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    476                 "Refund amount %s exceeded legal limits of the exchanges involved\n",
    477                 TALER_amount2s (&refund));
    478     return TALER_MHD_reply_with_error (
    479       connection,
    480       MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
    481       TALER_EC_MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION,
    482       NULL);
    483   case TALER_MERCHANTDB_RS_BAD_CURRENCY:
    484     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    485                 "Refund amount %s is not in the currency of the original payment\n",
    486                 TALER_amount2s (&refund));
    487     return TALER_MHD_reply_with_error (
    488       connection,
    489       MHD_HTTP_CONFLICT,
    490       TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH,
    491       "Order was paid in a different currency");
    492   case TALER_MERCHANTDB_RS_TOO_HIGH:
    493     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    494                 "Refusing refund amount %s that is larger than original payment\n",
    495                 TALER_amount2s (&refund));
    496     return TALER_MHD_reply_with_error (
    497       connection,
    498       MHD_HTTP_CONFLICT,
    499       TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT,
    500       "Amount above payment");
    501   case TALER_MERCHANTDB_RS_SOFT_ERROR:
    502   case TALER_MERCHANTDB_RS_HARD_ERROR:
    503     return TALER_MHD_reply_with_error (
    504       connection,
    505       MHD_HTTP_INTERNAL_SERVER_ERROR,
    506       TALER_EC_GENERIC_DB_COMMIT_FAILED,
    507       NULL);
    508   case TALER_MERCHANTDB_RS_NO_SUCH_ORDER:
    509     /* We know the order exists from the
    510        "get_contract_terms" at the beginning;
    511        so if we get 'no such order' here, it
    512        must be read as "no PAID order" */
    513     return TALER_MHD_reply_with_error (
    514       connection,
    515       MHD_HTTP_CONFLICT,
    516       TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID,
    517       hc->infix);
    518   case TALER_MERCHANTDB_RS_SUCCESS:
    519     /* continued below */
    520     break;
    521   } /* end switch */
    522 
    523   {
    524     uint64_t order_serial;
    525     enum GNUNET_DB_QueryStatus qs;
    526 
    527     qs = TALER_MERCHANTDB_get_order_summary (TMH_db,
    528                                              hc->instance->settings.id,
    529                                              hc->infix,
    530                                              &timestamp,
    531                                              &order_serial);
    532     if (0 >= qs)
    533     {
    534       GNUNET_break (0);
    535       return TALER_MHD_reply_with_error (
    536         connection,
    537         MHD_HTTP_INTERNAL_SERVER_ERROR,
    538         TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
    539         NULL);
    540     }
    541     TMH_notify_order_change (hc->instance,
    542                              TMH_OSF_CLAIMED
    543                              | TMH_OSF_PAID
    544                              | TMH_OSF_REFUNDED,
    545                              timestamp,
    546                              order_serial);
    547   }
    548   {
    549     enum MHD_Result ret;
    550     char *taler_refund_uri;
    551 
    552     taler_refund_uri = make_taler_refund_uri (connection,
    553                                               hc->instance->settings.id,
    554                                               hc->infix);
    555     ret = TALER_MHD_REPLY_JSON_PACK (
    556       connection,
    557       MHD_HTTP_OK,
    558       GNUNET_JSON_pack_string ("taler_refund_uri",
    559                                taler_refund_uri),
    560       GNUNET_JSON_pack_data_auto ("h_contract",
    561                                   &h_contract));
    562     GNUNET_free (taler_refund_uri);
    563     return ret;
    564   }
    565 }
    566 
    567 
    568 /* end of taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c */