diff options
51 files changed, 3479 insertions, 224 deletions
@@ -84,3 +84,5 @@ doc/mdate-sh doc/texinfo.tex .private-key src/merchant-tools/taler-merchant-passwd +/.cache +/compile_commands.json @@ -1,3 +1,9 @@ +Fri Apr 12 10:43:41 AM CEST 2024 + Releasing taler-merchant 0.10.2. -CG + +Wed Nov 29 09:06:49 AM JST 2023 + Creating bugfix release taler-merchant 0.9.3a. -CG + Wed Nov 29 09:06:49 AM JST 2023 Creating bugfix release taler-merchant 0.9.3a. -CG @@ -6,61 +12,61 @@ Sat 28 Aug 2021 05:27:02 PM CEST payment/refund (#6995). -CG/BP Used database-driven long-polling on all long-pollers for multi-process deployments. (#6956) -CG - Releasing taler-merchant 0.8.4. -CG + Releasing taler-merchant 0.8.4. -CG Tue 24 Aug 2021 03:08:53 PM CEST - Releasing taler-merchant 0.8.3. -CG + Releasing taler-merchant 0.8.3. -CG Tue 17 Aug 2021 03:08:53 PM CEST - Releasing taler-merchant 0.8.2. -CG + Releasing taler-merchant 0.8.2. -CG Thu 22 Apr 2021 11:30:13 PM CEST Integrate new single-page app. -CG/SM Tue 10 Nov 2020 01:08:46 PM CET - Major revamp of the entire API (now 1:0:0), changing the names of - many endpoints for better consistency. - Adding support for (optional) inventory management. - Releasing taler-merchant 0.8.0. -CG/JB + Major revamp of the entire API (now 1:0:0), changing the names of + many endpoints for better consistency. + Adding support for (optional) inventory management. + Releasing taler-merchant 0.8.0. -CG/JB Sun 12 Apr 2020 08:45:11 PM CEST - Changed /tip-pickup API to withdraw directly from the exchange - and return blind signatures, instead of having the wallet do it (#6173). -CG + Changed /tip-pickup API to withdraw directly from the exchange + and return blind signatures, instead of having the wallet do it (#6173). -CG Fri 10 Apr 2020 09:01:22 PM CEST - Changing refund API to have the merchant backend make the /refund - request to the exchange instead of having the wallet do it (#5299). -CG + Changing refund API to have the merchant backend make the /refund + request to the exchange instead of having the wallet do it (#5299). -CG Tue 31 Mar 2020 04:17:58 PM CEST - Releasing taler-merchant 0.7.0. -CG + Releasing taler-merchant 0.7.0. -CG Tue 24 Dec 2019 11:01:21 PM CET - Releasing taler-merchant 0.6.0. -CG + Releasing taler-merchant 0.6.0. -CG Sat 17 Aug 2019 10:03:38 PM CEST - Remove "currency" field from exchange database, as we only - support one currency per merchant backend anyway. -CG + Remove "currency" field from exchange database, as we only + support one currency per merchant backend anyway. -CG Wed Apr 4 00:19:38 CEST 2018 - Releasing taler-merchant-0.5.0 -FD + Releasing taler-merchant-0.5.0 -FD Mon Jan 22 21:54:42 CET 2018 - Address #5262. -CG + Address #5262. -CG Tue Jan 2 00:27:29 2018 - Implement #5158 (proper handling of aborted payments). -CG + Implement #5158 (proper handling of aborted payments). -CG Wed Dec 27 11:21:43 2017 - Complete logic to allow /pay to span coins from multiple exchanges. -CG + Complete logic to allow /pay to span coins from multiple exchanges. -CG Wed Dec 13 21:50:59 2017 - Use new wire transfer logic in payments generator. -CG + Use new wire transfer logic in payments generator. -CG Thu Dec 7 07:42:40 2017 - Implemented new tipping feature (now with private keys in files). -CG + Implemented new tipping feature (now with private keys in files). -CG Wed Oct 18 15:33:23 CEST 2017 - Releasing taler-merchant 0.4.0. -CG + Releasing taler-merchant 0.4.0. -CG Thu Jun 22 15:12:37 CEST 2017 Implementing /refund @@ -73,16 +79,16 @@ Tue Jun 6 14:30:43 CEST 2017 /history API now has the way to query from a starting base date ahead into the future. Memory leak fixes. -CG/MS - Releasing taler-merchant 0.3.0. -CG + Releasing taler-merchant 0.3.0. -CG Mon Mar 6 17:57:51 CET 2017 - Add support for wire fee calculations to /pay handling (#4935), - and adding setting "max_wire_fee" and "wire_fee_amortization" - in contract from defaults. -CG + Add support for wire fee calculations to /pay handling (#4935), + and adding setting "max_wire_fee" and "wire_fee_amortization" + in contract from defaults. -CG Mon Mar 6 00:59:25 CET 2017 - Implement BIND_TO option to allow binding backend to a particular - IP address (#4752). Enabling use of dual-stack by default. -CG + Implement BIND_TO option to allow binding backend to a particular + IP address (#4752). Enabling use of dual-stack by default. -CG Thu Dec 15 10:37:08 CET 2016 Implementing: @@ -91,8 +97,8 @@ Thu Dec 15 10:37:08 CET 2016 by looking for their hashcodes. Fri Nov 18 18:54:07 CET 2016 - Misc. minor updates to match API changes from exchange 0.2. - Releasing taler-merchant 0.2.0. -CG + Misc. minor updates to match API changes from exchange 0.2. + Releasing taler-merchant 0.2.0. -CG Mon Oct 10 16:27:57 CEST 2016 Implementing: @@ -103,8 +109,8 @@ Mon Oct 10 16:27:57 CEST 2016 same backend. -MS Tue Jun 7 15:17:45 CEST 2016 - Store signing key used by exchange in DB. Might be useful - in the future when we implement GC for the backenddb. -CG + Store signing key used by exchange in DB. Might be useful + in the future when we implement GC for the backenddb. -CG Wed Jun 1 17:27:36 CEST 2016 - Releasing taler-merchant-0.0.0. -CG + Releasing taler-merchant-0.0.0. -CG @@ -32,7 +32,7 @@ fi # Generate Makefile.am in contrib/ cd contrib rm -f Makefile.am -find wallet-core/backoffice/ -type f -printf ' %p \\\n' | sort > Makefile.am.ext +find wallet-core/backoffice/ -type f | sort | awk '{print " " $1 " \\" }' > Makefile.am.ext # Remove extra '\' at the end of the file truncate -s -2 Makefile.am.ext cat Makefile.am.in Makefile.am.ext >> Makefile.am diff --git a/configure.ac b/configure.ac index 8fd422fc..46eef3a7 100644 --- a/configure.ac +++ b/configure.ac @@ -18,7 +18,7 @@ # This configure file is in the public domain AC_PREREQ([2.69]) -AC_INIT([taler-merchant],[0.10.0],[taler-bug@gnunet.org]) +AC_INIT([taler-merchant],[0.10.2],[taler-bug@gnunet.org]) AC_CONFIG_SRCDIR([src/backend/taler-merchant-httpd.c]) AC_CONFIG_HEADERS([taler_merchant_config.h]) # support for non-recursive builds @@ -217,8 +217,18 @@ AS_IF([test $libgnunetutil != 1], *** https://gnunet.org *** ]])]) +libgnunetpq=0 AC_CHECK_HEADERS([gnunet/gnunet_pq_lib.h], - [AC_CHECK_LIB([gnunetpq], [GNUNET_PQ_connect_with_cfg], libgnunetpq=1)]) + [AC_CHECK_LIB([gnunetpq], [GNUNET_PQ_query_param_blind_sign_priv], libgnunetpq=1)]) + +AS_IF([test $libgnunetpq != 1], + [AC_MSG_ERROR([[ +*** +*** You need libgnunetpq >= 0.21.2 (API v7) to build this program. +*** This library is part of GNUnet, available at +*** https://gnunet.org +*** ]])]) + AM_CONDITIONAL(HAVE_GNUNETPQ, test x$libgnunetpq = x1) TALER_LIB_LDFLAGS="-export-dynamic -no-undefined" diff --git a/contrib/ci/jobs/0-codespell/job.sh b/contrib/ci/jobs/0-codespell/job.sh index 7b486bc7..28f43239 100755 --- a/contrib/ci/jobs/0-codespell/job.sh +++ b/contrib/ci/jobs/0-codespell/job.sh @@ -7,6 +7,7 @@ skip=$(cat <<EOF ABOUT-NLS */afl-tests/* **/auditor/*.sql +*/debian/tmp/** *.bbl *.bib *build-aux* diff --git a/contrib/ci/jobs/4-deb-package/version.sh b/contrib/ci/jobs/4-deb-package/version.sh index a6e740af..52031b23 100755 --- a/contrib/ci/jobs/4-deb-package/version.sh +++ b/contrib/ci/jobs/4-deb-package/version.sh @@ -7,7 +7,7 @@ if [ -z "${BRANCH}" ]; then else # "Unshallow" our checkout, but only our current branch, and exclude the submodules. git fetch --no-recurse-submodules --tags --depth=1000 origin "${BRANCH}" - RECENT_VERSION_TAG=$(git describe --tags --match 'v*.*.*' --always --abbrev=0 HEAD || exit 1) + RECENT_VERSION_TAG=$(git describe --tags --match 'v*.*.*' --exclude '*-dev*' --always --abbrev=0 HEAD || exit 1) commits="$(git rev-list ${RECENT_VERSION_TAG}..HEAD --count)" if [ "${commits}" = "0" ]; then git describe --tag HEAD | sed -r 's/^v//' || exit 1 diff --git a/contrib/wallet-core b/contrib/wallet-core -Subproject 35212ac57bfe5625fcf574b0fd2d9baf395dc4c +Subproject 240d647da85de6b575d15c37efec04757541e3d diff --git a/debian/changelog b/debian/changelog index 36344f8b..c04b84f3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,19 @@ +taler-merchant (0.10.2) unstable; urgency=low + + * Update various submodules to latest version. + + -- Christian Grothoff <grothoff@gnu.org> Fri, 12 Apr 2024 09:50:12 +0200 + +taler-merchant (0.10.1) unstable; urgency=low + + * Implement cache control headers for /config + * Do not return orders over amount of 0 as unpaid + * Handle refunds in wire transfer reconciliation + * Implement protocol v12 and v13 + * Simplify KYC logic in payment processing + + -- Christian Grothoff <grothoff@gnu.org> Tue, 9 Apr 2024 09:50:12 +0200 + taler-merchant (0.10.0) unstable; urgency=low * Implement public GET API for templates (#8608). diff --git a/debian/control b/debian/control index 9977ac39..6fa6e6c1 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,7 @@ Build-Depends: debhelper-compat (= 12), gettext, libgnunet-dev (>=0.21), - libtalerexchange-dev (>=0.9.4), + libtalerexchange-dev (>=0.10.2), libpq-dev (>=14.0), po-debconf, libqrencode-dev, @@ -48,7 +48,7 @@ Pre-Depends: ${misc:Pre-Depends} Depends: libtalermerchant (= ${binary:Version}), - libtalerexchange (>= 0.9.4), + libtalerexchange (>= 0.10.2), adduser, lsb-base, netbase, @@ -69,7 +69,7 @@ Package: libtalermerchant-dev Section: libdevel Architecture: any Depends: - libtalerexchange-dev (>= 0.9.4), + libtalerexchange-dev (>= 0.10.2), libgnunet-dev (>=0.21), ${misc:Depends}, ${shlibs:Depends} diff --git a/doc/doxygen/taler.doxy b/doc/doxygen/taler.doxy index d9785d42..e5185031 100644 --- a/doc/doxygen/taler.doxy +++ b/doc/doxygen/taler.doxy @@ -5,7 +5,7 @@ #--------------------------------------------------------------------------- DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "GNU Taler: Merchant" -PROJECT_NUMBER = 0.9.3 +PROJECT_NUMBER = 0.10.2 PROJECT_LOGO = logo.svg OUTPUT_DIRECTORY = . CREATE_SUBDIRS = YES diff --git a/doc/prebuilt b/doc/prebuilt -Subproject af8c69dfe397ff4bed7abca98ed8f3b2ed70541 +Subproject b8d2d2fa2ed2a771880f451725176f256583cb2 diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am index d521608d..dbc7cde8 100644 --- a/src/backend/Makefile.am +++ b/src/backend/Makefile.am @@ -25,6 +25,7 @@ bin_PROGRAMS = \ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd.c taler-merchant-httpd.h \ taler-merchant-httpd_config.c taler-merchant-httpd_config.h \ + taler-merchant-httpd_contract.c taler-merchant-httpd_contract.h \ taler-merchant-httpd_exchanges.c taler-merchant-httpd_exchanges.h \ taler-merchant-httpd_get-orders-ID.c \ taler-merchant-httpd_get-orders-ID.h \ diff --git a/src/backend/taler-merchant-depositcheck.c b/src/backend/taler-merchant-depositcheck.c index af2fa267..9245e1fb 100644 --- a/src/backend/taler-merchant-depositcheck.c +++ b/src/backend/taler-merchant-depositcheck.c @@ -391,9 +391,9 @@ run_at (struct GNUNET_TIME_Absolute deadline) GNUNET_TIME_absolute2s (deadline)); return; /* too early */ } + next_deadline = deadline; if (NULL != task) GNUNET_SCHEDULER_cancel (task); - next_deadline = deadline; task = GNUNET_SCHEDULER_add_at (deadline, &select_work, NULL); @@ -567,8 +567,12 @@ deposit_get_cb (void *cls, GNUNET_assert (NULL != keys); if ( (w_count < CONCURRENCY_LIMIT / 2) || (0 == w_count) ) + { + if (NULL != task) + GNUNET_SCHEDULER_cancel (task); task = GNUNET_SCHEDULER_add_now (&select_work, NULL); + } } @@ -774,6 +778,8 @@ keys_cb ( return; } keys = TALER_EXCHANGE_keys_incref (in_keys); + if (NULL != task) + GNUNET_SCHEDULER_cancel (task); task = GNUNET_SCHEDULER_add_now (&select_work, NULL); } diff --git a/src/backend/taler-merchant-exchange.c b/src/backend/taler-merchant-exchange.c index e3b0d6c4..7945cb50 100644 --- a/src/backend/taler-merchant-exchange.c +++ b/src/backend/taler-merchant-exchange.c @@ -238,6 +238,11 @@ static struct GNUNET_DB_EventHandler *eh; static unsigned int active_inquiries; /** + * Set to true if we ever encountered any problem. + */ +static bool found_problem; + +/** * Value to return from main(). 0 on success, non-zero on errors. */ static int global_ret; @@ -394,6 +399,8 @@ update_transaction_status (const struct Inquiry *w, { enum GNUNET_DB_QueryStatus qs; + if (failed) + found_problem = true; qs = db_plugin->update_transfer_status (db_plugin->cls, w->exchange->exchange_url, &w->wtid, @@ -476,6 +483,7 @@ end_inquiry (struct Inquiry *w) (at_limit) ) { at_limit = false; + GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&find_work, NULL); } @@ -539,6 +547,11 @@ shutdown_task (void *cls) db_plugin->event_listen_cancel (eh); eh = NULL; } + if (NULL != task) + { + GNUNET_SCHEDULER_cancel (task); + task = NULL; + } TALER_MERCHANTDB_plugin_unload (db_plugin); db_plugin = NULL; cfg = NULL; @@ -707,12 +720,32 @@ check_transfer (void *cls, GNUNET_break (0); return; /* already had a serious issue; odd that we're called more than once as well... */ } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checking coin with value %s\n", + TALER_amount2s (amount_with_fee)); if ( (GNUNET_OK != TALER_amount_cmp_currency (amount_with_fee, &ttd->coin_value)) || (0 != TALER_amount_cmp (amount_with_fee, - &ttd->coin_value)) || - (GNUNET_OK != + &ttd->coin_value)) ) + { + /* Disagreement between the exchange and us about how much this + coin is worth! */ + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Disagreement about coin value %s\n", + TALER_amount2s (amount_with_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Exchange gave it a value of %s\n", + TALER_amount2s (&ttd->coin_value)); + ctc->check_transfer_result = GNUNET_SYSERR; + /* Build the `TrackTransferConflictDetails` */ + ctc->ec = TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS; + ctc->failure = true; + /* FIXME: this should be reported to the auditor (once the auditor has an API for this) */ + return; + } + if ( (GNUNET_OK != TALER_amount_cmp_currency (deposit_fee, &ttd->coin_fee)) || (0 != TALER_amount_cmp (deposit_fee, @@ -721,6 +754,12 @@ check_transfer (void *cls, /* Disagreement between the exchange and us about how much this coin is worth! */ GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Expected fee is %s\n", + TALER_amount2s (&ttd->coin_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Fee claimed by exchange is %s\n", + TALER_amount2s (deposit_fee)); ctc->check_transfer_result = GNUNET_SYSERR; /* Build the `TrackTransferConflictDetails` */ ctc->ec = TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS; @@ -928,6 +967,12 @@ wire_transfer_cb (void *cls, &w->total)) ) { GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Wire transfer total value was %s\n", + TALER_amount2s (&w->total)); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Exchange claimed total value to be %s\n", + TALER_amount2s (&td->total_amount)); update_transaction_status (w, GNUNET_TIME_UNIT_FOREVER_ABS, TALER_EC_MERCHANT_EXCHANGE_TRANSFERS_CONFLICTING_TRANSFERS, @@ -1203,6 +1248,7 @@ run (void *cls, &transfer_added, NULL); } + GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&find_work, NULL); } @@ -1248,6 +1294,9 @@ main (int argc, return EXIT_INVALIDARGUMENT; if (GNUNET_NO == ret) return EXIT_SUCCESS; + if ( (found_problem) && + (0 == global_ret) ) + global_ret = 7; return global_ret; } diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c index c7dec0f9..d1340249 100644 --- a/src/backend/taler-merchant-httpd_config.c +++ b/src/backend/taler-merchant-httpd_config.c @@ -42,7 +42,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "13:0:9" +#define MERCHANT_PROTOCOL_VERSION "14:1:10" /** @@ -82,16 +82,36 @@ MH_handler_config (struct TMH_RequestHandler *rh, struct TMH_HandlerContext *hc) { static struct MHD_Response *response; + static struct GNUNET_TIME_Absolute a; (void) rh; (void) hc; + if ( (GNUNET_TIME_absolute_is_past (a)) && + (NULL != response) ) + { + MHD_destroy_response (response); + response = NULL; + } if (NULL == response) { json_t *specs = json_object (); json_t *exchanges = json_array (); + struct GNUNET_TIME_Timestamp km; + char dat[128]; GNUNET_assert (NULL != specs); GNUNET_assert (NULL != exchanges); + a = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_DAYS); + /* Round up to next full day to ensure the expiration + time does not become a fingerprint! */ + a = GNUNET_TIME_absolute_round_down (a, + GNUNET_TIME_UNIT_DAYS); + a = GNUNET_TIME_absolute_add (a, + GNUNET_TIME_UNIT_DAYS); + /* => /config response stays at most 48h in caches! */ + km = GNUNET_TIME_absolute_to_timestamp (a); + TALER_MHD_get_date_string (km.abs_time, + dat); TMH_exchange_get_trusted (&add_exchange, exchanges); for (unsigned int i = 0; i<TMH_num_cspecs; i++) @@ -102,7 +122,8 @@ MH_handler_config (struct TMH_RequestHandler *rh, GNUNET_assert (0 == json_object_set_new (specs, cspec->currency, - TALER_CONFIG_currency_specs_to_json ( + TALER_CONFIG_currency_specs_to_json + ( cspec))); } response = TALER_MHD_MAKE_JSON_PACK ( @@ -112,12 +133,21 @@ MH_handler_config (struct TMH_RequestHandler *rh, specs), GNUNET_JSON_pack_array_steal ("exchanges", exchanges), - GNUNET_JSON_pack_string ("implementation", - "urn:net:taler:specs:merchant:c-reference"), + GNUNET_JSON_pack_string ( + "implementation", + "urn:net:taler:specs:taler-merchant:c-reference"), GNUNET_JSON_pack_string ("name", "taler-merchant"), GNUNET_JSON_pack_string ("version", MERCHANT_PROTOCOL_VERSION)); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_EXPIRES, + dat)); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "public,max-age=21600")); /* 6h */ } return MHD_queue_response (connection, MHD_HTTP_OK, diff --git a/src/backend/taler-merchant-httpd_contract.c b/src/backend/taler-merchant-httpd_contract.c new file mode 100644 index 00000000..38c82e70 --- /dev/null +++ b/src/backend/taler-merchant-httpd_contract.c @@ -0,0 +1,47 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_contract.c + * @brief shared logic for contract terms handling + * @author Christian Blättler + */ +#include "platform.h" +#include <jansson.h> +#include "taler-merchant-httpd_contract.h" + +enum TALER_MerchantContractInputType +TMH_string_to_contract_input_type (const char *str) +{ + /* For now, only 'token' is the only supported option. */ + if (0 == strcmp("token", str)) + { + return TALER_MCIT_TOKEN; + } + + return TALER_MCIT_INVALID; +} + +enum TALER_MerchantContractOutputType +TMH_string_to_contract_output_type (const char *str) +{ + /* For now, only 'token' is the only supported option. */ + if (0 == strcmp("token", str)) + { + return TALER_MCOT_TOKEN; + } + + return TALER_MCOT_INVALID; +} diff --git a/src/backend/taler-merchant-httpd_contract.h b/src/backend/taler-merchant-httpd_contract.h new file mode 100644 index 00000000..b1e3938c --- /dev/null +++ b/src/backend/taler-merchant-httpd_contract.h @@ -0,0 +1,592 @@ +/* + This file is part of TALER + (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_contract.h + * @brief shared logic for contract terms handling + * @author Christian Blättler + */ +#include "taler-merchant-httpd.h" +#include <gnunet/gnunet_time_lib.h> +#include <jansson.h> + + +/** + * Possible versions of the contract terms. + */ +enum TALER_MerchantContractVersion +{ + + /** + * Version 0 + */ + TALER_MCV_V0 = 0, + + /** + * Version 1 + */ + TALER_MCV_V1 = 1 +}; + +/** + * Possible token kinds. + */ +enum TALER_MerchantContractTokenKind +{ + + /** + * Subscription token kind + */ + TALER_MCTK_SUBSCRIPTION = 0, + + /** + * Discount token kind + */ + TALER_MCTK_DISCOUNT = 1 +}; + +/** + * Possible input types for the contract terms. + */ +enum TALER_MerchantContractInputType +{ + + /** + * Input type invalid + */ + TALER_MCIT_INVALID = 0, + + /** + * Input type coin + */ + TALER_MCIT_COIN = 1, + + /** + * Input type token + */ + TALER_MCIT_TOKEN = 2 +}; + +/** + * Contract input (part of the v1 contract terms). + */ +struct TALER_MerchantContractInput +{ + /** + * Type of the input. + */ + enum TALER_MerchantContractInputType type; + + union + { + /** + * Coin-based input (ration). (Future work, only here for reference) + */ + // struct + // { + // /** + // * Price to be paid. + // */ + // struct TALER_Amount price; + + // /** + // * Base URL of the ration authority. + // */ + // const char *ration_authority_url; + // } coin; + + /** + * Token-based input. + */ + struct + { + /** + * Slug of the token family to be used. + */ + const char *token_family_slug; + + /** + * Start time of the validity period of the token. Base on this timestamp + * the wallet can find the correct key for this token in token_authorities. + */ + struct GNUNET_TIME_Timestamp valid_after; + + /** + * Number of tokens of this type required. Defaults to one if the + * field is not provided. + */ + unsigned int count; + } token; + } details; +}; + +/** + * Possible output types for the contract terms. + */ +enum TALER_MerchantContractOutputType +{ + + /** + * Invalid output type + */ + TALER_MCOT_INVALID = 0, + + /** + * Output type coin + */ + TALER_MCOT_COIN = 1, + + /** + * Output type token + */ + TALER_MCOT_TOKEN = 2, + + /** + * Output type tax-receipt + */ + TALER_MCOT_TAX_RECEIPT = 3 + +}; + +/** + * Contract output (part of the v1 contract terms). + */ +struct TALER_MerchantContractOutput +{ + /** + * Type of the output. + */ + enum TALER_MerchantContractOutputType type; + + union + { + /** + * Coin-based output. + */ + struct { + /** + * Coins that will be yielded. This excludes any applicable withdraw fees. + */ + struct TALER_Amount brutto_yield; + + /** + * Base URL of the exchange that will issue the coins. + */ + const char *exchange_url; + } coin; + + /** + * Tax-receipt output. + */ + struct + { + /** + * Base URL of the donation authority that will issue the tax receipt. + */ + const char *donau_url; + } tax_receipt; + + /** + * Token-based output. + */ + struct + { + /** + * Slug of the token family to be issued. + */ + const char *token_family_slug; + + /** + * Start time of the validity period of the token. Base on this timestamp + * the wallet can find the correct key for this token in token_authorities. + */ + struct GNUNET_TIME_Timestamp valid_after; + + /** + * Number of tokens of this type required. Defaults to one if the + * field is not provided. + */ + unsigned int count; + } token; + } details; +}; + +/** + * Contract choice (part of the v1 contract terms). + */ +struct TALER_MerchantContractChoice +{ + /** + * List of inputs the wallet must provision (all of them) to satisfy the + * conditions for the contract. + */ + struct TALER_MerchantContractInput *inputs; + + /** + * Length of the @e inputs array. + */ + unsigned int inputs_len; + + /** + * List of outputs the merchant promises to yield (all of them) once + * the contract is paid. + */ + struct TALER_MerchantContractOutput *outputs; + + /** + * Length of the @e outputs array. + */ + unsigned int outputs_len; +}; + +struct TALER_MerchantContractLimits +{ + /** + * Currency these limits are for. + */ + char currency[TALER_CURRENCY_LEN]; + + /** + * The hash of the merchant instance's wire details. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * Wire transfer method identifier for the wire method associated with ``h_wire``. + * The wallet may only select exchanges via a matching auditor if the + * exchange also supports this wire method. + * The wire transfer fees must be added based on this wire transfer method. + */ + char *wire_method; + + /** + * Maximum total deposit fee accepted by the merchant for this contract. + */ + struct TALER_Amount max_fee; +}; + +struct TALER_MerchantContractTokenAuthority +{ + /** + * Label of the token authority. + */ + const char *label; + + /** + * Human-readable description of the semantics of the tokens issued by + * this authority. + */ + char *description; + + /** + * Map from IETF BCP 47 language tags to localized description. + */ + json_t *description_i18n; + + /** + * Public key used to validate tokens signed by this authority. + */ + struct TALER_TokenFamilyPublicKey *pub; + + /** + * When will tokens signed by this key expire? + */ + struct GNUNET_TIME_Timestamp token_expiration; + + /** + * Must a wallet understand this token type to process contracts that + * consume or yield it? + */ + bool critical; + + /** + * Kind of the token. + */ + enum TALER_MerchantContractTokenKind kind; + + /** + * Kind-specific information about the token. + */ + union + { + /** + * Subscription token. + */ + struct + { + /** + * When does the subscription period start? + */ + struct GNUNET_TIME_Timestamp start_date; + + /** + * When does the subscription period end? + */ + struct GNUNET_TIME_Timestamp end_date; + + /** + * Array of domain names where this subscription can be safely used + * (e.g. the issuer warrants that these sites will re-issue tokens of + * this type if the respective contract says so). May contain "*" for + * any domain or subdomain. + */ + const char **trusted_domains; + + /** + * Length of the @e trusted_domains array. + */ + unsigned int trusted_domains_len; + } subscription; + + /** + * Discount token. + */ + struct + { + /** + * Array of domain names where this discount token is intended to be + * used. May contain "*" for any domain or subdomain. Users should be + * warned about sites proposing to consume discount tokens of this + * type that are not in this list that the merchant is accepting a + * coupon from a competitor and thus may be attaching different + * semantics (like get 20% discount for my competitors 30% discount + * token). + */ + const char **expected_domains; + + /** + * Length of the @e expected_domains array. + */ + unsigned int expected_domains_len; + + } discount; + } details; +}; + +/** + * Struct to hold contract terms in v0 and v1 format. v0 contracts are modelled + * as a v1 contract with a single choice and no inputs and outputs. Use the + * version field to explicitly differentiate between v0 and v1 contracts. + */ +struct TALER_MerchantContract +{ + /** + * URL where the same contract could be ordered again (if available). + */ + const char *public_reorder_url; + + /** + * Our order ID. + */ + const char *order_id; + + /** + * Merchant base URL. + */ + char *merchant_base_url; + + /** + * Merchant information. + */ + struct + { + /** + * Legal name of the instance + */ + char *name; + + /** + * Merchant's site url + */ + char *website; + + /** + * Email contact for customers + */ + char *email; + + /** + * merchant's logo data uri + */ + char *logo; + + /** + * Merchant address + */ + json_t *address; + + /** + * Jurisdiction of the business + */ + json_t *jurisdiction; + } merchant; + + /** + * Price to be paid for the transaction. Could be 0. The price is in addition + * to other instruments, such as rations and tokens. + * The exchange will subtract deposit fees from that amount + * before transferring it to the merchant. + */ + struct TALER_Amount brutto; + + /** + * Summary of the contract. + */ + const char *summary; + + /** + * Internationalized summary. + */ + json_t *summary_i18n; + + /** + * URL that will show that the contract was successful + * after it has been paid for. + */ + const char *fulfillment_url; + + /** + * Message shown to the customer after paying for the contract. + * Either fulfillment_url or fulfillment_message must be specified. + */ + const char *fulfillment_message; + + /** + * Map from IETF BCP 47 language tags to localized fulfillment messages. + */ + json_t *fulfillment_message_i18n; + + /** + * Array of products that are part of the purchase. + */ + const json_t *products; + + /** + * Timestamp of the contract. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Deadline for refunds. + */ + struct GNUNET_TIME_Timestamp refund_deadline; + + /** + * Specifies for how long the wallet should try to get an + * automatic refund for the purchase. + */ + struct GNUNET_TIME_Relative auto_refund; + + /** + * Payment deadline. + */ + struct GNUNET_TIME_Timestamp pay_deadline; + + /** + * Wire transfer deadline. + */ + struct GNUNET_TIME_Timestamp wire_deadline; + + /** + * Delivery date. + */ + struct GNUNET_TIME_Timestamp delivery_date; + + /** + * Delivery location. + */ + json_t *delivery_location; + + /** + * Nonce generated by the wallet and echoed by the merchant + * in this field when the proposal is generated. + */ + const char *nonce; + + /** + * Extra data that is only interpreted by the merchant frontend. + */ + const json_t *extra; + + /** + * Specified version of the contract. + */ + enum TALER_MerchantContractVersion version; + + /** + * Array of possible specific contracts the wallet/customer may choose + * from by selecting the respective index when signing the deposit + * confirmation. + */ + struct TALER_MerchantContractChoice *choices; + + /** + * Length of the @e choices array. + */ + unsigned int choices_len; + + /** + * Array of token authorities. + */ + struct TALER_MerchantContractTokenAuthority *token_authorities; + + /** + * Length of the @e token_authorities array. + */ + unsigned int token_authorities_len; + + /** + * Maximum fee as given by the client request. + */ + struct TALER_Amount max_fee; + + // TODO: Add exchanges array +}; + +enum TALER_MerchantContractInputType +TMH_string_to_contract_input_type (const char *str); + +enum TALER_MerchantContractOutputType +TMH_string_to_contract_output_type (const char *str); + +/** + * Serialize @a contract to a JSON object, ready to be stored in the database. + * The @a contract can be of v0 or v1. + * + * @param[in] contract contract struct to serialize + * @param[in] instance merchant instance for this contract + * @param[in] exchanges JSON array of exchanges + * @param[out] out serialized contract as JSON object + * @return #GNUNET_OK on success + * #GNUNET_NO if @a contract was not valid + * #GNUNET_SYSERR on failure + */ +enum GNUNET_GenericReturnValue +TMH_serialize_contract (const struct TALER_MerchantContract *contract, + const struct TMH_MerchantInstance *instance, + json_t *exchanges, + json_t **out); + +enum GNUNET_GenericReturnValue +TMH_serialize_contract_v0 (const struct TALER_MerchantContract *contract, + const struct TMH_MerchantInstance *instance, + json_t *exchanges, + json_t **out); + +enum GNUNET_GenericReturnValue +TMH_serialize_contract_v1 (const struct TALER_MerchantContract *contract, + const struct TMH_MerchantInstance *instance, + json_t *exchanges, + json_t **out);
\ No newline at end of file diff --git a/src/backend/taler-merchant-httpd_mhd.c b/src/backend/taler-merchant-httpd_mhd.c index e96acca5..0bb22b35 100644 --- a/src/backend/taler-merchant-httpd_mhd.c +++ b/src/backend/taler-merchant-httpd_mhd.c @@ -59,6 +59,7 @@ TMH_MHD_test_html_desired (struct MHD_Connection *connection) bool ret = false; const char *accept; + // FIXME: use TALER_MHD_check_accept here! accept = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_ACCEPT); diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c index 07a6233a..14edfd55 100644 --- a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c +++ b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -2032,9 +2032,10 @@ phase_execute_pay_transaction (struct PayContext *pc) * @param cls closure with our `struct PayContext *` * @param deposit_serial which deposit operation is this about * @param exchange_url URL of the exchange that issued the coin + * @param h_wire hash of merchant's wire details + * @param deposit_timestamp when was the deposit made * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin - * @param h_wire hash of merchant's wire details * @param coin_pub public key of the coin */ static void @@ -2043,6 +2044,7 @@ deposit_paid_check ( uint64_t deposit_serial, const char *exchange_url, const struct TALER_MerchantWireHashP *h_wire, + struct GNUNET_TIME_Timestamp deposit_timestamp, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub) diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-refund.c b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c index e5595296..134cd2ee 100644 --- a/src/backend/taler-merchant-httpd_post-orders-ID-refund.c +++ b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c @@ -565,7 +565,8 @@ TMH_post_orders_ID_refund (const struct TMH_RequestHandler *rh, enum GNUNET_GenericReturnValue res; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("h_contract", &prd->h_contract_terms), + GNUNET_JSON_spec_fixed_auto ("h_contract", + &prd->h_contract_terms), GNUNET_JSON_spec_end () }; res = TALER_MHD_parse_json_data (connection, @@ -666,8 +667,9 @@ TMH_post_orders_ID_refund (const struct TMH_RequestHandler *rh, } { - GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TMH_currency, - &prd->refund_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TMH_currency, + &prd->refund_amount)); qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, hc->instance->settings.id, &prd->h_contract_terms, diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c index 1c850990..98653997 100644 --- a/src/backend/taler-merchant-httpd_private-get-orders-ID.c +++ b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -263,6 +263,11 @@ struct GetOrderRequestContext struct GNUNET_TIME_Timestamp timestamp; /** + * Timestamp of the last payment. + */ + struct GNUNET_TIME_Timestamp last_payment; + + /** * Order summary. Pointer into @e contract_terms. */ const char *summary; @@ -996,15 +1001,16 @@ phase_unpaid_finish (struct GetOrderRequestContext *gorc) * @param pending true if the this refund was not yet processed by the wallet/exchange */ static void -process_refunds_cb (void *cls, - uint64_t refund_serial, - struct GNUNET_TIME_Timestamp timestamp, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const char *exchange_url, - uint64_t rtransaction_id, - const char *reason, - const struct TALER_Amount *refund_amount, - bool pending) +process_refunds_cb ( + void *cls, + uint64_t refund_serial, + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + uint64_t rtransaction_id, + const char *reason, + const struct TALER_Amount *refund_amount, + bool pending) { struct GetOrderRequestContext *gorc = cls; @@ -1013,18 +1019,19 @@ process_refunds_cb (void *cls, (unsigned long long) rtransaction_id, TALER_amount2s (refund_amount), reason); - GNUNET_assert (0 == - json_array_append_new ( - gorc->refund_details, - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - refund_amount), - GNUNET_JSON_pack_bool ("pending", - pending), - GNUNET_JSON_pack_timestamp ("timestamp", - timestamp), - GNUNET_JSON_pack_string ("reason", - reason)))); + GNUNET_assert ( + 0 == + json_array_append_new ( + gorc->refund_details, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + refund_amount), + GNUNET_JSON_pack_bool ("pending", + pending), + GNUNET_JSON_pack_timestamp ("timestamp", + timestamp), + GNUNET_JSON_pack_string ("reason", + reason)))); /* For refunded coins, we are not charged deposit fees, so subtract those again */ for (struct TransferQuery *tq = gorc->tq_head; @@ -1044,10 +1051,11 @@ process_refunds_cb (void *cls, return; } - GNUNET_assert (0 <= - TALER_amount_subtract (&gorc->deposit_fees_total, - &gorc->deposit_fees_total, - &tq->deposit_fee)); + GNUNET_assert ( + 0 <= + TALER_amount_subtract (&gorc->deposit_fees_total, + &gorc->deposit_fees_total, + &tq->deposit_fee)); } } if (GNUNET_OK != @@ -1084,29 +1092,32 @@ phase_check_refunds (struct GetOrderRequestContext *gorc) GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (gorc->contract_amount.currency, &gorc->refund_amount)); - qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, - hc->instance->settings.id, - &gorc->h_contract_terms, - &process_refunds_cb, - gorc); + qs = TMH_db->lookup_refunds_detailed ( + TMH_db->cls, + hc->instance->settings.id, + &gorc->h_contract_terms, + &process_refunds_cb, + gorc); if (0 > qs) { GNUNET_break (0); phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "detailed refunds")); + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "detailed refunds")); return; } if (gorc->refund_currency_mismatch) { GNUNET_break (0); phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "refunds in different currency than original order price")); + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "refunds in different currency than original order price")); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -1127,19 +1138,22 @@ phase_check_refunds (struct GetOrderRequestContext *gorc) * @param cls a `struct GetOrderRequestContext` * @param deposit_serial identifies the deposit operation * @param exchange_url URL of the exchange that issued @a coin_pub + * @param h_wire hash of the merchant's wire account into which the deposit was made + * @param deposit_timestamp when was the deposit made * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin - * @param h_wire hash of the merchant's wire account into which the deposit was made * @param coin_pub public key of the deposited coin */ static void -deposit_cb (void *cls, - uint64_t deposit_serial, - const char *exchange_url, - const struct TALER_MerchantWireHashP *h_wire, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_CoinSpendPublicKeyP *coin_pub) +deposit_cb ( + void *cls, + uint64_t deposit_serial, + const char *exchange_url, + const struct TALER_MerchantWireHashP *h_wire, + struct GNUNET_TIME_Timestamp deposit_timestamp, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_CoinSpendPublicKeyP *coin_pub) { struct GetOrderRequestContext *gorc = cls; struct TransferQuery *tq; @@ -1148,6 +1162,9 @@ deposit_cb (void *cls, "Checking deposit status for coin %s (over %s)\n", TALER_B2S (coin_pub), TALER_amount2s (amount_with_fee)); + gorc->last_payment + = GNUNET_TIME_timestamp_max (gorc->last_payment, + deposit_timestamp); tq = GNUNET_new (struct TransferQuery); tq->gorc = gorc; tq->exchange_url = GNUNET_strdup (exchange_url); @@ -1380,7 +1397,12 @@ phase_reply_result (struct GetOrderRequestContext *gorc) &gorc->claim_token, h_contract); } - + if (GNUNET_TIME_absolute_is_zero (gorc->last_payment.abs_time)) + { + GNUNET_break (GNUNET_YES == + TALER_amount_is_zero (&gorc->contract_amount)); + gorc->last_payment = gorc->timestamp; + } ret = TALER_MHD_REPLY_JSON_PACK ( gorc->sc.con, MHD_HTTP_OK, @@ -1403,6 +1425,8 @@ phase_reply_result (struct GetOrderRequestContext *gorc) gorc->contract_terms), GNUNET_JSON_pack_string ("order_status", "paid"), + GNUNET_JSON_pack_timestamp ("last_payment", + gorc->last_payment), GNUNET_JSON_pack_bool ("refunded", gorc->refunded), GNUNET_JSON_pack_bool ("wired", @@ -1443,9 +1467,10 @@ phase_error (struct GetOrderRequestContext *gorc) MHD_RESULT -TMH_private_get_orders_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +TMH_private_get_orders_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { struct GetOrderRequestContext *gorc = hc->ctx; diff --git a/src/backend/taler-merchant-httpd_private-get-orders.c b/src/backend/taler-merchant-httpd_private-get-orders.c index 5fc91188..4c6a104e 100644 --- a/src/backend/taler-merchant-httpd_private-get-orders.c +++ b/src/backend/taler-merchant-httpd_private-get-orders.c @@ -406,6 +406,18 @@ add_order (void *cls, return; } + if (TALER_amount_is_zero (&order_amount) && + (po->of.wired != TALER_EXCHANGE_YNA_ALL) ) + { + /* If we are actually filtering by wire status, + and the order was over an amount of zero, + do not return it as wire status is not + exactly meaningful for orders over zero. */ + json_decref (contract_terms); + GNUNET_free (order_id); + return; + } + if (GNUNET_TIME_absolute_is_future (rd.abs_time) && paid) { diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c index 7ca56319..eedece55 100644 --- a/src/backend/taler-merchant-httpd_private-post-orders.c +++ b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -22,19 +22,25 @@ * @brief the POST /orders handler * @author Christian Grothoff * @author Marcello Stanisci + * @author Christian Blättler */ #include "platform.h" #include <gnunet/gnunet_common.h> #include <gnunet/gnunet_json_lib.h> #include <gnunet/gnunet_time_lib.h> #include <jansson.h> +#include <microhttpd.h> #include <string.h> +#include <taler/taler_error_codes.h> #include <taler/taler_signatures.h> #include <taler/taler_json_lib.h> +#include "taler-merchant-httpd.h" #include "taler-merchant-httpd_private-post-orders.h" #include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_contract.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_private-get-orders.h" +#include "taler_merchantdb_plugin.h" /** @@ -231,43 +237,58 @@ struct OrderContext struct { /** + * Version of the contract terms. + */ + enum TALER_MerchantContractVersion version; + + /** * Our order ID. */ const char *order_id; /** - * Summary of the order. - */ + * Summary of the contract. + */ const char *summary; /** - * Internationalized summary. - */ + * Internationalized summary. + */ json_t *summary_i18n; /** - * URL where the same contract could be ordered again (if available). - */ - const char *public_reorder_url; - - /** - * URL that will show that the order was successful - * after it has been paid for. - */ + * URL that will show that the contract was successful + * after it has been paid for. + */ const char *fulfillment_url; /** - * Message shown to the customer after paying for the order. - * Either fulfillment_url or fulfillment_message must be specified. - */ + * Message shown to the customer after paying for the contract. + * Either fulfillment_url or fulfillment_message must be specified. + */ const char *fulfillment_message; /** - * Map from IETF BCP 47 language tags to localized fulfillment messages. - */ + * Map from IETF BCP 47 language tags to localized fulfillment messages. + */ json_t *fulfillment_message_i18n; /** + * Array of products that are part of the purchase. + */ + const json_t *products; + + /** + * URL where the same contract could be ordered again (if available). + */ + const char *public_reorder_url; + + /** + * Array of contract choices. Is null for v0 contracts. + */ + const json_t *choices; + + /** * Merchant base URL. */ char *merchant_base_url; @@ -303,14 +324,9 @@ struct OrderContext json_t *delivery_location; /** - * Array of products that are part of the purchase. - */ - const json_t *products; - - /** - * Gross amount value of the contract. Used to - * compute @e max_stefan_fee. - */ + * Gross amount value of the contract. Used to + * compute @e max_stefan_fee. + */ struct TALER_Amount brutto; /** @@ -343,6 +359,34 @@ struct OrderContext } parse_order; /** + * Information set in the ORDER_PHASE_PARSE_CHOICES phase. + */ + struct + { + /** + * Array of possible specific contracts the wallet/customer may choose + * from by selecting the respective index when signing the deposit + * confirmation. + */ + struct TALER_MerchantContractChoice *choices; + + /** + * Length of the @e choices array. + */ + unsigned int choices_len; + + /** + * Array of token types referenced in the contract. + */ + struct TALER_MerchantContractTokenAuthority *authorities; + + /** + * Length of the @e authorities array. + */ + unsigned int authorities_len; + } parse_choices; + + /** * Information set in the ORDER_PHASE_MERGE_INVENTORY phase. */ struct @@ -477,6 +521,7 @@ struct OrderContext { ORDER_PHASE_PARSE_REQUEST, ORDER_PHASE_PARSE_ORDER, + ORDER_PHASE_PARSE_CHOICES, ORDER_PHASE_MERGE_INVENTORY, ORDER_PHASE_ADD_PAYMENT_DETAILS, ORDER_PHASE_SET_EXCHANGES, @@ -634,6 +679,16 @@ clean_order (void *cls) json_decref (oc->merge_inventory.products); oc->merge_inventory.products = NULL; } + // TODO: Check if this is even correct + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + GNUNET_array_grow (oc->parse_choices.choices[i].inputs, + oc->parse_choices.choices[i].inputs_len, + 0); + GNUNET_array_grow (oc->parse_choices.choices[i].outputs, + oc->parse_choices.choices[i].outputs_len, + 0); + } GNUNET_array_grow (oc->parse_request.inventory_products, oc->parse_request.inventory_products_length, 0); @@ -1262,6 +1317,142 @@ get_exchange_keys (void *cls, rx); } +/** + * Fetch details about the token family with the given @a slug + * and add them to the list of token authorities. Check if the + * token family already has a valid key configured and if not, + * create a new one. + * + * @param[in,out] oc order context + * @param slug slug of the token family + * @param start_date validity start date of the token + */ +static MHD_RESULT +set_token_authority (struct OrderContext *oc, + const char *slug, + struct GNUNET_TIME_Timestamp start_date) +{ + struct TALER_MERCHANTDB_TokenFamilyKeyDetails key_details; + struct TALER_MerchantContractTokenAuthority authority; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute min_start_date = GNUNET_TIME_absolute_subtract ( + start_date.abs_time, + // TODO: make this configurable. This is the granularity of token + // expiration dates. + GNUNET_TIME_UNIT_DAYS + ); + + qs = TMH_db->lookup_token_family_key (TMH_db->cls, + oc->hc->instance->settings.id, + slug, + GNUNET_TIME_absolute_to_timestamp (min_start_date), + start_date, + &key_details); + + if (qs <= 0) + { + enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + unsigned int http_status = 0; + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_FETCH_FAILED; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Token family slug unknown\n"); + http_status = MHD_HTTP_NOT_FOUND; + ec = TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* case listed to make compilers happy */ + GNUNET_assert (0); + } + GNUNET_break (0); + reply_with_error (oc, + http_status, + ec, + "token_family_slug"); + return MHD_NO; + } + + if (NULL == key_details.pub) + { + /* If public key is NULL, private key must also be NULL */ + GNUNET_assert (NULL == key_details.priv); + + struct GNUNET_CRYPTO_BlindSignPrivateKey *priv; + struct GNUNET_CRYPTO_BlindSignPublicKey *pub; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_TIME_Timestamp valid_before = GNUNET_TIME_absolute_to_timestamp( + GNUNET_TIME_absolute_add (now, + key_details.token_family.duration)); + + GNUNET_CRYPTO_blind_sign_keys_create (&priv, + &pub, + // TODO: Make cipher and key length configurable + GNUNET_CRYPTO_BSA_RSA, + 4096); + + struct TALER_TokenFamilyPublicKey token_pub = { + .public_key = *pub, + }; + struct TALER_TokenFamilyPrivateKey token_priv = { + .private_key = *priv, + }; + + qs = TMH_db->insert_token_family_key (TMH_db->cls, + slug, + &token_pub, + &token_priv, + GNUNET_TIME_absolute_to_timestamp (now), + valid_before); + + authority.token_expiration = valid_before; + authority.pub = &token_pub; + // GNUNET_CRYPTO_blind_sign_priv_decref (&token_priv.private_key); + } else { + authority.token_expiration = key_details.valid_before; + authority.pub = key_details.pub; + } + + authority.label = slug; + authority.description = key_details.token_family.description; + authority.description_i18n = key_details.token_family.description_i18n; + + GNUNET_free (key_details.token_family.slug); + GNUNET_free (key_details.token_family.name); + if (NULL != key_details.priv) { + GNUNET_CRYPTO_blind_sign_priv_decref (&key_details.priv->private_key); + } + + switch (key_details.token_family.kind) { + case TALER_MERCHANTDB_TFK_Subscription: + authority.kind = TALER_MCTK_SUBSCRIPTION; + authority.details.subscription.start_date = key_details.valid_after; + authority.details.subscription.end_date = key_details.valid_before; + authority.critical = true; + // TODO: Set trusted domains + break; + case TALER_MERCHANTDB_TFK_Discount: + authority.kind = TALER_MCTK_DISCOUNT; + authority.critical = false; + // TODO: Set expected domains + break; + } + + GNUNET_array_append (oc->parse_choices.authorities, + oc->parse_choices.authorities_len, + authority); + + return MHD_YES; +} /** * Serialize order into @a oc->serialize_order.contract, @@ -1276,45 +1467,159 @@ serialize_order (struct OrderContext *oc) const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; json_t *merchant; + json_t *token_types = json_object (); + json_t *choices = json_array (); merchant = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("name", - settings->name), + settings->name), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("website", - settings->website)), + settings->website)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("email", - settings->email)), + settings->email)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("logo", - settings->logo))); + settings->logo))); GNUNET_assert (NULL != merchant); { - json_t *loca = settings->address; + json_t *loca; + /* Handle merchant address */ + loca = settings->address; if (NULL != loca) { + loca = json_deep_copy (loca); + GNUNET_assert (NULL != loca); GNUNET_assert (0 == - json_object_set (merchant, - "address", - loca)); + json_object_set_new (merchant, + "address", + loca)); } } { - json_t *juri = settings->jurisdiction; + json_t *juri; /* Handle merchant jurisdiction */ + juri = settings->jurisdiction; if (NULL != juri) { + juri = json_deep_copy (juri); + GNUNET_assert (NULL != juri); GNUNET_assert (0 == - json_object_set (merchant, - "jurisdiction", - juri)); + json_object_set_new (merchant, + "jurisdiction", + juri)); } } + for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) + { + struct TALER_MerchantContractTokenAuthority *authority = &oc->parse_choices.authorities[i]; + + // TODO: Finish spec to clearly define how token families are stored in + // ContractTerms. + // Here are some thoughts: + // - Multiple keys of the same token family can be referenced in + // one contract. E.g. exchanging old subscription for new. + // - TokenAuthority should be renamed to TokenFamily for consistency. + // - TokenFamilySlug can be used instead of TokenAuthorityLabel, but + // every token-based in- or output needs to have a valid_after date, + // so it's clear with key is referenced. + json_t *jauthority = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("description", + authority->description), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("description_i18n", + authority->description_i18n)), + GNUNET_JSON_pack_data_auto ("h_pub", + &authority->pub->public_key.pub_key_hash), + GNUNET_JSON_pack_data_auto ("pub", + &authority->pub->public_key.pub_key_hash), + GNUNET_JSON_pack_timestamp ("token_expiration", + authority->token_expiration) + ); + + GNUNET_assert (0 == + json_object_set_new (token_types, + authority->label, + jauthority)); + } + + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + struct TALER_MerchantContractChoice *choice = &oc->parse_choices.choices[i]; + + json_t *inputs = json_array (); + json_t *outputs = json_array (); + + for (unsigned int j = 0; j<choice->inputs_len; j++) + { + struct TALER_MerchantContractInput *input = &choice->inputs[j]; + + json_t *jinput = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_int64 ("type", + input->type) + ); + + if (TALER_MCIT_TOKEN == input->type) + { + GNUNET_assert(0 == + json_object_set_new(jinput, + "number", + json_integer ( + input->details.token.count))); + GNUNET_assert(0 == + json_object_set_new(jinput, + "token_family_slug", + json_string ( + input->details.token.token_family_slug))); + } + + GNUNET_assert (0 == json_array_append_new (inputs, jinput)); + } + + for (unsigned int j = 0; j<choice->outputs_len; j++) + { + struct TALER_MerchantContractOutput *output = &choice->outputs[j]; + + json_t *joutput = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_int64 ("type", + output->type) + ); + + if (TALER_MCOT_TOKEN == output->type) + { + GNUNET_assert(0 == + json_object_set_new(joutput, + "number", + json_integer ( + output->details.token.count))); + + GNUNET_assert(0 == + json_object_set_new(joutput, + "token_family_slug", + json_string ( + output->details.token.token_family_slug))); + } + + GNUNET_assert (0 == json_array_append (outputs, joutput)); + } + + json_t *jchoice = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_incref ("inputs", + inputs), + GNUNET_JSON_pack_array_incref ("outputs", + outputs) + ); + + GNUNET_assert (0 == json_array_append (choices, jchoice)); + } + oc->serialize_order.contract = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_int64 ("version", + oc->parse_order.version), GNUNET_JSON_pack_string ("summary", oc->parse_order.summary), GNUNET_JSON_pack_allow_null ( @@ -1368,6 +1673,14 @@ serialize_order (struct OrderContext *oc) TALER_JSON_pack_amount ("amount", &oc->parse_order.brutto), GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("choices", + choices) + ), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("token_types", + token_types) + ), + GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("extra", (json_t *) oc->parse_order.extra)) ); @@ -1378,6 +1691,7 @@ serialize_order (struct OrderContext *oc) "refund_deadline", GNUNET_JSON_from_timestamp ( oc->parse_order.refund_deadline))); + GNUNET_log ( GNUNET_ERROR_TYPE_INFO, "Refund deadline for contact is %llu\n", @@ -1401,7 +1715,6 @@ serialize_order (struct OrderContext *oc) oc->phase++; } - /** * Set max_fee in @a oc based on STEFAN value if * not yet present. Upon success, continue @@ -1437,7 +1750,6 @@ set_max_fee (struct OrderContext *oc) oc->phase++; } - /** * Set list of acceptable exchanges in @a oc. Upon success, continue * processing with set_max_fee(). @@ -1503,8 +1815,8 @@ set_exchanges (struct OrderContext *oc) /** - * Add missing fields to the order. Upon success, continue - * processing with merge_inventory(). + * Parse the order field of the request. Upon success, continue + * processing with parse_choices(). * * @param[in,out] oc order context */ @@ -1514,12 +1826,17 @@ parse_order (struct OrderContext *oc) const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; const char *merchant_base_url = NULL; + const char *version = NULL; const json_t *jmerchant = NULL; /* auto_refund only needs to be type-checked, * mostly because in GNUnet relative times can't * be negative. */ bool no_fee; struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("version", + &version), + NULL), TALER_JSON_spec_amount_any ("amount", &oc->parse_order.brutto), GNUNET_JSON_spec_string ("summary", @@ -1537,10 +1854,6 @@ parse_order (struct OrderContext *oc) &oc->parse_order.order_id), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("public_reorder_url", - &oc->parse_order.public_reorder_url), - NULL), - GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("fulfillment_message", &oc->parse_order.fulfillment_message), NULL), @@ -1553,6 +1866,14 @@ parse_order (struct OrderContext *oc) &oc->parse_order.fulfillment_url), NULL), GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("public_reorder_url", + &oc->parse_order.public_reorder_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("choices", + &oc->parse_order.choices), + NULL), + GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_web_url ("merchant_base_url", &merchant_base_url), NULL), @@ -1616,6 +1937,46 @@ parse_order (struct OrderContext *oc) ret); return; } + if (NULL == version || 0 == strcmp("0", version)) + { + oc->parse_order.version = TALER_MCV_V0; + + if (NULL != oc->parse_order.choices) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR, + "choices array must be null for v0 contracts"); + return; + } + } + else if (0 == strcmp("1", version)) + { + oc->parse_order.version = TALER_MCV_V1; + + if (! json_is_array(oc->parse_order.choices)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order.choices is not a valid array"); + return; + } + } + else + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_VERSION_MALFORMED, + "invalid version specified in order, supported are null, '0' or '1'"); + return; + } if (! TMH_test_exchange_configured_for_currency ( oc->parse_order.brutto.currency)) { @@ -1628,9 +1989,9 @@ parse_order (struct OrderContext *oc) return; } if ( (! no_fee) && - (GNUNET_OK != - TALER_amount_cmp_currency (&oc->parse_order.brutto, - &oc->parse_order.max_fee)) ) + (GNUNET_OK != + TALER_amount_cmp_currency (&oc->parse_order.brutto, + &oc->parse_order.max_fee)) ) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); @@ -1907,6 +2268,263 @@ parse_order (struct OrderContext *oc) oc->phase++; } +/** + * Parse contract choices. Upon success, continue + * processing with merge_inventory(). + * + * @param[in,out] oc order context + */ +static void +parse_choices (struct OrderContext *oc) +{ + if (NULL == oc->parse_order.choices) + { + oc->phase++; + return; + } + + GNUNET_array_grow (oc->parse_choices.choices, + oc->parse_choices.choices_len, + json_array_size (oc->parse_order.choices)); + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + const char *error_name; + unsigned int error_line; + const json_t *jinputs; + const json_t *joutputs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("inputs", + &jinputs), + GNUNET_JSON_spec_array_const ("outputs", + &joutputs), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + + ret = GNUNET_JSON_parse (json_array_get (oc->parse_order.choices, i), + spec, + &error_name, + &error_line); + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Choice parsing failed: %s:%u\n", + error_name, + error_line); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "choice"); + return; + } + + if (! json_is_array (jinputs) || + ! json_is_array (joutputs)) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inputs or outputs"); + return; + } + + { + // TODO: Maybe move to a separate function + const json_t *jinput; + size_t idx; + json_array_foreach ((json_t *) jinputs, idx, jinput) + { + // TODO: Assuming this struct would have more fields, would i use GNUNET_new then? + // Or should i use GNUNET_grow first and then get the element using the index? + // Assuming you add a field in the future, it's easier to that way, so you don't + // free it. + struct TALER_MerchantContractInput input = {.details.token.count = 1}; + const char *kind; + const char *ierror_name; + unsigned int ierror_line; + + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &input.details.token.token_family_slug), + GNUNET_JSON_spec_timestamp ("valid_after", + &input.details.token.valid_after), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &input.details.token.count), + NULL), + GNUNET_JSON_spec_end() + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (jinput, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid input #%u for field %s\n", + (unsigned int) idx, + ierror_name); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ierror_name); + return; + } + + input.type = TMH_string_to_contract_input_type (kind); + + if (TALER_MCIT_INVALID == input.type) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'kind' invalid in input #%u\n", + (unsigned int) idx); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kind"); + return; + } + + if (0 == input.details.token.count) + { + /* Ignore inputs with 'number' field set to 0 */ + continue; + } + + bool found = false; + + for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) + { + if (0 == strcmp (oc->parse_choices.authorities[i].label, + input.details.token.token_family_slug)) + { + found = true; + break; + } + } + + if (! found) + { + MHD_RESULT res; + res = set_token_authority (oc, + input.details.token.token_family_slug, + input.details.token.valid_after); + + if (MHD_NO == res) + { + return; + } + } + + GNUNET_array_append (oc->parse_choices.choices[i].inputs, + oc->parse_choices.choices[i].inputs_len, + input); + } + } + + { + const json_t *joutput; + size_t idx; + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MerchantContractOutput output = { .details.token.count = 1 }; + const char *kind; + const char *ierror_name; + unsigned int ierror_line; + + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &output.details.token.token_family_slug), + GNUNET_JSON_spec_timestamp ("valid_after", + &output.details.token.valid_after), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &output.details.token.count), + NULL), + GNUNET_JSON_spec_end() + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (joutput, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_JSON_parse_free (spec); + GNUNET_JSON_parse_free (ispec); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid output #%u for field %s\n", + (unsigned int) idx, + ierror_name); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ierror_name); + return; + } + + output.type = TMH_string_to_contract_output_type (kind); + + if (TALER_MCOT_INVALID == output.type) + { + GNUNET_JSON_parse_free (spec); + GNUNET_JSON_parse_free (ispec); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'kind' invalid in output #%u\n", + (unsigned int) idx); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kind"); + return; + } + + if (0 == output.details.token.count) + { + /* Ignore outputs with 'number' field set to 0 */ + continue; + } + + bool found = false; + + for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) + { + if (0 == strcmp (oc->parse_choices.authorities[i].label, + output.details.token.token_family_slug)) + { + found = true; + break; + } + } + + if (! found) + { + MHD_RESULT res; + res = set_token_authority (oc, + output.details.token.token_family_slug, + output.details.token.valid_after); + + if (MHD_NO == res) + { + return; + } + } + + GNUNET_array_append (oc->parse_choices.choices[i].outputs, + oc->parse_choices.choices[i].outputs_len, + output); + } + } + } + + oc->phase++; +} /** * Process the @a payment_target and add the details of how the @@ -2305,6 +2923,9 @@ TMH_private_post_orders ( case ORDER_PHASE_PARSE_ORDER: parse_order (oc); break; + case ORDER_PHASE_PARSE_CHOICES: + parse_choices (oc); + break; case ORDER_PHASE_MERGE_INVENTORY: merge_inventory (oc); break; diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am index e7fb15cf..defc3cf9 100644 --- a/src/backenddb/Makefile.am +++ b/src/backenddb/Makefile.am @@ -173,6 +173,8 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_lookup_token_families.h pg_lookup_token_families.c \ pg_delete_token_family.h pg_delete_token_family.c \ pg_update_token_family.h pg_update_token_family.c \ + pg_insert_token_family_key.h pg_insert_token_family_key.c \ + pg_lookup_token_family_key.h pg_lookup_token_family_key.c \ plugin_merchantdb_postgres.c \ pg_helper.h pg_helper.c libtaler_plugin_merchantdb_postgres_la_LIBADD = \ diff --git a/src/backenddb/pg_insert_token_family.h b/src/backenddb/pg_insert_token_family.h index e05755a6..d584f5e7 100644 --- a/src/backenddb/pg_insert_token_family.h +++ b/src/backenddb/pg_insert_token_family.h @@ -28,7 +28,7 @@ /** * @param cls closure - * @param instance_id instance to insert token family for TODO: Is this needed? + * @param instance_id instance to insert token family for * @param token_family_slug slug of the token family to insert * @param details the token family details to insert * @return database result code diff --git a/src/backenddb/pg_insert_token_family_key.c b/src/backenddb/pg_insert_token_family_key.c new file mode 100644 index 00000000..b13c8079 --- /dev/null +++ b/src/backenddb/pg_insert_token_family_key.c @@ -0,0 +1,97 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_insert_token_family_key.c + * @brief Implementation of the insert_token_family_key function for Postgres + * @author Christian Blättler + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_pq_lib.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_insert_token_family_key.h" +#include "pg_helper.h" + +enum GNUNET_DB_QueryStatus +TMH_PG_insert_token_family_key (void *cls, + const char *token_family_slug, + const struct TALER_TokenFamilyPublicKey *pub, + const struct TALER_TokenFamilyPrivateKey *priv, + const struct GNUNET_TIME_Timestamp valid_after, + const struct GNUNET_TIME_Timestamp valid_before) +{ + struct PostgresClosure *pg = cls; + const char *cipher = NULL; + struct GNUNET_HashCode pub_hash; + + switch (pub->public_key.cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + cipher = "rsa"; + GNUNET_CRYPTO_rsa_public_key_hash (pub->public_key.details.rsa_public_key, + &pub_hash); + break; + case GNUNET_CRYPTO_BSA_CS: + cipher = "cs"; + GNUNET_CRYPTO_hash (&pub->public_key.details.cs_public_key, + sizeof (pub->public_key.details.cs_public_key), + &pub_hash); + break; + case GNUNET_CRYPTO_BSA_INVALID: + /* case listed to make compilers happy */ + return GNUNET_DB_STATUS_HARD_ERROR; + } + + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (token_family_slug), + GNUNET_PQ_query_param_blind_sign_pub (&pub->public_key), + GNUNET_PQ_query_param_auto_from_type (&pub->public_key.pub_key_hash), + GNUNET_PQ_query_param_blind_sign_priv (&priv->private_key), + GNUNET_PQ_query_param_timestamp (&valid_after), + GNUNET_PQ_query_param_timestamp (&valid_before), + GNUNET_PQ_query_param_string (cipher), + GNUNET_PQ_query_param_end + }; + + GNUNET_assert (pub->public_key.cipher == priv->private_key.cipher); + + GNUNET_assert (0 == + GNUNET_memcmp (&pub_hash, + &pub->public_key.pub_key_hash)); + GNUNET_assert (! GNUNET_TIME_absolute_is_zero ( + valid_after.abs_time)); + GNUNET_assert (! GNUNET_TIME_absolute_is_zero ( + valid_before.abs_time)); + + PREPARE (pg, + "token_family_key_insert", + "INSERT INTO merchant_token_family_keys " + "(token_family_serial" + ",pub" + ",h_pub" + ",priv" + ",valid_after" + ",valid_before" + ",cipher)" + " SELECT token_family_serial, $2, $3, $4, $5, $6, $7" + " FROM merchant_token_families" + " WHERE slug = $1"); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "token_family_key_insert", + params); +}
\ No newline at end of file diff --git a/src/backenddb/pg_insert_token_family_key.h b/src/backenddb/pg_insert_token_family_key.h new file mode 100644 index 00000000..c4fc8d85 --- /dev/null +++ b/src/backenddb/pg_insert_token_family_key.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_insert_token_family_key.h + * @brief implementation of the insert_token_family_key function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_INSERT_TOKEN_FAMILY_KEY_H +#define PG_INSERT_TOKEN_FAMILY_KEY_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + + +/** + * @param cls closure + * @param token_family_slug slug of the token family to insert the key for + * @param pub public key to insert + * @param priv private key to insert + * @param valid_after start of validity period for this key + * @param valid_before end of validity period for this key + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_insert_token_family_key (void *cls, + const char *token_family_slug, + const struct TALER_TokenFamilyPublicKey *pub, + const struct TALER_TokenFamilyPrivateKey *priv, + const struct GNUNET_TIME_Timestamp valid_after, + const struct GNUNET_TIME_Timestamp valid_before); + +#endif diff --git a/src/backenddb/pg_insert_transfer_details.sql b/src/backenddb/pg_insert_transfer_details.sql index 1650d157..0dd11b1b 100644 --- a/src/backenddb/pg_insert_transfer_details.sql +++ b/src/backenddb/pg_insert_transfer_details.sql @@ -135,7 +135,7 @@ THEN FROM merchant_transfer_signatures WHERE credit_serial=my_credit_serial AND signkey_serial=my_signkey_serial - AND credit_amount=in_credit_amount + AND credit_amount=in_total_amount AND wire_fee=in_wire_fee AND execution_time=in_execution_time AND exchange_sig=in_exchange_sig; diff --git a/src/backenddb/pg_lookup_deposits_by_contract_and_coin.c b/src/backenddb/pg_lookup_deposits_by_contract_and_coin.c index 9bf46d0c..089543ea 100644 --- a/src/backenddb/pg_lookup_deposits_by_contract_and_coin.c +++ b/src/backenddb/pg_lookup_deposits_by_contract_and_coin.c @@ -46,6 +46,11 @@ struct LookupDepositsByCnCContext struct PostgresClosure *pg; /** + * Total amount refunded on this coin and contract. + */ + struct TALER_Amount refund_total; + + /** * Transaction result. */ enum GNUNET_DB_QueryStatus qs; @@ -61,6 +66,54 @@ struct LookupDepositsByCnCContext * @param num_results the number of results in @a result */ static void +lookup_refunds_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupDepositsByCnCContext *ldcc = cls; + + for (unsigned int i = 0; i<num_results; i++) + { + struct TALER_Amount refund_amount; + struct GNUNET_PQ_ResultSpec rs[] = { + TALER_PQ_result_spec_amount_with_currency ("refund_amount", + &refund_amount), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + ldcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Coin had refund of %s\n", + TALER_amount2s (&refund_amount)); + if (0 == i) + ldcc->refund_total = refund_amount; + else + GNUNET_assert (0 <= + TALER_amount_add (&ldcc->refund_total, + &ldcc->refund_total, + &refund_amount)); + GNUNET_PQ_cleanup_result (rs); /* technically useless here */ + } +} + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls of type `struct LookupDepositsByCnCContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void lookup_deposits_by_contract_and_coin_cb (void *cls, PGresult *result, unsigned int num_results) @@ -112,6 +165,56 @@ lookup_deposits_by_contract_and_coin_cb (void *cls, ldcc->qs = GNUNET_DB_STATUS_HARD_ERROR; return; } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Coin original deposit value is %s\n", + TALER_amount2s (&amount_with_fee)); + if (TALER_amount_is_valid (&ldcc->refund_total)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Coin had total refunds of %s\n", + TALER_amount2s (&ldcc->refund_total)); + if (1 == + TALER_amount_cmp (&ldcc->refund_total, + &amount_with_fee)) + { + /* Refunds exceeded total deposit? not OK! */ + GNUNET_break (0); + ldcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return; + } + if (0 == + TALER_amount_cmp (&ldcc->refund_total, + &amount_with_fee)) + { + /* refund_total == amount_with_fee; + in this case, the total contributed to the + wire transfer is zero (as are fees) */ + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (ldcc->refund_total.currency, + &amount_with_fee)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (ldcc->refund_total.currency, + &deposit_fee)); + + } + else + { + /* Compute deposit value by subtracting refunds */ + GNUNET_assert (0 < + TALER_amount_subtract (&amount_with_fee, + &amount_with_fee, + &ldcc->refund_total)); + if (-1 == + TALER_amount_cmp (&amount_with_fee, + &deposit_fee)) + { + /* amount_with_fee < deposit_fee, so after refunds less than + the deposit fee remains; reduce deposit fee to + the remaining value of the coin */ + deposit_fee = amount_with_fee; + } + } + } ldcc->cb (ldcc->cb_cls, exchange_url, &amount_with_fee, @@ -128,13 +231,15 @@ lookup_deposits_by_contract_and_coin_cb (void *cls, ldcc->qs = num_results; } + enum GNUNET_DB_QueryStatus -TMH_PG_lookup_deposits_by_contract_and_coin (void *cls, - const char *instance_id, - const struct TALER_PrivateContractHashP *h_contract_terms, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - TALER_MERCHANTDB_CoinDepositCallback cb, - void *cb_cls) +TMH_PG_lookup_deposits_by_contract_and_coin ( + void *cls, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + TALER_MERCHANTDB_CoinDepositCallback cb, + void *cb_cls) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -151,6 +256,37 @@ TMH_PG_lookup_deposits_by_contract_and_coin (void *cls, enum GNUNET_DB_QueryStatus qs; check_connection (pg); + /* no preflight check here, run in transaction by caller! */ + TALER_LOG_DEBUG ("Looking for refund of h_contract_terms %s at `%s'\n", + GNUNET_h2s (&h_contract_terms->hash), + instance_id); + check_connection (pg); + PREPARE (pg, + "lookup_refunds_by_coin_and_contract", + "SELECT" + " refund_amount" + " FROM merchant_refunds" + /* Join to filter by refunds that actually + did work, not only those we approved */ + " JOIN merchant_refund_proofs" + " USING (refund_serial)" + " WHERE coin_pub=$3" + " AND order_serial=" + " (SELECT order_serial" + " FROM merchant_contract_terms" + " WHERE h_contract_terms=$2" + " AND merchant_serial=" + " (SELECT merchant_serial" + " FROM merchant_instances" + " WHERE merchant_id=$1))"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "lookup_refunds_by_coin_and_contract", + params, + &lookup_refunds_cb, + &ldcc); + if (0 > qs) + return qs; + PREPARE (pg, "lookup_deposits_by_contract_and_coin", "SELECT" diff --git a/src/backenddb/pg_lookup_deposits_by_order.c b/src/backenddb/pg_lookup_deposits_by_order.c index fdaf1dfc..fb7637f0 100644 --- a/src/backenddb/pg_lookup_deposits_by_order.c +++ b/src/backenddb/pg_lookup_deposits_by_order.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023 Taler Systems SA + Copyright (C) 2023, 2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -74,6 +74,7 @@ lookup_deposits_by_order_cb (void *cls, char *exchange_url; struct TALER_MerchantWireHashP h_wire; struct TALER_CoinSpendPublicKeyP coin_pub; + struct GNUNET_TIME_Timestamp deposit_timestamp; struct TALER_Amount amount_with_fee; struct TALER_Amount deposit_fee; struct GNUNET_PQ_ResultSpec rs[] = { @@ -81,6 +82,8 @@ lookup_deposits_by_order_cb (void *cls, &deposit_serial), GNUNET_PQ_result_spec_string ("exchange_url", &exchange_url), + GNUNET_PQ_result_spec_timestamp ("deposit_timestamp", + &deposit_timestamp), GNUNET_PQ_result_spec_auto_from_type ("h_wire", &h_wire), TALER_PQ_result_spec_amount_with_currency ("amount_with_fee", @@ -105,6 +108,7 @@ lookup_deposits_by_order_cb (void *cls, deposit_serial, exchange_url, &h_wire, + deposit_timestamp, &amount_with_fee, &deposit_fee, &coin_pub); @@ -139,6 +143,7 @@ TMH_PG_lookup_deposits_by_order (void *cls, " dep.deposit_serial" ",mcon.exchange_url" ",acc.h_wire" + ",mcon.deposit_timestamp" ",dep.amount_with_fee" ",dep.deposit_fee" ",dep.coin_pub" diff --git a/src/backenddb/pg_lookup_refunds.h b/src/backenddb/pg_lookup_refunds.h index 2b047019..20f44e9d 100644 --- a/src/backenddb/pg_lookup_refunds.h +++ b/src/backenddb/pg_lookup_refunds.h @@ -36,11 +36,11 @@ * @return transaction status */ enum GNUNET_DB_QueryStatus -TMH_PG_lookup_refunds (void *cls, - const char *instance_id, - const struct - TALER_PrivateContractHashP *h_contract_terms, - TALER_MERCHANTDB_RefundCallback rc, - void *rc_cls); +TMH_PG_lookup_refunds ( + void *cls, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms, + TALER_MERCHANTDB_RefundCallback rc, + void *rc_cls); #endif diff --git a/src/backenddb/pg_lookup_refunds_detailed.h b/src/backenddb/pg_lookup_refunds_detailed.h index c2531446..665f06cf 100644 --- a/src/backenddb/pg_lookup_refunds_detailed.h +++ b/src/backenddb/pg_lookup_refunds_detailed.h @@ -36,10 +36,11 @@ * @return transaction status */ enum GNUNET_DB_QueryStatus -TMH_PG_lookup_refunds_detailed (void *cls, - const char *instance_id, - const struct TALER_PrivateContractHashP *h_contract_terms, - TALER_MERCHANTDB_RefundDetailCallback rc, - void *rc_cls); +TMH_PG_lookup_refunds_detailed ( + void *cls, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms, + TALER_MERCHANTDB_RefundDetailCallback rc, + void *rc_cls); #endif diff --git a/src/backenddb/pg_lookup_token_family.c b/src/backenddb/pg_lookup_token_family.c index 848b79a9..d2c651c9 100644 --- a/src/backenddb/pg_lookup_token_family.c +++ b/src/backenddb/pg_lookup_token_family.c @@ -105,9 +105,9 @@ TMH_PG_lookup_token_family (void *cls, if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { - if (strcmp(kind, "discount") == 0) + if (0 == strcmp(kind, "discount")) details->kind = TALER_MERCHANTDB_TFK_Discount; - else if (strcmp(kind, "subscription") == 0) + else if (0 == strcmp(kind, "subscription")) details->kind = TALER_MERCHANTDB_TFK_Subscription; else { diff --git a/src/backenddb/pg_lookup_token_family_key.c b/src/backenddb/pg_lookup_token_family_key.c new file mode 100644 index 00000000..f1fa75f3 --- /dev/null +++ b/src/backenddb/pg_lookup_token_family_key.c @@ -0,0 +1,161 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_lookup_token_family_key.c + * @brief Implementation of the lookup_token_family_key function for Postgres + * @author Christian Blättler + */ +#include "platform.h" +#include <gnunet/gnunet_pq_lib.h> +#include <gnunet/gnunet_time_lib.h> +#include <string.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_token_family_key.h" +#include "pg_helper.h" + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_token_family_key (void *cls, + const char *instance_id, + const char *token_family_slug, + struct GNUNET_TIME_Timestamp min_valid_after, + struct GNUNET_TIME_Timestamp max_valid_after, + struct TALER_MERCHANTDB_TokenFamilyKeyDetails *details) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_string (token_family_slug), + GNUNET_PQ_query_param_timestamp (&min_valid_after), + GNUNET_PQ_query_param_timestamp (&max_valid_after), + GNUNET_PQ_query_param_end + }; + + if (NULL == details) + { + struct GNUNET_PQ_ResultSpec rs_null[] = { + GNUNET_PQ_result_spec_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "lookup_token_family_key", + params, + rs_null); + } + else + { + char *kind; + details->pub = NULL; + details->priv = NULL; + details->valid_after = GNUNET_TIME_UNIT_ZERO_TS; + details->valid_before = GNUNET_TIME_UNIT_ZERO_TS; + + struct GNUNET_PQ_ResultSpec rs[] = { + // GNUNET_PQ_result_spec_allow_null ( + // GNUNET_PQ_result_spec_blind_sign_pub ("pub", + // &details->pub->public_key), + // NULL), + // GNUNET_PQ_result_spec_allow_null ( + // GNUNET_PQ_result_spec_blind_sign_priv ("priv", + // &details->priv->private_key), + // NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_timestamp ("key_valid_after", + &details->valid_after), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_timestamp ("key_valid_before", + &details->valid_before), + NULL), + GNUNET_PQ_result_spec_string ("slug", + &details->token_family.slug), + GNUNET_PQ_result_spec_string ("name", + &details->token_family.name), + GNUNET_PQ_result_spec_string ("description", + &details->token_family.description), + TALER_PQ_result_spec_json ("description_i18n", + &details->token_family.description_i18n), + GNUNET_PQ_result_spec_timestamp ("valid_after", + &details->token_family.valid_after), + GNUNET_PQ_result_spec_timestamp ("valid_before", + &details->token_family.valid_before), + GNUNET_PQ_result_spec_relative_time ("duration", + &details->token_family.duration), + GNUNET_PQ_result_spec_string ("kind", + &kind), + GNUNET_PQ_result_spec_uint64 ("issued", + &details->token_family.issued), + GNUNET_PQ_result_spec_uint64 ("redeemed", + &details->token_family.redeemed), + GNUNET_PQ_result_spec_end + }; + + check_connection (pg); + PREPARE (pg, + "lookup_token_family_key", + "SELECT" + " h_pub" + ",pub" + ",priv" + ",cipher" + ",merchant_token_family_keys.valid_after as key_valid_after" + ",merchant_token_family_keys.valid_before as key_valid_before" + ",slug" + ",name" + ",description" + ",description_i18n" + ",merchant_token_families.valid_after" + ",merchant_token_families.valid_before" + ",duration" + ",kind" + ",issued" + ",redeemed" + " FROM merchant_token_families" + " LEFT JOIN merchant_token_family_keys" + " ON merchant_token_families.token_family_serial = merchant_token_family_keys.token_family_serial" + " AND merchant_token_family_keys.valid_after >= $3" + " AND merchant_token_family_keys.valid_after <= $4" + " JOIN merchant_instances" + " USING (merchant_serial)" + " WHERE merchant_instances.merchant_id=$1" + " AND slug=$2" + " LIMIT 1"); + enum GNUNET_DB_QueryStatus qs; + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "lookup_token_family_key", + params, + rs); + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + if (0 == strcmp(kind, "discount")) + details->token_family.kind = TALER_MERCHANTDB_TFK_Discount; + else if (0 == strcmp(kind, "subscription")) + details->token_family.kind = TALER_MERCHANTDB_TFK_Subscription; + else + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + + /* TODO: How to handle multiple results? */ + + return qs; + } +}
\ No newline at end of file diff --git a/src/backenddb/pg_lookup_token_family_key.h b/src/backenddb/pg_lookup_token_family_key.h new file mode 100644 index 00000000..aa7335cf --- /dev/null +++ b/src/backenddb/pg_lookup_token_family_key.h @@ -0,0 +1,50 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_lookup_token_family_key.h + * @brief implementation of the lookup_token_family_key function for Postgres + * @author Christian Blättler + */ +#ifndef PG_LOOKUP_TOKEN_FAMILY_KEY_H +#define PG_LOOKUP_TOKEN_FAMILY_KEY_H + +#include <gnunet/gnunet_common.h> +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Lookup details about a particular token family key. + * + * @param cls closure + * @param instance_id instance to lookup token family key for + * @param token_family_slug slug of token family to lookup + * @param min_valid_after lower bound of the start of the key validation period + * @param max_valid_after upper bound of the start of the key validation period + * @param[out] details set to the token family key details on success, can be NULL + * (in that case we only want to check if the token family key exists) + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_token_family_key (void *cls, + const char *instance_id, + const char *token_family_slug, + struct GNUNET_TIME_Timestamp min_valid_after, + struct GNUNET_TIME_Timestamp max_valid_after, + struct TALER_MERCHANTDB_TokenFamilyKeyDetails *details); + + +#endif diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c index e09b4827..e5f3f2a1 100644 --- a/src/backenddb/plugin_merchantdb_postgres.c +++ b/src/backenddb/plugin_merchantdb_postgres.c @@ -138,6 +138,8 @@ #include "pg_lookup_token_families.h" #include "pg_delete_token_family.h" #include "pg_update_token_family.h" +#include "pg_insert_token_family_key.h" +#include "pg_lookup_token_family_key.h" /** @@ -575,9 +577,14 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_delete_token_family; plugin->update_token_family = &TMH_PG_update_token_family; + plugin->insert_token_family_key + = &TMH_PG_insert_token_family_key; + plugin->lookup_token_family_key + = &TMH_PG_lookup_token_family_key; plugin->update_deposit_confirmation_status = &TMH_PG_update_deposit_confirmation_status; + return plugin; } diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c index 1a4c15db..fbb662f8 100644 --- a/src/backenddb/test_merchantdb.c +++ b/src/backenddb/test_merchantdb.c @@ -42,28 +42,28 @@ static struct TALER_MERCHANTDB_Plugin *plugin; * @param test 0 on success, non-zero on failure */ #define TEST_WITH_FAIL_CLAUSE(test, on_fail) \ - if ((test)) \ - { \ - GNUNET_break (0); \ - on_fail \ - } + if ((test)) \ + { \ + GNUNET_break (0); \ + on_fail \ + } #define TEST_COND_RET_ON_FAIL(cond, msg) \ - if (! (cond)) \ - { \ - GNUNET_break (0); \ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ - msg); \ - return 1; \ - } + if (! (cond)) \ + { \ + GNUNET_break (0); \ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ + msg); \ + return 1; \ + } /** * @param __test 0 on success, non-zero on failure */ #define TEST_RET_ON_FAIL(__test) \ - TEST_WITH_FAIL_CLAUSE (__test, \ - return 1; \ - ) + TEST_WITH_FAIL_CLAUSE (__test, \ + return 1; \ + ) /* ********** Instances ********** */ @@ -1136,7 +1136,8 @@ run_test_products (struct TestProducts_Closure *cls) stock_dec.product.total_stock = 40; TEST_RET_ON_FAIL (test_update_product (&cls->instance, &stock_dec, - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)) + ; } { struct ProductData lost_dec = cls->products[0]; @@ -1144,7 +1145,8 @@ run_test_products (struct TestProducts_Closure *cls) lost_dec.product.total_lost = 1; TEST_RET_ON_FAIL (test_update_product (&cls->instance, &lost_dec, - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)) + ; } TEST_RET_ON_FAIL (test_lookup_product (&cls->instance, &cls->products[0])); @@ -2195,14 +2197,16 @@ run_test_orders (struct TestOrders_Closure *cls) cls->orders)); /* Test marking orders as wired */ TEST_RET_ON_FAIL (test_mark_order_wired (serial, - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)) + ; TEST_RET_ON_FAIL (test_lookup_payment_status (cls->instance.instance.id, cls->orders[0].id, NULL, true, true)); TEST_RET_ON_FAIL (test_mark_order_wired (1007, - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)) + ; /* If an order has been claimed and we aren't past the pay deadline, we can't delete it. */ TEST_RET_ON_FAIL (test_delete_order (&cls->instance, @@ -2478,7 +2482,8 @@ test_insert_exchange_signkey (const struct ExchangeSignkeyData *signkey, TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_exchange_signkey (plugin->cls, &signkey->master_pub, - &signkey->exchange_pub, + &signkey->exchange_pub + , signkey->start_date, signkey->expire_date, signkey->end_date, @@ -2806,8 +2811,8 @@ test_lookup_deposits_contract_and_coin ( * @param cls pointer to the test lookup closure. * @param deposit_serial row number of the deposit in the database. * @param exchange_url URL to the exchange - * @param amount_with_fee amount of the deposit with fees. * @param h_wire hash of the wire transfer details. + * @param deposit_timestamp when was the deposit made * @param amount_with_fee amount of the deposit with fees. * @param deposit_fee fee charged for the deposit. * @param coin_pub public key of the coin deposited. @@ -2817,11 +2822,13 @@ lookup_deposits_order_cb (void *cls, uint64_t deposit_serial, const char *exchange_url, const struct TALER_MerchantWireHashP *h_wire, + struct GNUNET_TIME_Timestamp deposit_timestamp, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub) { struct TestLookupDeposits_Closure *cmp = cls; + if (NULL == cmp) return; cmp->results_length += 1; @@ -2871,11 +2878,11 @@ test_lookup_deposits_by_order (uint64_t order_serial, memset (results_matching, 0, sizeof (unsigned int) * deposits_length); - if (deposits_length != plugin->lookup_deposits_by_order (plugin->cls, - order_serial, - & - lookup_deposits_order_cb, - &cmp)) + if (deposits_length != + plugin->lookup_deposits_by_order (plugin->cls, + order_serial, + &lookup_deposits_order_cb, + &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup deposits by order failed\n"); @@ -2923,8 +2930,8 @@ struct LookupDepositSerial_Closure * @param cls pointer to the test lookup closure. * @param deposit_serial row number of the deposit in the database. * @param exchange_url URL to the exchange - * @param amount_with_fee amount of the deposit with fees. * @param h_wire hash of the wire transfer details. + * @param deposit_timestamp when was the deposit made. * @param amount_with_fee amount of the deposit with fees. * @param deposit_fee fee charged for the deposit. * @param coin_pub public key of the coin deposited. @@ -2934,11 +2941,14 @@ get_deposit_serial_cb (void *cls, uint64_t deposit_serial, const char *exchange_url, const struct TALER_MerchantWireHashP *h_wire, + struct GNUNET_TIME_Timestamp deposit_timestamp, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub) { struct LookupDepositSerial_Closure *lookup_cls = cls; + + (void) deposit_timestamp; if (NULL == lookup_cls) return; if ((0 == strcmp (lookup_cls->deposit->exchange_url, @@ -4090,7 +4100,8 @@ test_insert_transfer_details ( TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_transfer_details (plugin->cls, instance->instance.id, - transfer->exchange_url, + transfer->exchange_url + , account->payto_uri, &transfer->wtid, &transfer->data), @@ -4458,7 +4469,8 @@ test_lookup_refunds (const struct InstanceData *instance, if (refunds_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Lookup refunds failed: incorrect number of results returned\n"); + "Lookup refunds failed: incorrect number of results returned\n") + ; return 1; } for (unsigned int i = 0; refunds_length > i; ++i) @@ -4666,7 +4678,8 @@ test_lookup_refunds_detailed ( if (refunds_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Lookup refunds detailed failed: incorrect number of results\n"); + "Lookup refunds detailed failed: incorrect number of results\n") + ; return 1; } for (unsigned int i = 0; refunds_length > i; ++i) @@ -4980,7 +4993,8 @@ run_test_refunds (struct TestRefunds_Closure *cls) TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->refund_coin (plugin->cls, cls->instance.instance.id, - &cls->deposits[0].h_contract_terms, + &cls->deposits[0].h_contract_terms + , cls->refunds[0].timestamp, cls->refunds[0].coin_pub, cls->refunds[0].reason), @@ -4992,7 +5006,8 @@ run_test_refunds (struct TestRefunds_Closure *cls) TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->refund_coin (plugin->cls, cls->instance.instance.id, - &cls->deposits[0].h_contract_terms, + &cls->deposits[0].h_contract_terms + , cls->refunds[0].timestamp, cls->refunds[0].coin_pub, cls->refunds[0].reason), @@ -5016,14 +5031,16 @@ run_test_refunds (struct TestRefunds_Closure *cls) refund_serial, &cls->refund_proof. exchange_sig, - &cls->signkey.exchange_pub), + &cls->signkey.exchange_pub + ), "Insert refund proof failed\n"); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->insert_refund_proof (plugin->cls, refund_serial, &cls->refund_proof. exchange_sig, - &cls->signkey.exchange_pub), + &cls->signkey.exchange_pub + ), "Insert refund proof failed\n"); /* Test that we can't give too much in refunds */ GNUNET_assert (GNUNET_OK == @@ -6568,7 +6585,8 @@ test_insert_pending_webhook (const struct InstanceData *instance, http_method, pwebhook->pwebhook. header, - pwebhook->pwebhook.body), + pwebhook->pwebhook.body + ), "Insert pending webhook failed\n"); return 0; } @@ -7020,7 +7038,8 @@ run_test_pending_webhooks (struct TestPendingWebhooks_Closure *cls) &cls->pwebhooks[1])); TEST_RET_ON_FAIL (test_update_pending_webhook (&cls->instance, &cls->pwebhooks[1], - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); // ??? + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + // ??? TEST_RET_ON_FAIL (test_lookup_all_webhooks (&cls->instance, 2, cls->pwebhooks)); diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h index 263a6fec..057c9eff 100644 --- a/src/include/taler_merchant_service.h +++ b/src/include/taler_merchant_service.h @@ -34,7 +34,7 @@ /** * Library version (in hex) for compatibility tests. */ -#define TALER_MERCHANT_SERVICE_VERSION 0x00090403 +#define TALER_MERCHANT_SERVICE_VERSION 0x00100000 /** @@ -949,7 +949,7 @@ TALER_MERCHANT_instance_delete_cancel ( * @param arg request to cancel. */ #define TALER_MERCHANT_instance_purge_cancel(arg) \ - TALER_MERCHANT_instance_delete_cancel (arg) + TALER_MERCHANT_instance_delete_cancel (arg) /* *************** Accounts **************** */ @@ -1895,6 +1895,172 @@ TALER_MERCHANT_product_delete_cancel ( struct TALER_MERCHANT_ProductDeleteHandle *pdh); +/* ********************* /tokenfamilies ************************** */ + +/** + * Handle for a GET /tokenfamilies/$SLUG operation. + */ +struct TALER_MERCHANT_TokenFamilyGetHandle; + + +/** + * Response to GET /tokenfamilies/$SLUG operation. + */ +struct TALER_MERCHANT_TokenFamilyGetResponse +{ + /** + * HTTP response details + */ + struct TALER_MERCHANT_HttpResponse hr; + + /** + * Details depending on HTTP status. + */ + union + { + /** + * Details for #MHD_HTTP_OK. + */ + struct + { + + /** + * Identifier for the token family consisting of unreserved characters + * according to RFC 3986. + */ + const char *slug; + + /** + * Human-readable name for the token family. + */ + const char *name; + + /** + * description of the token family + */ + const char *description; + + /** + * Optional map from IETF BCP 47 language tags to localized descriptions. + */ + const json_t *description_i18n; + + /** + * Start time of the token family's validity period. + */ + struct GNUNET_TIME_Timestamp valid_after; + + /** + * End time of the token family's validity period. + */ + struct GNUNET_TIME_Timestamp valid_before; + + /** + * Validity duration of an issued token. + */ + struct GNUNET_TIME_Relative duration; + + /** + * Kind of token family, "subscription" or "discount". + */ + const char *kind; + + /** + * How many tokens have been issued for this family. + */ + uint64_t issued; + + /** + * How many tokens have been redeemed for this family. + */ + uint64_t redeemed; + } ok; + + } details; + +}; + +/** + * Cancel GET /tokenfamilies/$SLUG operation. + * + * @param handle operation to cancel + */ +void +TALER_MERCHANT_token_family_get_cancel ( + struct TALER_MERCHANT_TokenFamilyGetHandle *handle); + + +/** + * Function called with the result of the GET /tokenfamilies/$SLUG operation. + * + * @param cls closure + * @param pgr response details + */ +typedef void +(*TALER_MERCHANT_TokenFamilyGetCallback)( + void *cls, + const struct TALER_MERCHANT_TokenFamilyGetResponse *pgr); + +/** + * Handle for a POST /tokenfamilies operation. + */ +struct TALER_MERCHANT_TokenFamiliesPostHandle; + + +/** + * Function called with the result of the POST /tokenfamilies operation. + * + * @param cls closure + * @param hr HTTP response details + */ +typedef void +(*TALER_MERCHANT_TokenFamiliesPostCallback)( + void *cls, + const struct TALER_MERCHANT_HttpResponse *hr); + + +/** + * Make a POST /tokenfamilies request to add a token family to the + * merchant instance. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend + * @param slug short, url-safe identifier for the token family + * @param name human-readable name for the token family + * @param description description of the token family + * @param description_i18n Map from IETF BCP 47 language tags to localized descriptions + * @param valid_after when the token family becomes valid + * @param valid_before when the token family expires + * @param duration how long tokens issued by this token family are valid for + * @param kind kind of token family, "subscription" or "discount" + * @param cb function to call with the backend's result + * @param cb_cls closure for @a cb + * @return the request handle; NULL upon error + */ +struct TALER_MERCHANT_TokenFamiliesPostHandle * +TALER_MERCHANT_token_families_post ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *slug, + const char *name, + const char *description, + const json_t *description_i18n, + struct GNUNET_TIME_Timestamp valid_after, + struct GNUNET_TIME_Timestamp valid_before, + struct GNUNET_TIME_Relative duration, + const char *kind, + TALER_MERCHANT_TokenFamiliesPostCallback cb, + void *cb_cls); + +/** + * Cancel POST /tokenfamilies operation. + * + * @param handle operation to cancel + */ +void +TALER_MERCHANT_token_families_post_cancel ( + struct TALER_MERCHANT_TokenFamiliesPostHandle *handle); + /* ********************* /orders ************************** */ @@ -2664,6 +2830,12 @@ struct TALER_MERCHANT_OrderStatusResponse */ bool wired; + /** + * Time of the last payment made on this order. + * Only available if the server supports protocol + * **v14** or higher, otherwise zero. + */ + struct GNUNET_TIME_Timestamp last_payment; } paid; /** diff --git a/src/include/taler_merchant_testing_lib.h b/src/include/taler_merchant_testing_lib.h index b1de5292..47d081fc 100644 --- a/src/include/taler_merchant_testing_lib.h +++ b/src/include/taler_merchant_testing_lib.h @@ -27,6 +27,7 @@ #ifndef TALER_MERCHANT_TESTING_LIB_H #define TALER_MERCHANT_TESTING_LIB_H +#include <gnunet/gnunet_time_lib.h> #include <taler/taler_testing_lib.h> #include "taler_merchant_service.h" @@ -605,6 +606,35 @@ TALER_TESTING_cmd_merchant_post_orders3 ( /** + * Create an order with a choices array with input and output tokens. + * + * @param label command label + * @param cfg configuration to use + * @param merchant_url base URL of the merchant serving + * the proposal request. + * @param http_status expected HTTP status. + * @param token_family_reference label of the POST /tokenfamilies cmd. + * @param order_id the name of the order to add. + * @param refund_deadline the deadline for refunds on this order. + * @param pay_deadline the deadline for payment on this order. + * @param amount the amount this order is for. + * @return the command + */ + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_orders_choices ( + const char *label, + const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *merchant_url, + unsigned int http_status, + const char *token_family_reference, + const char *order_id, + struct GNUNET_TIME_Timestamp refund_deadline, + struct GNUNET_TIME_Timestamp pay_deadline, + const char *amount); + + +/** * Define a "GET /orders" CMD. * * @param label command label. @@ -1473,6 +1503,40 @@ TALER_TESTING_cmd_merchant_post_using_templates ( unsigned int http_status); +/* ****** Token Families ******* */ + + +/** + * Define a "POST /tokenfamilies" CMD. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the + * POST /tokenfamilies request. + * @param http_status expected HTTP response code. + * @param slug slug of the token family. + * @param name name of the token family. + * @param description description of the token family. + * @param description_i18n internationalized description of the token family. + * @param valid_after start of the validity time of the token family. + * @param valid_before end of the validity time of the token family. + * @param duration validity duration of an issued token of the token family. + * @param kind kind of the token family. either "subscription" or "discount". + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_tokenfamilies ( + const char *label, + const char *merchant_url, + unsigned int http_status, + const char *slug, + const char *name, + const char *description, + json_t *description_i18n, + struct GNUNET_TIME_Timestamp valid_after, + struct GNUNET_TIME_Timestamp valid_before, + struct GNUNET_TIME_Relative duration, + const char *kind); + /* ****** Webhooks ******* */ @@ -1737,7 +1801,10 @@ TALER_TESTING_cmd_checkserver2 (const char *label, op (http_method, const char) \ op (header_template, const char) \ op (body_template, const char) \ - op (summary, const char) + op (summary, const char) \ + op (token_family_slug, const char) \ + op (token_family_duration, const struct GNUNET_TIME_Relative) \ + op (token_family_kind, const char) /** diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h index e7eb2d0f..44fdc0ab 100644 --- a/src/include/taler_merchantdb_plugin.h +++ b/src/include/taler_merchantdb_plugin.h @@ -23,6 +23,8 @@ #ifndef TALER_MERCHANTDB_PLUGIN_H #define TALER_MERCHANTDB_PLUGIN_H +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_time_lib.h> #include <gnunet/gnunet_util_lib.h> #include <gnunet/gnunet_db_lib.h> #include <taler/taler_exchange_service.h> @@ -932,9 +934,10 @@ typedef void * @param cls closure * @param deposit_serial which deposit operation is this about * @param exchange_url URL of the exchange that issued the coin + * @param h_wire hash of merchant's wire details + * @param deposit_timestamp when was the deposit made * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin - * @param h_wire hash of merchant's wire details * @param coin_pub public key of the coin */ typedef void @@ -943,6 +946,7 @@ typedef void uint64_t deposit_serial, const char *exchange_url, const struct TALER_MerchantWireHashP *h_wire, + struct GNUNET_TIME_Timestamp deposit_timestamp, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub); @@ -1092,17 +1096,17 @@ struct TALER_MERCHANTDB_TokenFamilyKeyDetails /** * Token family public key. */ - struct TALER_TokenFamilyPublicKey pub; + struct TALER_TokenFamilyPublicKey *pub; /** - * Hash of the token family public key. + * Token family private key. */ - struct TALER_TokenFamilyPublicKeyHash pub_h; + struct TALER_TokenFamilyPrivateKey *priv; /** - * Token family private key. - */ - struct TALER_TokenFamilyPrivateKey priv; + * Details about the token family this key belongs to. + */ + struct TALER_MERCHANTDB_TokenFamilyDetails token_family; }; /** @@ -3238,7 +3242,7 @@ struct TALER_MERCHANTDB_Plugin * Insert details about a particular token family. * * @param cls closure - * @param instance_id instance to insert product for + * @param instance_id instance to insert token family for * @param token_family_slug slug of token family to insert * @param details the token family details to insert * @return database result code @@ -3250,6 +3254,49 @@ struct TALER_MERCHANTDB_Plugin const char *token_family_slug, const struct TALER_MERCHANTDB_TokenFamilyDetails *details); + + /** + * Lookup details about a particular token family key. + * + * @param cls closure + * @param instance_id instance to lookup token family key for + * @param token_family_slug slug of token family to lookup + * @param min_valid_after lower bound of the start of the key validation period + * @param max_valid_after upper bound of the start of the key validation period + * @param[out] details set to the token family key details on success, can be NULL + * (in that case we only want to check if the token family key exists) + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*lookup_token_family_key) ( + void *cls, + const char *instance_id, + const char *token_family_slug, + struct GNUNET_TIME_Timestamp min_valid_after, + struct GNUNET_TIME_Timestamp max_valid_after, + struct TALER_MERCHANTDB_TokenFamilyKeyDetails *details); + + + /** + * Insert details a key pair for a token family. + * + * @param cls closure + * @param token_family_slug slug of token family to insert the key pair for + * @param pub token family public key + * @param priv token family private key + * @param valid_after start of the key validation period + * @param valid_before end of the key validation period + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*insert_token_family_key)( + void *cls, + const char *token_family_slug, + const struct TALER_TokenFamilyPublicKey *pub, + const struct TALER_TokenFamilyPrivateKey *priv, + struct GNUNET_TIME_Timestamp valid_after, + struct GNUNET_TIME_Timestamp valid_before); + /** * Lookup deposits that are finished and awaiting a wire transfer. * diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 04b2b089..1e7430d4 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -62,6 +62,7 @@ libtalermerchant_la_SOURCES = \ merchant_api_post_products.c \ merchant_api_post_transfers.c \ merchant_api_post_templates.c \ + merchant_api_post_tokenfamilies.c \ merchant_api_post_using_templates.c \ merchant_api_post_webhooks.c \ merchant_api_wallet_get_order.c \ diff --git a/src/lib/merchant_api_get_config.c b/src/lib/merchant_api_get_config.c index ddbc20a3..b4b700bd 100644 --- a/src/lib/merchant_api_get_config.c +++ b/src/lib/merchant_api_get_config.c @@ -34,12 +34,12 @@ * Which version of the Taler protocol is implemented * by this library? Used to determine compatibility. */ -#define MERCHANT_PROTOCOL_CURRENT 13 +#define MERCHANT_PROTOCOL_CURRENT 14 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 1 +#define MERCHANT_PROTOCOL_AGE 2 /** * How many exchanges do we allow at most per merchant? diff --git a/src/lib/merchant_api_get_tokenfamily.c b/src/lib/merchant_api_get_tokenfamily.c new file mode 100644 index 00000000..d7e6b06e --- /dev/null +++ b/src/lib/merchant_api_get_tokenfamily.c @@ -0,0 +1,210 @@ +/* + This file is part of TALER + Copyright (C) 2014-2023 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 2.1, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with + TALER; see the file COPYING.LGPL. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant_api_get_tokenfamily.c + * @brief Implementation of the GET /tokenfamily/$ID request of the merchant's HTTP API + * @author Christian Blättler + */ +#include "platform.h" +#include <curl/curl.h> +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_merchant_service.h" +#include "merchant_api_curl_defaults.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_signatures.h> + + +/** + * Handle for a GET /tokenfamilies/$SLUG operation. + */ +struct TALER_MERCHANT_TokenFamilyGetHandle +{ + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_TokenFamilyGetCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + +}; + + +/** + * Function called when we're done processing the + * HTTP GET /tokenfamilies/$ID request. + * + * @param cls the `struct TALER_MERCHANT_TokenFamilyGetHandle` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_get_token_family_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_MERCHANT_TokenFamilyGetHandle *handle = cls; + const json_t *json = response; + struct TALER_MERCHANT_TokenFamilyGetResponse res = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + handle->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Got /tokenfamilies/$ID response with status code %u\n", + (unsigned int) response_code); + switch (response_code) + { + case MHD_HTTP_OK: + { + // Parse token family response + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("slug", + &res.details.ok.slug), + GNUNET_JSON_spec_string ("name", + &res.details.ok.name), + GNUNET_JSON_spec_string ("description", + &res.details.ok.description), + GNUNET_JSON_spec_object_const ("description_i18n", + &res.details.ok.description_i18n), + GNUNET_JSON_spec_timestamp ("valid_after", + &res.details.ok.valid_after), + GNUNET_JSON_spec_timestamp ("valid_before", + &res.details.ok.valid_before), + GNUNET_JSON_spec_relative_time ("duation", + &res.details.ok.duration), + GNUNET_JSON_spec_string ("kind", + &res.details.ok.kind), + GNUNET_JSON_spec_uint64 ("issued", + &res.details.ok.issued), + GNUNET_JSON_spec_uint64 ("redeemed", + &res.details.ok.redeemed), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK == + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + handle->cb (handle->cb_cls, + &res); + GNUNET_JSON_parse_free (spec); + TALER_MERCHANT_token_family_get_cancel (handle); + return; + } + res.hr.http_status = 0; + res.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + case MHD_HTTP_UNAUTHORIZED: + res.hr.ec = TALER_JSON_get_error_code (json); + res.hr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, merchant says we need to authenticate. */ + break; + case MHD_HTTP_NOT_FOUND: + res.hr.ec = TALER_JSON_get_error_code (json); + res.hr.hint = TALER_JSON_get_error_hint (json); + break; + default: + /* unexpected response code */ + res.hr.ec = TALER_JSON_get_error_code (json); + res.hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d\n", + (unsigned int) response_code, + (int) res.hr.ec); + break; + } +} + +struct TALER_MERCHANT_TokenFamilyGetHandle * +TALER_MERCHANT_token_family_get ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *token_family_slug, + TALER_MERCHANT_TokenFamilyGetCallback cb, + void *cb_cls) +{ + struct TALER_MERCHANT_TokenFamilyGetHandle *handle; + CURL *eh; + + handle = GNUNET_new (struct TALER_MERCHANT_TokenFamilyGetHandle); + handle->ctx = ctx; + handle->cb = cb; + handle->cb_cls = cb_cls; + { + char *path; + + GNUNET_asprintf (&path, + "private/tokenfamilies/%s", + token_family_slug); + handle->url = TALER_url_join (backend_url, + path, + NULL); + GNUNET_free (path); + } + if (NULL == handle->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (handle); + return NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + handle->url); + eh = TALER_MERCHANT_curl_easy_get_ (handle->url); + handle->job = GNUNET_CURL_job_add (ctx, + eh, + &handle_get_token_family_finished, + handle); + return handle; +} + +void +TALER_MERCHANT_token_family_get_cancel ( + struct TALER_MERCHANT_TokenFamilyGetHandle *handle) +{ + if (NULL != handle->job) + GNUNET_CURL_job_cancel (handle->job); + GNUNET_free (handle->url); + GNUNET_free (handle); +}
\ No newline at end of file diff --git a/src/lib/merchant_api_merchant_get_order.c b/src/lib/merchant_api_merchant_get_order.c index 3a49db34..3bd4003b 100644 --- a/src/lib/merchant_api_merchant_get_order.c +++ b/src/lib/merchant_api_merchant_get_order.c @@ -202,6 +202,11 @@ handle_paid (struct TALER_MERCHANT_OrderMerchantGetHandle *omgh, &wire_details), GNUNET_JSON_spec_array_const ("refund_details", &refund_details), + /* Only available since **v14** */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("last_payment", + &osr->details.ok.details.paid.last_payment), + NULL), GNUNET_JSON_spec_end () }; diff --git a/src/lib/merchant_api_post_tokenfamilies.c b/src/lib/merchant_api_post_tokenfamilies.c new file mode 100644 index 00000000..0c5e18c2 --- /dev/null +++ b/src/lib/merchant_api_post_tokenfamilies.c @@ -0,0 +1,246 @@ +/* + This file is part of TALER + Copyright (C) 2020-2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with TALER; see the file COPYING.LGPL. + If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant_api_post_tokenfamilies.c + * @brief Implementation of the POST /tokenfamilies request + * of the merchant's HTTP API + * @author Christian Blättler + */ +#include "platform.h" +#include <curl/curl.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_time_lib.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include "taler_merchant_service.h" +#include "merchant_api_curl_defaults.h" +#include "merchant_api_common.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_curl_lib.h> + + +/** + * Handle for a POST /tokenfamilies operation. + */ +struct TALER_MERCHANT_TokenFamiliesPostHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_TokenFamiliesPostCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + +}; + +/** + * Function called when we're done processing the + * HTTP POST /tokenfamilies request. + * + * @param cls the `struct TALER_MERCHANT_TokenFamiliesPostHandle` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_post_token_families_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_MERCHANT_TokenFamiliesPostHandle *handle = cls; + const json_t *json = response; + struct TALER_MERCHANT_HttpResponse hr = { + .http_status = (unsigned int) response_code, + .reply = json + }; + + handle->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "POST /tokenfamilies completed with response code %u\n", + (unsigned int) response_code); + switch (response_code) + { + case 0: + hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_BAD_REQUEST: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + /* This should never happen, either us + * or the merchant is buggy (or API version conflict); + * just pass JSON reply to the application */ + break; + case MHD_HTTP_UNAUTHORIZED: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, merchant says we need to authenticate. */ + break; + case MHD_HTTP_FORBIDDEN: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, merchant says we tried to abort the payment + * after it was successful. We should pass the JSON reply to the + * application */ + break; + case MHD_HTTP_NOT_FOUND: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the + application */ + break; + case MHD_HTTP_CONFLICT: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + /* Server had an internal issue; we should retry, + but this API leaves this to the application */ + break; + default: + TALER_MERCHANT_parse_error_details_ (json, + response_code, + &hr); + /* unexpected response code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d\n", + (unsigned int) response_code, + (int) hr.ec); + GNUNET_break_op (0); + break; + } + handle->cb (handle->cb_cls, + &hr); + TALER_MERCHANT_token_families_post_cancel (handle); +} + +struct TALER_MERCHANT_TokenFamiliesPostHandle * +TALER_MERCHANT_token_families_post ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *slug, + const char *name, + const char *description, + const json_t *description_i18n, + struct GNUNET_TIME_Timestamp valid_after, + struct GNUNET_TIME_Timestamp valid_before, + struct GNUNET_TIME_Relative duration, + const char *kind, + TALER_MERCHANT_TokenFamiliesPostCallback cb, + void *cb_cls) +{ + struct TALER_MERCHANT_TokenFamiliesPostHandle *handle; + json_t *req_obj; + + req_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("slug", + slug), + GNUNET_JSON_pack_string ("name", + name), + GNUNET_JSON_pack_string ("description", + description), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("description_i18n", + (json_t *) description_i18n)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("valid_after", + valid_after)), + GNUNET_JSON_pack_timestamp ("valid_before", + valid_before), + GNUNET_JSON_pack_time_rel ("duration", + duration), + GNUNET_JSON_pack_string ("kind", + kind)); + handle = GNUNET_new (struct TALER_MERCHANT_TokenFamiliesPostHandle); + handle->ctx = ctx; + handle->cb = cb; + handle->cb_cls = cb_cls; + handle->url = TALER_url_join (backend_url, + "private/tokenfamilies", + NULL); + if (NULL == handle->url) + { + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + json_decref (req_obj); + GNUNET_free (handle); + return NULL; + } + { + CURL *eh; + + eh = TALER_MERCHANT_curl_easy_get_ (handle->url); + GNUNET_assert (GNUNET_OK == + TALER_curl_easy_post (&handle->post_ctx, + eh, + req_obj)); + json_decref (req_obj); + handle->job = GNUNET_CURL_job_add2 (ctx, + eh, + handle->post_ctx.headers, + &handle_post_token_families_finished, + handle); + GNUNET_assert (NULL != handle->job); + } + return handle; +} + + +void +TALER_MERCHANT_token_families_post_cancel ( + struct TALER_MERCHANT_TokenFamiliesPostHandle *pph) +{ + if (NULL != pph->job) + { + GNUNET_CURL_job_cancel (pph->job); + pph->job = NULL; + } + TALER_curl_easy_post_finished (&pph->post_ctx); + GNUNET_free (pph->url); + GNUNET_free (pph); +} diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am index 49eb8cc8..67bbbced 100644 --- a/src/testing/Makefile.am +++ b/src/testing/Makefile.am @@ -71,6 +71,7 @@ libtalermerchanttesting_la_SOURCES = \ testing_api_cmd_post_products.c \ testing_api_cmd_post_transfers.c \ testing_api_cmd_post_templates.c \ + testing_api_cmd_post_tokenfamilies.c \ testing_api_cmd_post_using_templates.c \ testing_api_cmd_post_webhooks.c \ testing_api_cmd_refund_order.c \ diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c index ed07bce6..3f9136bc 100644 --- a/src/testing/test_merchant_api.c +++ b/src/testing/test_merchant_api.c @@ -24,6 +24,7 @@ * @author Marcello Stanisci */ #include "platform.h" +#include <gnunet/gnunet_time_lib.h> #include <taler/taler_util.h> #include <taler/taler_signatures.h> #include <taler/taler_exchange_service.h> @@ -1656,6 +1657,55 @@ run (void *cls, TALER_TESTING_cmd_end () }; + struct TALER_TESTING_Command tokens[] = { + /** + * Move money to the exchange's bank account. + */ + cmd_transfer_to_exchange ("create-reserve-tokens", + "EUR:10.02"), + /** + * Make a reserve exist, according to the previous transfer. + */ + cmd_exec_wirewatch ("wirewatch-1"), + TALER_TESTING_cmd_check_bank_admin_transfer ("check_bank_transfer-tokens", + "EUR:10.02", + payer_payto, + exchange_payto, + "create-reserve-tokens"), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-1", + "create-reserve-tokens", + "EUR:5", + 0, + MHD_HTTP_OK), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-2", + "create-reserve-tokens", + "EUR:5", + 0, + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_post_tokenfamilies ("create-tokenfamily", + merchant_url, + MHD_HTTP_NO_CONTENT, + "subscription-1", + "Subscription", + "A subscription.", + NULL, + GNUNET_TIME_timestamp_get (), + GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_YEARS), + GNUNET_TIME_UNIT_MONTHS, + "subscription"), + TALER_TESTING_cmd_merchant_post_orders_choices ("create-order-with-choices", + cred.cfg, + merchant_url, + MHD_HTTP_OK, + "create-tokenfamily", + "5-choices", + GNUNET_TIME_UNIT_ZERO_TS, + GNUNET_TIME_UNIT_FOREVER_TS, + "EUR:5.0"), + + TALER_TESTING_cmd_end () + }; + struct TALER_TESTING_Command commands[] = { /* general setup */ TALER_TESTING_cmd_run_fakebank ( @@ -1978,6 +2028,8 @@ run (void *cls, auth), TALER_TESTING_cmd_batch ("repurchase", repurchase), + TALER_TESTING_cmd_batch ("tokens", + tokens), /** * End the suite. */ diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh index 175667b9..2336ad4e 100755 --- a/src/testing/test_merchant_order_creation.sh +++ b/src/testing/test_merchant_order_creation.sh @@ -244,6 +244,76 @@ fi echo "OK" # +# CREATE TOKEN FAMILY AND V1 ORDER WITH CHOICES +# +echo -n "Creating token family ..." +NOW=$(date +%s) +IN_A_YEAR=$((NOW + 31536000)) +STATUS=$(curl 'http://localhost:9966/private/tokenfamilies' \ + -d '{"slug":"test-sub","kind":"subscription","description":"Test token family","name":"Test Subscription","valid_after":{"t_s":'$NOW'},"valid_before":{"t_s":'$IN_A_YEAR'},"duration": {"d_us": 2592000000}}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "204" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 204, token family created. got: $STATUS" +fi + +echo " OK" + +echo -n "Creating v1 order with token family ..." +STATUS=$(curl 'http://localhost:9966/private/orders' \ + -d '{"order":{"version":"1","amount":"TESTKUDOS:7","summary":"with_subscription","fulfillment_message":"Paid successfully","choices":[{"inputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}],"outputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}]}]}}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, order created. got: $STATUS" +fi + +echo " OK" + +echo -n "Claming order with token family ..." + +ORDER_ID=$(jq -r .order_id < "$LAST_RESPONSE") +TOKEN=$(jq -r .token < "$LAST_RESPONSE") + +STATUS=$(curl http://localhost:9966/orders/"$ORDER_ID"/claim \ + -d '{"nonce":"","token":"'"$TOKEN"'"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, order claimed. got: $STATUS" +fi + +echo " OK" + +# echo -n "Fetching pay URL for order ..." +# STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \ +# -w "%{http_code}" -s -o "$LAST_RESPONSE") + +# if [ "$STATUS" != "200" ] +# then +# jq . < "$LAST_RESPONSE" +# exit_fail "Expected 200, getting order info before claming it. got: $STATUS" +# fi + +# PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE") + +# echo " OK" + +# NOW=$(date +%s) + +# echo -n "Pay for order ${PAY_URL} ..." +# taler-wallet-cli --no-throttle --wallet-db="$WALLET_DB" handle-uri "${PAY_URL}" -y 2> wallet-pay1.err > wallet-pay1.log +# taler-wallet-cli --no-throttle --wallet-db="$WALLET_DB" run-until-done 2> wallet-finish-pay1.err > wallet-finish-pay1.log +# NOW2=$(date +%s) +# echo " OK (took $(( NOW2 - NOW )) secs )" + +# # CREATE ORDER WITH NON-INVENTORY AND CHECK # diff --git a/src/testing/testing_api_cmd_post_orders.c b/src/testing/testing_api_cmd_post_orders.c index d5cfdddc..8f7bd46d 100644 --- a/src/testing/testing_api_cmd_post_orders.c +++ b/src/testing/testing_api_cmd_post_orders.c @@ -24,6 +24,10 @@ */ #include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_time_lib.h> +#include <jansson.h> +#include <stdint.h> #include <taler/taler_exchange_service.h> #include <taler/taler_testing_lib.h> #include "taler_merchant_service.h" @@ -56,6 +60,11 @@ struct OrdersState const char *expected_order_id; /** + * Reference to a POST /tokenfamilies command. Can be NULL. + */ + const char *token_family_reference; + + /** * Contract terms obtained from the backend. */ json_t *contract_terms; @@ -66,6 +75,11 @@ struct OrdersState json_t *order_terms; /** + * Choices array with inputs and outputs for v1 order. + */ + json_t *choices; + + /** * Contract terms hash code. */ struct TALER_PrivateContractHashP h_contract_terms; @@ -563,6 +577,118 @@ orders_run2 (void *cls, /** + * Constructs the json for a the choices of an order request. + * + * @param slug the name of the order to add, can be NULL. + * @param valid_after valid_after date for the input and output token. + * @param[out] choices where to write the json string. + */ +static void +make_choices_json ( + const char *input_slug, + const char *output_slug, + uint16_t input_count, + uint16_t output_count, + struct GNUNET_TIME_Timestamp input_valid_after, + struct GNUNET_TIME_Timestamp output_valid_after, + json_t **choices) +{ + json_t *c; + + c = json_pack("[{s:o, s:o}]", + "inputs", json_pack("[{s:s, s:i, s:s, s:o}]", + "kind", "token", + "count", input_count, + "token_family_slug", input_slug, + "valid_after", GNUNET_JSON_from_timestamp(input_valid_after)), + "outputs", json_pack("[{s:s, s:i, s:s, s:o}]", + "kind", "token", + "count", output_count, + "token_family_slug", output_slug, + "valid_after", GNUNET_JSON_from_timestamp(output_valid_after))); + + *choices = c; +} + + +/** + * Run a "orders" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +orders_run3 (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct OrdersState *ps = cls; + struct GNUNET_TIME_Absolute now; + const char *slug; + + ps->is = is; + now = GNUNET_TIME_absolute_get_monotonic (ps->cfg); + if (NULL == json_object_get (ps->order_terms, + "order_id")) + { + char *order_id; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &now, + sizeof (now)); + GNUNET_assert (0 == + json_object_set_new (ps->order_terms, + "order_id", + json_string (order_id))); + GNUNET_free (order_id); + } + + { + const struct TALER_TESTING_Command *token_family_cmd; + token_family_cmd = + TALER_TESTING_interpreter_lookup_command (is, + ps->token_family_reference); + if (NULL == token_family_cmd) + TALER_TESTING_FAIL (is); + if (GNUNET_OK != + TALER_TESTING_get_trait_token_family_slug (token_family_cmd, + &slug)) + TALER_TESTING_FAIL (is); + } + make_choices_json (slug, slug, + 1, 1, + GNUNET_TIME_absolute_to_timestamp(now), + GNUNET_TIME_absolute_to_timestamp(now), + &ps->choices); + + GNUNET_assert (0 == + json_object_set_new (ps->order_terms, + "choices", + ps->choices) + ); + GNUNET_assert (0 == + json_object_set_new (ps->order_terms, + "version", + json_string ("1")) + ); + + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &ps->nonce, + sizeof (struct GNUNET_CRYPTO_EddsaPublicKey)); + ps->po = TALER_MERCHANT_orders_post (TALER_TESTING_interpreter_get_context ( + is), + ps->merchant_url, + ps->order_terms, + GNUNET_TIME_UNIT_ZERO, + &order_cb, + ps); + GNUNET_assert (NULL != ps->po); +} + + +/** * Free the state of a "orders" CMD, and possibly * cancel it if it did not complete. * @@ -651,8 +777,7 @@ make_order_json (const char *order_id, "dummy_array", /* For testing forgetting parts of arrays */ "item", "speakers", "item", "headphones", - "item", "earbuds" - ); + "item", "earbuds"); GNUNET_assert (GNUNET_OK == TALER_JSON_expand_path (contract_terms, "$.dummy_obj", @@ -828,3 +953,43 @@ TALER_TESTING_cmd_merchant_post_orders3 ( return cmd; } } + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_orders_choices ( + const char *label, + const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *merchant_url, + unsigned int http_status, + const char *token_family_reference, + const char *order_id, + struct GNUNET_TIME_Timestamp refund_deadline, + struct GNUNET_TIME_Timestamp pay_deadline, + const char *amount) +{ + struct OrdersState *ps; + + ps = GNUNET_new (struct OrdersState); + ps->cfg = cfg; + make_order_json (order_id, + refund_deadline, + pay_deadline, + amount, + &ps->order_terms); + ps->http_status = http_status; + ps->token_family_reference = token_family_reference; + ps->expected_order_id = order_id; + ps->merchant_url = merchant_url; + ps->with_claim = true; + { + struct TALER_TESTING_Command cmd = { + .cls = ps, + .label = label, + .run = &orders_run3, + .cleanup = &orders_cleanup, + .traits = &orders_traits + }; + + return cmd; + } +}
\ No newline at end of file diff --git a/src/testing/testing_api_cmd_post_products.c b/src/testing/testing_api_cmd_post_products.c index 4ffafddc..c841f1b1 100644 --- a/src/testing/testing_api_cmd_post_products.c +++ b/src/testing/testing_api_cmd_post_products.c @@ -35,7 +35,7 @@ struct PostProductsState { /** - * Handle for a "GET product" request. + * Handle for a "POST /products" request. */ struct TALER_MERCHANT_ProductsPostHandle *iph; diff --git a/src/testing/testing_api_cmd_post_tokenfamilies.c b/src/testing/testing_api_cmd_post_tokenfamilies.c new file mode 100644 index 00000000..aafff9ef --- /dev/null +++ b/src/testing/testing_api_cmd_post_tokenfamilies.c @@ -0,0 +1,272 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ + +/** + * @file testing_api_cmd_post_tokenfamilies.c + * @brief command to run POST /tokenfamilies + * @author Christian Blättler + */ +#include "platform.h" +#include <gnunet/gnunet_time_lib.h> +#include <taler/taler_exchange_service.h> +#include <taler/taler_testing_lib.h> +#include "taler_merchant_service.h" +#include "taler_merchant_testing_lib.h" + + +/** + * State of a "POST /tokenfamilies" CMD. + */ +struct PostTokenFamiliesState +{ + + /** + * Expected status code. + */ + unsigned int http_status; + + /** + * Handle for a "POST /tokenfamilies" request. + */ + struct TALER_MERCHANT_TokenFamiliesPostHandle *handle; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Base URL of the merchant serving the request. + */ + const char *merchant_url; + + /** + * Slug of the token family. + */ + const char *slug; + + /** + * Name of the token family. + */ + const char *name; + + /** + * Description of the token family. + */ + const char *description; + + /** + * Map from IETF BCP 47 language tags to localized descriptions. + */ + json_t *description_i18n; + + /** + * Start of the validity period. + */ + struct GNUNET_TIME_Timestamp valid_after; + + /** + * End of the validity period. + */ + struct GNUNET_TIME_Timestamp valid_before; + + /** + * Validity duation of issued tokens of this family. + */ + struct GNUNET_TIME_Relative duration; + + /** + * Kind of the token family. "subscription" or "discount". + */ + const char *kind; +}; + + +/** + * Callback for a POST /tokenfamilies operation. + * + * @param cls closure for this function + * @param hr response being processed + */ +static void +post_tokenfamilies_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr) +{ + struct PostTokenFamiliesState *state = cls; + + state->handle = NULL; + if (state->http_status != hr->http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u (%d) to command %s\n", + hr->http_status, + (int) hr->ec, + TALER_TESTING_interpreter_get_current_label (state->is)); + TALER_TESTING_interpreter_fail (state->is); + return; + } + switch (hr->http_status) + { + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_UNAUTHORIZED: + break; + case MHD_HTTP_FORBIDDEN: + break; + case MHD_HTTP_NOT_FOUND: + break; + default: + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unhandled HTTP status %u for POST /tokenfamilies.\n", + hr->http_status); + } + TALER_TESTING_interpreter_next (state->is); +} + +/** + * Run the "POST /tokenfamilies" CMD. + * + * + * @param cls closure. + * @param cmd command being run now. + * @param is interpreter state. + */ +static void +post_tokenfamilies_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct PostTokenFamiliesState *state = cls; + + state->is = is; + state->handle = TALER_MERCHANT_token_families_post ( + TALER_TESTING_interpreter_get_context (is), + state->merchant_url, + state->slug, + state->name, + state->description, + state->description_i18n, + state->valid_after, + state->valid_before, + state->duration, + state->kind, + &post_tokenfamilies_cb, + state); + GNUNET_assert (NULL != state->handle); +} + +/** + * Offers information from the "POST /tokenfamilies" CMD state to other + * commands. + * + * @param cls closure + * @param[out] ret result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +post_tokenfamilies_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct PostTokenFamiliesState *state = cls; + struct TALER_TESTING_Trait traits[] = { + TALER_TESTING_make_trait_token_family_slug (state->slug), + TALER_TESTING_make_trait_timestamp (0, + &state->valid_after), + TALER_TESTING_make_trait_timestamp (1, + &state->valid_before), + TALER_TESTING_make_trait_token_family_duration (&state->duration), + TALER_TESTING_make_trait_token_family_kind (state->kind), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + +/** + * Free the state of a "POST /tokenfamilies" CMD, and possibly + * cancel a pending operation thereof. + * + * @param cls closure. + * @param cmd command being run. + */ +static void +post_tokenfamilies_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct PostTokenFamiliesState *state = cls; + + if (NULL != state->handle) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "POST /tokenfamilies operation did not complete\n"); + TALER_MERCHANT_token_families_post_cancel (state->handle); + } + json_decref (state->description_i18n); + GNUNET_free (state); +} + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_tokenfamilies ( + const char *label, + const char *merchant_url, + unsigned int http_status, + const char *slug, + const char *name, + const char *description, + json_t *description_i18n, + struct GNUNET_TIME_Timestamp valid_after, + struct GNUNET_TIME_Timestamp valid_before, + struct GNUNET_TIME_Relative duration, + const char *kind) /* "subscription" or "discount" */ +{ + struct PostTokenFamiliesState *state; + + GNUNET_assert ((NULL == description_i18n) || + json_is_object (description_i18n)); + state = GNUNET_new (struct PostTokenFamiliesState); + state->merchant_url = merchant_url; + state->http_status = http_status; + state->slug = slug; + state->name = name; + state->description = description; + state->description_i18n = description_i18n; /* ownership taken */ + state->valid_after = valid_after; + state->valid_before = valid_before; + state->duration = duration; + state->kind = kind; + { + struct TALER_TESTING_Command cmd = { + .cls = state, + .label = label, + .run = &post_tokenfamilies_run, + .cleanup = &post_tokenfamilies_cleanup, + .traits = &post_tokenfamilies_traits + }; + + return cmd; + } +}
\ No newline at end of file |