LCOV - code coverage report
Current view: top level - backend - taler-merchant-httpd_post-private-orders-ORDER_ID-collect.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 82.1 % 117 96
Test Date: 2026-09-04 23:42:01 Functions: 100.0 % 3 3

            Line data    Source code
       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           10 : derive_collect_nonce (const struct TMH_HandlerContext *hc,
      99              :                       const char *order_id,
     100              :                       struct GNUNET_CRYPTO_EddsaPublicKey *nonce)
     101              : {
     102           10 :   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           10 : }
     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           18 : check_collectable (const json_t *contract_terms,
     128              :                    int16_t choice_index)
     129              : {
     130           18 :   enum TALER_MERCHANT_ContractVersion version
     131              :     = TALER_MERCHANT_CONTRACT_VERSION_0;
     132              :   struct GNUNET_JSON_Specification spec[] = {
     133           18 :     GNUNET_JSON_spec_mark_optional (
     134              :       TALER_MERCHANT_spec_contract_version ("version",
     135              :                                             &version),
     136              :       NULL),
     137           18 :     GNUNET_JSON_spec_end ()
     138              :   };
     139              : 
     140           18 :   if (GNUNET_OK !=
     141           18 :       GNUNET_JSON_parse (contract_terms,
     142              :                          spec,
     143              :                          NULL,
     144              :                          NULL))
     145              :   {
     146            0 :     GNUNET_break (0);
     147            0 :     return COLLECT_CHECK_INVALID;
     148              :   }
     149           18 :   switch (version)
     150              :   {
     151           10 :   case TALER_MERCHANT_CONTRACT_VERSION_0:
     152              :     {
     153              :       struct TALER_Amount amount;
     154              :       struct GNUNET_JSON_Specification aspec[] = {
     155           10 :         TALER_JSON_spec_amount_any ("amount",
     156              :                                     &amount),
     157           10 :         GNUNET_JSON_spec_end ()
     158              :       };
     159              : 
     160           10 :       if (GNUNET_OK !=
     161           10 :           GNUNET_JSON_parse (contract_terms,
     162              :                              aspec,
     163              :                              NULL,
     164              :                              NULL))
     165              :       {
     166            0 :         GNUNET_break (0);
     167            0 :         return COLLECT_CHECK_INVALID;
     168              :       }
     169           10 :       if (0 <= choice_index)
     170              :       {
     171              :         /* v0 contracts have no choices to select from */
     172            2 :         GNUNET_break_op (0);
     173            2 :         return COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS;
     174              :       }
     175            8 :       if (! TALER_amount_is_zero (&amount))
     176            2 :         return COLLECT_CHECK_NOT_FREE;
     177            6 :       return COLLECT_CHECK_OK;
     178              :     }
     179            8 :   case TALER_MERCHANT_CONTRACT_VERSION_1:
     180              :     {
     181              :       const json_t *choice;
     182              :       struct TALER_Amount amount;
     183              : 
     184            8 :       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            2 :         GNUNET_break_op (0);
     189            2 :         return COLLECT_CHECK_CHOICE_MISSING;
     190              :       }
     191            6 :       choice = json_array_get (json_object_get (contract_terms,
     192              :                                                 "choices"),
     193              :                                (size_t) choice_index);
     194            6 :       if (NULL == choice)
     195              :       {
     196            2 :         GNUNET_break_op (0);
     197            2 :         return COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS;
     198              :       }
     199              :       {
     200              :         struct GNUNET_JSON_Specification cspec[] = {
     201            4 :           TALER_JSON_spec_amount_any ("amount",
     202              :                                       &amount),
     203            4 :           GNUNET_JSON_spec_end ()
     204              :         };
     205              : 
     206            4 :         if (GNUNET_OK !=
     207            4 :             GNUNET_JSON_parse (choice,
     208              :                                cspec,
     209              :                                NULL,
     210              :                                NULL))
     211              :         {
     212            0 :           GNUNET_break (0);
     213            0 :           return COLLECT_CHECK_INVALID;
     214              :         }
     215              :       }
     216            4 :       if (! TALER_amount_is_zero (&amount))
     217            0 :         return COLLECT_CHECK_NOT_FREE;
     218            4 :       if (0 != json_array_size (json_object_get (choice,
     219              :                                                  "inputs")))
     220            0 :         return COLLECT_CHECK_NOT_FREE;
     221            4 :       if (0 != json_array_size (json_object_get (choice,
     222              :                                                  "outputs")))
     223            0 :         return COLLECT_CHECK_NOT_FREE;
     224            4 :       return COLLECT_CHECK_OK;
     225              :     }
     226              :   }
     227            0 :   GNUNET_break (0);
     228            0 :   return COLLECT_CHECK_INVALID;
     229              : }
     230              : 
     231              : 
     232              : enum MHD_Result
     233           20 : TMH_private_post_orders_ID_collect (const struct TMH_RequestHandler *rh,
     234              :                                     struct MHD_Connection *connection,
     235              :                                     struct TMH_HandlerContext *hc)
     236              : {
     237           20 :   const char *order_id = hc->infix;
     238           20 :   const char *session_id = NULL;
     239           20 :   int16_t choice_index = -1;
     240              :   struct GNUNET_CRYPTO_EddsaPublicKey nonce;
     241           20 :   struct TALER_ClaimTokenP order_ct = { 0 };
     242           20 :   json_t *contract_terms = NULL;
     243              :   enum GNUNET_DB_QueryStatus qs;
     244              : 
     245           20 :   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            0 :     return TMH_post_orders_ID_pay (rh,
     250              :                                    connection,
     251              :                                    hc);
     252              :   }
     253           20 :   if (NULL != hc->request_body)
     254              :   {
     255              :     struct GNUNET_JSON_Specification spec[] = {
     256           20 :       GNUNET_JSON_spec_mark_optional (
     257              :         GNUNET_JSON_spec_string ("session_id",
     258              :                                  &session_id),
     259              :         NULL),
     260           20 :       GNUNET_JSON_spec_mark_optional (
     261              :         GNUNET_JSON_spec_int16 ("choice_index",
     262              :                                 &choice_index),
     263              :         NULL),
     264           20 :       GNUNET_JSON_spec_end ()
     265              :     };
     266              :     enum GNUNET_GenericReturnValue res;
     267              : 
     268           20 :     res = TALER_MHD_parse_json_data (connection,
     269           20 :                                      hc->request_body,
     270              :                                      spec);
     271           20 :     if (GNUNET_OK != res)
     272              :     {
     273            0 :       GNUNET_break_op (0);
     274              :       return (GNUNET_NO == res)
     275              :              ? MHD_YES
     276            0 :              : 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           20 :     json_t *order_terms = NULL;
     284              :     uint64_t order_serial;
     285              : 
     286           20 :     TALER_MERCHANTDB_preflight (TMH_db);
     287           20 :     qs = TALER_MERCHANTDB_get_contract_terms (TMH_db,
     288           20 :                                               hc->instance->settings.id,
     289              :                                               order_id,
     290              :                                               &order_terms,
     291              :                                               &order_serial,
     292              :                                               NULL);
     293           20 :     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           14 :       qs = TALER_MERCHANTDB_get_order (TMH_db,
     300           14 :                                        hc->instance->settings.id,
     301              :                                        order_id,
     302              :                                        &order_ct,
     303              :                                        &unused,
     304              :                                        &order_terms);
     305              :     }
     306           20 :     if (0 > qs)
     307           10 :       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           20 :     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
     312            2 :       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           18 :       cc = check_collectable (order_terms,
     320              :                               choice_index);
     321           18 :       json_decref (order_terms);
     322           18 :       switch (cc)
     323              :       {
     324           10 :       case COLLECT_CHECK_OK:
     325           10 :         break;
     326            2 :       case COLLECT_CHECK_NOT_FREE:
     327            2 :         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            2 :       case COLLECT_CHECK_CHOICE_MISSING:
     333            2 :         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            4 :       case COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS:
     339            4 :         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            0 :       case COLLECT_CHECK_INVALID:
     345            0 :         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           10 :   derive_collect_nonce (hc,
     356              :                         order_id,
     357              :                         &nonce);
     358           10 :   for (unsigned int i=0; i<MAX_RETRIES; i++)
     359              :   {
     360           10 :     TALER_MERCHANTDB_preflight (TMH_db);
     361           10 :     qs = TMH_claim_order (hc,
     362              :                           order_id,
     363              :                           &nonce,
     364              :                           &order_ct,
     365              :                           &contract_terms);
     366           10 :     if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
     367           10 :       break;
     368              :   }
     369           10 :   switch (qs)
     370              :   {
     371            0 :   case GNUNET_DB_STATUS_HARD_ERROR:
     372            0 :     return TALER_MHD_reply_with_error (connection,
     373              :                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
     374              :                                        TALER_EC_GENERIC_DB_COMMIT_FAILED,
     375              :                                        NULL);
     376            0 :   case GNUNET_DB_STATUS_SOFT_ERROR:
     377            0 :     return TALER_MHD_reply_with_error (connection,
     378              :                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
     379              :                                        TALER_EC_GENERIC_DB_SOFT_FAILURE,
     380              :                                        NULL);
     381            2 :   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     382            2 :     if (NULL == contract_terms)
     383            0 :       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            2 :     json_decref (contract_terms);
     390            2 :     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            8 :   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     396            8 :     GNUNET_assert (NULL != contract_terms);
     397            8 :     json_decref (contract_terms);
     398            8 :     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            8 :     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            8 :     GNUNET_assert (NULL != pay_request);
     414            8 :     if (0 <= choice_index)
     415            4 :       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            8 :     if (NULL != hc->request_body)
     423            8 :       json_decref (hc->request_body);
     424            8 :     hc->request_body = pay_request;
     425              :   }
     426            8 :   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 */
        

Generated by: LCOV version 2.0-1