merchant

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

taler-merchant-httpd_post-private-orders-ORDER_ID-collect.c (14925B)


      1 /*
      2   This file is part of TALER
      3   (C) 2026 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-collect.c
     18  * @brief Handle request to collect a zero-Taler order without a wallet
     19  * @author Bohdan Potuzhnyi
     20  * @author Volodymyr Potuzhnyi
     21  */
     22 #include "platform.h"
     23 #include <jansson.h>
     24 #include <taler/taler_json_lib.h>
     25 #include <taler/taler_merchant_util.h>
     26 #include "taler-merchant-httpd_post-private-orders-ORDER_ID-collect.h"
     27 #include "taler-merchant-httpd_post-orders-ORDER_ID-claim.h"
     28 #include "taler-merchant-httpd_post-orders-ORDER_ID-pay.h"
     29 #include "merchant-database/get_contract_terms.h"
     30 #include "merchant-database/get_order.h"
     31 #include "merchant-database/preflight.h"
     32 
     33 
     34 /**
     35  * Outcome of checking whether an order may be collected.
     36  */
     37 enum CollectCheck
     38 {
     39 
     40   /**
     41    * The contract terms could not be parsed, or use a contract version
     42    * we do not know.  This is about the contract we stored ourselves,
     43    * so it indicates a problem on our side, not a bad request.
     44    */
     45   COLLECT_CHECK_INVALID = 0,
     46 
     47   /**
     48    * The order can be collected: nothing remains to be paid with
     49    * coins, so the payment logic will find an empty balance to settle.
     50    */
     51   COLLECT_CHECK_OK,
     52 
     53   /**
     54    * The order is not free on the Taler side, so completing it needs a
     55    * wallet and the merchant must not do it on the customer's behalf.
     56    * "Free" here means all of: the amount to be paid over Taler is
     57    * zero (the rest being covered by @e amount_external), the choice
     58    * consumes no token @e inputs, and it yields no token @e outputs.
     59    */
     60   COLLECT_CHECK_NOT_FREE,
     61 
     62   /**
     63    * The contract is v1 and thus offers several choices, but the
     64    * client did not say which one to collect.  We do not pick one on
     65    * the client's behalf, just as the backend does not pick one on the
     66    * wallet's behalf when paying normally.
     67    */
     68   COLLECT_CHECK_CHOICE_MISSING,
     69 
     70   /**
     71    * A choice was given that the contract does not offer: either an
     72    * index beyond the end of the v1 @e choices array, or any index at
     73    * all for a v0 contract, which has no choices to select from.
     74    */
     75   COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS
     76 
     77 };
     78 
     79 
     80 /**
     81  * How often do we retry the database transaction?
     82  */
     83 #define MAX_RETRIES 3
     84 
     85 
     86 /**
     87  * Derive the deterministic nonce this backend uses to claim
     88  * @a order_id itself. Using a deterministic nonce allows us to
     89  * distinguish orders we claimed via collect from orders claimed
     90  * by a customer wallet, and makes the collect operation
     91  * idempotent.
     92  *
     93  * @param hc handler context with the instance public key
     94  * @param order_id order the nonce is for
     95  * @param[out] nonce set to the derived nonce
     96  */
     97 static void
     98 derive_collect_nonce (const struct TMH_HandlerContext *hc,
     99                       const char *order_id,
    100                       struct GNUNET_CRYPTO_EddsaPublicKey *nonce)
    101 {
    102   GNUNET_assert (GNUNET_YES ==
    103                  GNUNET_CRYPTO_hkdf_gnunet (
    104                    nonce,
    105                    sizeof (*nonce),
    106                    order_id,
    107                    strlen (order_id),
    108                    &hc->instance->merchant_pub,
    109                    sizeof (hc->instance->merchant_pub)));
    110 }
    111 
    112 
    113 /**
    114  * Check that @a contract_terms describe an order the backend may
    115  * complete on its own: a genuinely free Taler payment.
    116  *
    117  * The payment logic we hand the order to assumes it is paid with
    118  * coins; handing it an order that actually costs something would make
    119  * it fail in a way that says nothing useful, so we filter those out
    120  * before claiming anything.
    121  *
    122  * @param contract_terms contract terms to check
    123  * @param choice_index choice selected by the client, -1 if none was given
    124  * @return #COLLECT_CHECK_OK if the order can be collected
    125  */
    126 static enum CollectCheck
    127 check_collectable (const json_t *contract_terms,
    128                    int16_t choice_index)
    129 {
    130   enum TALER_MERCHANT_ContractVersion version
    131     = TALER_MERCHANT_CONTRACT_VERSION_0;
    132   struct GNUNET_JSON_Specification spec[] = {
    133     GNUNET_JSON_spec_mark_optional (
    134       TALER_MERCHANT_spec_contract_version ("version",
    135                                             &version),
    136       NULL),
    137     GNUNET_JSON_spec_end ()
    138   };
    139 
    140   if (GNUNET_OK !=
    141       GNUNET_JSON_parse (contract_terms,
    142                          spec,
    143                          NULL,
    144                          NULL))
    145   {
    146     GNUNET_break (0);
    147     return COLLECT_CHECK_INVALID;
    148   }
    149   switch (version)
    150   {
    151   case TALER_MERCHANT_CONTRACT_VERSION_0:
    152     {
    153       struct TALER_Amount amount;
    154       struct GNUNET_JSON_Specification aspec[] = {
    155         TALER_JSON_spec_amount_any ("amount",
    156                                     &amount),
    157         GNUNET_JSON_spec_end ()
    158       };
    159 
    160       if (GNUNET_OK !=
    161           GNUNET_JSON_parse (contract_terms,
    162                              aspec,
    163                              NULL,
    164                              NULL))
    165       {
    166         GNUNET_break (0);
    167         return COLLECT_CHECK_INVALID;
    168       }
    169       if (0 <= choice_index)
    170       {
    171         /* v0 contracts have no choices to select from */
    172         GNUNET_break_op (0);
    173         return COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS;
    174       }
    175       if (! TALER_amount_is_zero (&amount))
    176         return COLLECT_CHECK_NOT_FREE;
    177       return COLLECT_CHECK_OK;
    178     }
    179   case TALER_MERCHANT_CONTRACT_VERSION_1:
    180     {
    181       const json_t *choice;
    182       struct TALER_Amount amount;
    183 
    184       if (0 > choice_index)
    185       {
    186         /* Which choice to complete is the client's decision, just as
    187            it is the wallet's decision when paying normally. */
    188         GNUNET_break_op (0);
    189         return COLLECT_CHECK_CHOICE_MISSING;
    190       }
    191       choice = json_array_get (json_object_get (contract_terms,
    192                                                 "choices"),
    193                                (size_t) choice_index);
    194       if (NULL == choice)
    195       {
    196         GNUNET_break_op (0);
    197         return COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS;
    198       }
    199       {
    200         struct GNUNET_JSON_Specification cspec[] = {
    201           TALER_JSON_spec_amount_any ("amount",
    202                                       &amount),
    203           GNUNET_JSON_spec_end ()
    204         };
    205 
    206         if (GNUNET_OK !=
    207             GNUNET_JSON_parse (choice,
    208                                cspec,
    209                                NULL,
    210                                NULL))
    211         {
    212           GNUNET_break (0);
    213           return COLLECT_CHECK_INVALID;
    214         }
    215       }
    216       if (! TALER_amount_is_zero (&amount))
    217         return COLLECT_CHECK_NOT_FREE;
    218       if (0 != json_array_size (json_object_get (choice,
    219                                                  "inputs")))
    220         return COLLECT_CHECK_NOT_FREE;
    221       if (0 != json_array_size (json_object_get (choice,
    222                                                  "outputs")))
    223         return COLLECT_CHECK_NOT_FREE;
    224       return COLLECT_CHECK_OK;
    225     }
    226   }
    227   GNUNET_break (0);
    228   return COLLECT_CHECK_INVALID;
    229 }
    230 
    231 
    232 enum MHD_Result
    233 TMH_private_post_orders_ID_collect (const struct TMH_RequestHandler *rh,
    234                                     struct MHD_Connection *connection,
    235                                     struct TMH_HandlerContext *hc)
    236 {
    237   const char *order_id = hc->infix;
    238   const char *session_id = NULL;
    239   int16_t choice_index = -1;
    240   struct GNUNET_CRYPTO_EddsaPublicKey nonce;
    241   struct TALER_ClaimTokenP order_ct = { 0 };
    242   json_t *contract_terms = NULL;
    243   enum GNUNET_DB_QueryStatus qs;
    244 
    245   if (NULL != hc->ctx)
    246   {
    247     /* We already handed this request over to the payment logic and
    248        were resumed; let it continue where it left off. */
    249     return TMH_post_orders_ID_pay (rh,
    250                                    connection,
    251                                    hc);
    252   }
    253   if (NULL != hc->request_body)
    254   {
    255     struct GNUNET_JSON_Specification spec[] = {
    256       GNUNET_JSON_spec_mark_optional (
    257         GNUNET_JSON_spec_string ("session_id",
    258                                  &session_id),
    259         NULL),
    260       GNUNET_JSON_spec_mark_optional (
    261         GNUNET_JSON_spec_int16 ("choice_index",
    262                                 &choice_index),
    263         NULL),
    264       GNUNET_JSON_spec_end ()
    265     };
    266     enum GNUNET_GenericReturnValue res;
    267 
    268     res = TALER_MHD_parse_json_data (connection,
    269                                      hc->request_body,
    270                                      spec);
    271     if (GNUNET_OK != res)
    272     {
    273       GNUNET_break_op (0);
    274       return (GNUNET_NO == res)
    275              ? MHD_YES
    276              : MHD_NO;
    277     }
    278   }
    279 
    280   /* Pre-filter: only genuinely free orders may be completed without a
    281      wallet, and the client has to say which choice to complete. */
    282   {
    283     json_t *order_terms = NULL;
    284     uint64_t order_serial;
    285 
    286     TALER_MERCHANTDB_preflight (TMH_db);
    287     qs = TALER_MERCHANTDB_get_contract_terms (TMH_db,
    288                                               hc->instance->settings.id,
    289                                               order_id,
    290                                               &order_terms,
    291                                               &order_serial,
    292                                               NULL);
    293     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    294     {
    295       struct TALER_MerchantPostDataHashP unused;
    296 
    297       /* Remember the order's own claim token: as the merchant we may
    298          present it ourselves when claiming the order below. */
    299       qs = TALER_MERCHANTDB_get_order (TMH_db,
    300                                        hc->instance->settings.id,
    301                                        order_id,
    302                                        &order_ct,
    303                                        &unused,
    304                                        &order_terms);
    305     }
    306     if (0 > qs)
    307       return TALER_MHD_reply_with_error (connection,
    308                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    309                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    310                                          "get order for collection");
    311     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    312       return TALER_MHD_reply_with_error (connection,
    313                                          MHD_HTTP_NOT_FOUND,
    314                                          TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
    315                                          order_id);
    316     {
    317       enum CollectCheck cc;
    318 
    319       cc = check_collectable (order_terms,
    320                               choice_index);
    321       json_decref (order_terms);
    322       switch (cc)
    323       {
    324       case COLLECT_CHECK_OK:
    325         break;
    326       case COLLECT_CHECK_NOT_FREE:
    327         return TALER_MHD_reply_with_error (
    328           connection,
    329           MHD_HTTP_CONFLICT,
    330           TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_NOT_FREE,
    331           order_id);
    332       case COLLECT_CHECK_CHOICE_MISSING:
    333         return TALER_MHD_reply_with_error (
    334           connection,
    335           MHD_HTTP_BAD_REQUEST,
    336           TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING,
    337           order_id);
    338       case COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS:
    339         return TALER_MHD_reply_with_error (
    340           connection,
    341           MHD_HTTP_BAD_REQUEST,
    342           TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS,
    343           order_id);
    344       case COLLECT_CHECK_INVALID:
    345         return TALER_MHD_reply_with_error (
    346           connection,
    347           MHD_HTTP_INTERNAL_SERVER_ERROR,
    348           TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
    349           order_id);
    350       }
    351     }
    352   }
    353 
    354   /* Claim the order for ourselves */
    355   derive_collect_nonce (hc,
    356                         order_id,
    357                         &nonce);
    358   for (unsigned int i=0; i<MAX_RETRIES; i++)
    359   {
    360     TALER_MERCHANTDB_preflight (TMH_db);
    361     qs = TMH_claim_order (hc,
    362                           order_id,
    363                           &nonce,
    364                           &order_ct,
    365                           &contract_terms);
    366     if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
    367       break;
    368   }
    369   switch (qs)
    370   {
    371   case GNUNET_DB_STATUS_HARD_ERROR:
    372     return TALER_MHD_reply_with_error (connection,
    373                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    374                                        TALER_EC_GENERIC_DB_COMMIT_FAILED,
    375                                        NULL);
    376   case GNUNET_DB_STATUS_SOFT_ERROR:
    377     return TALER_MHD_reply_with_error (connection,
    378                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    379                                        TALER_EC_GENERIC_DB_SOFT_FAILURE,
    380                                        NULL);
    381   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    382     if (NULL == contract_terms)
    383       return TALER_MHD_reply_with_error (connection,
    384                                          MHD_HTTP_NOT_FOUND,
    385                                          TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
    386                                          order_id);
    387     /* Claimed by a customer wallet: the wallet owns the order and has
    388        to execute the payment itself. */
    389     json_decref (contract_terms);
    390     return TALER_MHD_reply_with_error (
    391       connection,
    392       MHD_HTTP_CONFLICT,
    393       TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_ALREADY_CLAIMED,
    394       order_id);
    395   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    396     GNUNET_assert (NULL != contract_terms);
    397     json_decref (contract_terms);
    398     break;
    399   }
    400 
    401   /* Turn our request into the payment request a wallet would send for
    402      a free order and let the regular payment logic handle it, so that
    403      collecting an order behaves exactly like paying it. */
    404   {
    405     json_t *pay_request;
    406 
    407     pay_request = GNUNET_JSON_PACK (
    408       GNUNET_JSON_pack_array_steal ("coins",
    409                                     json_array ()),
    410       GNUNET_JSON_pack_allow_null (
    411         GNUNET_JSON_pack_string ("session_id",
    412                                  session_id)));
    413     GNUNET_assert (NULL != pay_request);
    414     if (0 <= choice_index)
    415       GNUNET_assert (0 ==
    416                      json_object_set_new (
    417                        pay_request,
    418                        "wallet_data",
    419                        GNUNET_JSON_PACK (
    420                          GNUNET_JSON_pack_int64 ("choice_index",
    421                                                  choice_index))));
    422     if (NULL != hc->request_body)
    423       json_decref (hc->request_body);
    424     hc->request_body = pay_request;
    425   }
    426   return TMH_post_orders_ID_pay (rh,
    427                                  connection,
    428                                  hc);
    429 }
    430 
    431 
    432 /* end of taler-merchant-httpd_post-private-orders-ORDER_ID-collect.c */