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 */