merchant

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

commit df28a7eadb8d0ce452bc2681a5204e61e03e6794
parent 764191401144a21e2e402b9afa46835a93778b9b
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 20 May 2025 12:33:58 +0200

implement backend logic for #9613 (schanzen)

Diffstat:
Msrc/backend/Makefile.am | 4++++
Msrc/backend/taler-merchant-httpd.c | 16++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.c | 254+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.c | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.h | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/Makefile.am | 4++++
Asrc/backenddb/pg_lookup_statistics_amount_by_bucket.c | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_statistics_amount_by_bucket.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_statistics_amount_by_interval.c | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_statistics_amount_by_interval.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_statistics_counter_by_bucket.c | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_statistics_counter_by_bucket.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_statistics_counter_by_interval.c | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_statistics_counter_by_interval.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_statistics_helpers.sql | 24++++++++++++++++++++++--
Msrc/backenddb/plugin_merchantdb_postgres.c | 12++++++++++++
Msrc/include/taler_merchant_service.h | 336+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler_merchant_testing_lib.h | 36++++++++++++++++++++++++++++++++++++
Msrc/include/taler_merchantdb_plugin.h | 209+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/lib/Makefile.am | 1+
Asrc/lib/merchant_api_get_statistics.c | 718+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/Makefile.am | 2++
Msrc/testing/test_merchant_api.c | 12++++++++++++
Asrc/testing/testing_api_cmd_get_statisticsamount.c | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testing/testing_api_cmd_get_statisticscounter.c | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
26 files changed, 3284 insertions(+), 5 deletions(-)

diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -197,6 +197,10 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_post-orders-ID-refund.h \ taler-merchant-httpd_post-using-templates.c \ taler-merchant-httpd_post-using-templates.h \ + taler-merchant-httpd_private-get-statistics-amount-SLUG.c \ + taler-merchant-httpd_private-get-statistics-amount-SLUG.h \ + taler-merchant-httpd_private-get-statistics-counter-SLUG.c \ + taler-merchant-httpd_private-get-statistics-counter-SLUG.h \ taler-merchant-httpd_qr.c \ taler-merchant-httpd_qr.h \ taler-merchant-httpd_spa.c \ diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -59,6 +59,8 @@ #include "taler-merchant-httpd_private-get-orders-ID.h" #include "taler-merchant-httpd_private-get-otp-devices.h" #include "taler-merchant-httpd_private-get-otp-devices-ID.h" +#include "taler-merchant-httpd_private-get-statistics-amount-SLUG.h" +#include "taler-merchant-httpd_private-get-statistics-counter-SLUG.h" #include "taler-merchant-httpd_private-get-templates.h" #include "taler-merchant-httpd_private-get-templates-ID.h" #include "taler-merchant-httpd_private-get-token-families.h" @@ -1339,6 +1341,20 @@ url_handler (void *cls, .have_id_segment = true, .handler = &TMH_private_patch_token_family_SLUG, }, + /* GET /statistics-counter/$SLUG: */ + { + .url_prefix = "/statistics-counter/", + .method = MHD_HTTP_METHOD_GET, + .have_id_segment = true, + .handler = &TMH_private_get_statistics_counter_SLUG, + }, + /* GET /statistics-amount/$SLUG: */ + { + .url_prefix = "/statistics-amount/", + .method = MHD_HTTP_METHOD_GET, + .have_id_segment = true, + .handler = &TMH_private_get_statistics_amount_SLUG, + }, { .url_prefix = NULL } diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.c b/src/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.c @@ -0,0 +1,254 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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_private-get-statistics-amount-SLUG.c + * @brief implement GET /statistics-amount/$SLUG/ + * @author Martin Schanzenbach + */ +#include "platform.h" +#include "taler-merchant-httpd_private-get-statistics-amount-SLUG.h" +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> + + +/** + * Typically called by `lookup_statistics_amount_by_bucket`. + * + * @param cls a `json_t *` JSON array to build + * @param description description of the statistic + * @param bucket_start start time of the bucket + * @param bucket_end end time of the bucket + * @param bucket_range range of the bucket + * @param cumulative_amounts_len the length of @a cumulative_amounts + * @param cumulative_amounts the cumulative amounts array + */ +static void +amount_by_bucket (void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + struct GNUNET_TIME_Timestamp bucket_end, + const char *bucket_range, + unsigned int amounts_len, + const struct TALER_Amount amounts[static amounts_len]) +{ + json_t *root = cls; + json_t *amount_array; + json_t *buckets_array; + + GNUNET_assert (json_is_object (root)); + buckets_array = json_object_get (root, + "buckets"); + GNUNET_assert (NULL != buckets_array); + GNUNET_assert (json_is_array (buckets_array)); + + amount_array = json_array (); + GNUNET_assert (NULL != amount_array); + for (unsigned int i = 0; i < amounts_len; i++) + { + GNUNET_assert ( + 0 == + json_array_append_new (amount_array, + TALER_JSON_from_amount (&amounts[i]))); + } + + GNUNET_assert ( + 0 == + json_array_append_new ( + buckets_array, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ( + "start_time", + bucket_start), + GNUNET_JSON_pack_timestamp ( + "end_time", + bucket_end), + GNUNET_JSON_pack_string ( + "range", + bucket_range), + GNUNET_JSON_pack_array_steal ( + "cumulative_amount", + amount_array)))); + if (NULL == json_object_get (root, + "buckets_description")) + { + GNUNET_assert (NULL == + json_object_set_new (root, + "buckets_description", + json_string (description))); + } +} + + +/** + * Typically called by `lookup_statistics_amount_by_interval`. + * + * @param cls a `json_t *` JSON array to build + * @param description description of the statistic + * @param interval_start start time of the bucket + * @param cumulative_amounts_len the length of @a cumulative_amounts + * @param cumulative_amounts the cumulative amounts array + */ +static void +amount_by_interval (void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + unsigned int amounts_len, + const struct TALER_Amount amounts[static amounts_len]) +{ + json_t *root; + json_t *amount_array; + json_t *intervals_array; + + root = cls; + GNUNET_assert (json_is_object (root)); + intervals_array = json_object_get (root, + "intervals"); + GNUNET_assert (NULL != intervals_array); + GNUNET_assert (json_is_array (intervals_array)); + + amount_array = json_array (); + GNUNET_assert (NULL != amount_array); + for (unsigned int i = 0; i < amounts_len; i++) + { + GNUNET_assert ( + 0 == + json_array_append_new (amount_array, + TALER_JSON_from_amount (&amounts[i]))); + } + + + GNUNET_assert ( + 0 == + json_array_append_new ( + intervals_array, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ( + "start_time", + bucket_start), + GNUNET_JSON_pack_array_steal ( + "cumulative_amount", + amount_array)))); + if (NULL == json_object_get (root, + "intervals_description")) + { + GNUNET_assert ( + 0 == + json_object_set_new (root, + "intervals_description", + json_string (description))); + } +} + + +/** + * Handle a GET "/statistics-amount/$SLUG" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_statistics_amount_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + json_t *root; + bool get_buckets = true; + bool get_intervals = true; + + GNUNET_assert (NULL != mi); + { + const char *filter; + + filter = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "by"); + if (NULL != filter) + { + if (0 == strcasecmp (filter, + "bucket")) + get_intervals = false; + else if (0 == strcasecmp (filter, + "interval")) + get_buckets = false; + else if (0 != strcasecmp (filter, + "any")) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "by"); + } + } + } + root = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("intervals", + json_array ()), + GNUNET_JSON_pack_array_steal ("buckets", + json_array ())); + if (get_buckets) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_statistics_amount_by_bucket ( + TMH_db->cls, + mi->settings.id, + hc->infix, + &amount_by_bucket, + root); + if (0 > qs) + { + GNUNET_break (0); + json_decref (root); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_amount_by_bucket"); + } + } + if (get_intervals) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_statistics_amount_by_interval ( + TMH_db->cls, + mi->settings.id, + hc->infix, + &amount_by_interval, + root); + if (0 > qs) + { + GNUNET_break (0); + json_decref (root); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_amount_by_interval"); + } + } + return TALER_MHD_reply_json (connection, + root, + MHD_HTTP_OK); +} + + +/* end of taler-merchant-httpd_private-get-statistics-amount-SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.h b/src/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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_private-get-statistics-counter-SLUG.h + * @brief implement GET /statistics-amount/$SLUG/ + * @author Martin Schanzenbach + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_AMOUNT_SLUG_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_AMOUNT_SLUG_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/statistics-amount/$SLUG" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_statistics_amount_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_private-get-statistics-amount-SLUG.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.c b/src/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.c @@ -0,0 +1,227 @@ +/* + This file is part of TALER + (C) 2023, 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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_private-get-statistics-counter-SLUG.c + * @brief implement GET /statistics-counter/$SLUG/ + * @author Martin Schanzenbach + */ +#include "platform.h" +#include "taler-merchant-httpd_private-get-statistics-counter-SLUG.h" +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> + + +/** + * Function returning integer-valued statistics. + * Typically called by `lookup_statistics_counter_by_bucket`. + * + * @param cls a `json_t *` JSON array to build + * @param description description of the statistic + * @param bucket_start start time of the bucket + * @param bucket_end end time of the bucket + * @param bucket_range range of the bucket + * @param cumulative_counter counter value + */ +static void +counter_by_bucket (void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + struct GNUNET_TIME_Timestamp bucket_end, + const char *bucket_range, + uint64_t cumulative_number) +{ + json_t *root = cls; + json_t *buckets_array; + + GNUNET_assert (json_is_object (root)); + buckets_array = json_object_get (root, + "buckets"); + GNUNET_assert (NULL != buckets_array); + GNUNET_assert (json_is_array (buckets_array)); + GNUNET_assert ( + 0 == + json_array_append_new ( + buckets_array, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ( + "start_time", + bucket_start), + GNUNET_JSON_pack_timestamp ( + "end_time", + bucket_end), + GNUNET_JSON_pack_string ( + "range", + bucket_range), + GNUNET_JSON_pack_uint64 ( + "cumulative_counter", + cumulative_number)))); + if (NULL == json_object_get (root, + "buckets_description")) + { + GNUNET_assert ( + 0 == + json_object_set_new (root, + "buckets_description", + json_string (description))); + } +} + + +/** + * Function returning integer-valued statistics for a time interval. + * Called by `lookup_statistics_counter_by_interval`. + * + * @param cls a `json_t *` JSON array to build + * @param description description of the statistic + * @param interval_start start time of the interval + * @param cumulative_counter counter value + */ +static void +counter_by_interval (void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + uint64_t cumulative_number) +{ + json_t *root = cls; + json_t *intervals_array; + + GNUNET_assert (json_is_object (root)); + intervals_array = json_object_get (root, + "intervals"); + GNUNET_assert (NULL != intervals_array); + GNUNET_assert (json_is_array (intervals_array)); + GNUNET_assert ( + 0 == + json_array_append_new ( + intervals_array, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ( + "start_time", + bucket_start), + GNUNET_JSON_pack_uint64 ( + "cumulative_counter", + cumulative_number)))); + if (NULL == json_object_get (root, + "intervals_description")) + { + GNUNET_assert ( + 0 == + json_object_set_new (root, + "intervals_description", + json_string (description))); + } +} + + +/** + * Handle a GET "/statistics-counter/$SLUG" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_statistics_counter_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + json_t *root; + bool get_buckets = true; + bool get_intervals = true; + + GNUNET_assert (NULL != mi); + { + const char *filter; + + filter = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "by"); + if (NULL != filter) + { + if (0 == strcasecmp (filter, + "bucket")) + get_intervals = false; + else if (0 == strcasecmp (filter, + "interval")) + get_buckets = false; + else if (0 != strcasecmp (filter, + "any")) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "by"); + } + } + } + root = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("intervals", + json_array ()), + GNUNET_JSON_pack_array_steal ("buckets", + json_array ())); + if (get_buckets) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_statistics_counter_by_bucket ( + TMH_db->cls, + mi->settings.id, + hc->infix, + &counter_by_bucket, + root); + if (0 > qs) + { + GNUNET_break (0); + json_decref (root); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_counter_by_bucket"); + } + } + if (get_intervals) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_statistics_counter_by_interval ( + TMH_db->cls, + mi->settings.id, + hc->infix, + &counter_by_interval, + root); + if (0 > qs) + { + GNUNET_break (0); + json_decref (root); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_counter_by_interval"); + } + } + return TALER_MHD_reply_json (connection, + root, + MHD_HTTP_OK); +} + + +/* end of taler-merchant-httpd_private-get-statistics-counter-SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.h b/src/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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_private-get-statistics-counter-SLUG.h + * @brief implement GET /statistics-counter/$SLUG/ + * @author Martin Schanzenbach + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_COUNTER_SLUG_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_COUNTER_SLUG_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/statistics-counter/$SLUG" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_statistics_counter_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_private-get-statistics-counter-SLUG.h */ +#endif diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am @@ -204,6 +204,10 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_update_transfer_status.h pg_update_transfer_status.c \ pg_update_webhook.h pg_update_webhook.c \ pg_update_wirewatch_progress.h pg_update_wirewatch_progress.c \ + pg_lookup_statistics_counter_by_bucket.h pg_lookup_statistics_counter_by_bucket.c \ + pg_lookup_statistics_counter_by_interval.h pg_lookup_statistics_counter_by_interval.c \ + pg_lookup_statistics_amount_by_bucket.h pg_lookup_statistics_amount_by_bucket.c \ + pg_lookup_statistics_amount_by_interval.h pg_lookup_statistics_amount_by_interval.c \ plugin_merchantdb_postgres.c libtaler_plugin_merchantdb_postgres_la_LIBADD = \ $(LTLIBINTL) diff --git a/src/backenddb/pg_lookup_statistics_amount_by_bucket.c b/src/backenddb/pg_lookup_statistics_amount_by_bucket.c @@ -0,0 +1,227 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_statistics_amount_by_bucket.c + * @brief Implementation of the lookup_statistics_amount_by_bucket function for Postgres + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_statistics_amount_by_bucket.h" +#include "pg_helper.h" +#include "taler_merchantdb_plugin.h" + + +/** + * Context used for TMH_PG_lookup_statistics_amount(). + */ +struct LookupAmountStatisticsContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_AmountByBucketStatisticsCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Did database result extraction fail? + */ + bool extract_failed; + + /** + * Postgres context for array lookups + */ + struct PostgresClosure *pg; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about token families. + * + * @param[in,out] cls of type `struct LookupTokenFamiliesContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_statistics_amount_by_bucket_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupAmountStatisticsContext *tflc = cls; + struct TALER_Amount *amounts = NULL; + char *resp_range = NULL; + char *resp_desc = NULL; + uint64_t cur_bucket_start_epoch; + uint64_t cur_bucket_end_epoch; + uint64_t bmeta_id_current; + unsigned int amounts_len = 0; + + for (unsigned int i = 0; i < num_results; i++) + { + struct TALER_Amount cumulative_amount; + char *description; + char *bucket_range; + uint64_t bmeta_id; + uint64_t bucket_start_epoch; + uint64_t bucket_end_epoch; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("bmeta_serial_id", + &bmeta_id), + GNUNET_PQ_result_spec_string ("description", + &description), + GNUNET_PQ_result_spec_uint64 ("bucket_start", + &bucket_start_epoch), + GNUNET_PQ_result_spec_uint64 ("bucket_end", + &bucket_end_epoch), + GNUNET_PQ_result_spec_string ("bucket_range", + &bucket_range), + TALER_PQ_result_spec_amount_with_currency ("cumulative_amount", + &cumulative_amount), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + tflc->extract_failed = true; + return; + } + /* Call callback if the bucket changed */ + if ( (NULL != resp_desc) && + ( (bmeta_id != bmeta_id_current) || + (bucket_start_epoch != cur_bucket_start_epoch) || + (0 != strcasecmp (resp_range, + bucket_range)) ) ) + { + struct GNUNET_TIME_Timestamp bucket_start; + struct GNUNET_TIME_Timestamp bucket_end; + + bucket_start = GNUNET_TIME_timestamp_from_s (cur_bucket_start_epoch); + bucket_end = GNUNET_TIME_timestamp_from_s (cur_bucket_end_epoch); + tflc->cb (tflc->cb_cls, + resp_desc, + bucket_start, + bucket_end, + resp_range, + amounts_len, + amounts); + GNUNET_free (resp_range); + GNUNET_free (resp_desc); + GNUNET_array_grow (amounts, + amounts_len, + 0); + } + if (NULL == resp_desc) + { + cur_bucket_end_epoch = bucket_end_epoch; + cur_bucket_start_epoch = bucket_start_epoch; + resp_range = GNUNET_strdup (bucket_range); + resp_desc = GNUNET_strdup (description); + bmeta_id_current = bmeta_id; + } + GNUNET_array_append (amounts, + amounts_len, + cumulative_amount); + GNUNET_PQ_cleanup_result (rs); + } + if (0 != amounts_len) + { + struct GNUNET_TIME_Timestamp bucket_start; + struct GNUNET_TIME_Timestamp bucket_end; + + bucket_start = GNUNET_TIME_timestamp_from_s (cur_bucket_start_epoch); + bucket_end = GNUNET_TIME_timestamp_from_s (cur_bucket_end_epoch); + tflc->cb (tflc->cb_cls, + resp_desc, + bucket_start, + bucket_end, + resp_range, + amounts_len, + amounts); + GNUNET_array_grow (amounts, + amounts_len, + 0); + GNUNET_free (resp_range); + GNUNET_free (resp_desc); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_amount_by_bucket ( + void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_AmountByBucketStatisticsCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupAmountStatisticsContext context = { + .cb = cb, + .cb_cls = cb_cls, + /* Can be overwritten by the lookup_statistics_amount_by_bucket_cb */ + .extract_failed = false, + .pg = pg, + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_string (slug), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_statistics_amount_by_bucket", + "SELECT" + " bmeta_serial_id" + ",description" + ",bucket_start" + ",bucket_range::TEXT" + ",merchant_statistics_bucket_end(bucket_start, bucket_range) AS bucket_end" + ",(cumulative_value,cumulative_frac,curr)::taler_amount_currency AS cumulative_amount" + " FROM merchant_statistic_bucket_amount" + " JOIN merchant_statistic_bucket_meta" + " USING (bmeta_serial_id)" + " JOIN merchant_instances" + " USING (merchant_serial)" + " WHERE merchant_instances.merchant_id=$1" + " AND merchant_statistic_bucket_meta.slug=$2" + " AND merchant_statistic_bucket_meta.stype='amount'"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "lookup_statistics_amount_by_bucket", + params, + &lookup_statistics_amount_by_bucket_cb, + &context); + /* If there was an error inside the cb, return a hard error. */ + if (context.extract_failed) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} diff --git a/src/backenddb/pg_lookup_statistics_amount_by_bucket.h b/src/backenddb/pg_lookup_statistics_amount_by_bucket.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_statistics_amount_by_bucket.h + * @brief implementation of the lookup_statistics_amount_by_bucket function for Postgres + * @author Martin Schanzenbach + */ +#ifndef PG_LOOKUP_STATISTICS_AMOUNT_BY_BUCKET_H +#define PG_LOOKUP_STATISTICS_AMOUNT_BY_BUCKET_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Lookup statistics where the values are amounts. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug slug to lookup statistics for + * @param cb function to call on all statistics found + * @param cb_cls closure for @a cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_amount_by_bucket ( + void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_AmountByBucketStatisticsCallback cb, + void *cb_cls); + +#endif diff --git a/src/backenddb/pg_lookup_statistics_amount_by_interval.c b/src/backenddb/pg_lookup_statistics_amount_by_interval.c @@ -0,0 +1,189 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_statistics_amount_by_interval.c + * @brief Implementation of the lookup_statistics_amount_by_interval function for Postgres + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_statistics_amount_by_interval.h" +#include "pg_helper.h" +#include "taler_merchantdb_plugin.h" + + +/** + * Context used for TMH_PG_lookup_statistics_amount(). + */ +struct LookupAmountStatisticsContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_AmountByIntervalStatisticsCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Did database result extraction fail? + */ + bool extract_failed; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about token families. + * + * @param[in,out] cls of type `struct LookupTokenFamiliesContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_statistics_amount_by_interval_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupAmountStatisticsContext *tflc = cls; + struct TALER_Amount *amounts = NULL; + char *resp_desc = NULL; + uint64_t cur_interval_start_epoch; + uint64_t bmeta_id_current; + unsigned int amounts_len = 0; + + for (unsigned int i = 0; i < num_results; i++) + { + char *description; + struct TALER_Amount cumulative_amount; + uint64_t interval_start_epoch; + uint64_t bmeta_id; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("bmeta_serial_id", + &bmeta_id), + GNUNET_PQ_result_spec_string ("description", + &description), + GNUNET_PQ_result_spec_uint64 ("range", + &interval_start_epoch), + TALER_PQ_result_spec_amount_with_currency ("rvalue", + &cumulative_amount), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + tflc->extract_failed = true; + return; + } + + /* Call callback if the bucket changed */ + if ( (NULL != resp_desc) && + ( (bmeta_id != bmeta_id_current) || + (interval_start_epoch != cur_interval_start_epoch)) ) + { + struct GNUNET_TIME_Timestamp interval_start; + + interval_start = GNUNET_TIME_timestamp_from_s (cur_interval_start_epoch); + tflc->cb (tflc->cb_cls, + resp_desc, + interval_start, + amounts_len, + amounts); + GNUNET_array_grow (amounts, + amounts_len, + 0); + GNUNET_free (resp_desc); + } + if (NULL == resp_desc) + { + cur_interval_start_epoch = interval_start_epoch; + resp_desc = GNUNET_strdup (description); + bmeta_id_current = bmeta_id; + } + GNUNET_array_append (amounts, + amounts_len, + cumulative_amount); + GNUNET_PQ_cleanup_result (rs); + } + if (0 != amounts_len) + { + struct GNUNET_TIME_Timestamp interval_start; + + interval_start = GNUNET_TIME_timestamp_from_s (cur_interval_start_epoch); + tflc->cb (tflc->cb_cls, + resp_desc, + interval_start, + amounts_len, + amounts); + GNUNET_array_grow (amounts, + amounts_len, + 0); + GNUNET_free (resp_desc); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_amount_by_interval ( + void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_AmountByIntervalStatisticsCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupAmountStatisticsContext context = { + .cb = cb, + .cb_cls = cb_cls, + /* Can be overwritten by the lookup_statistics_amount_by_interval_cb */ + .extract_failed = false, + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_string (slug), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_statistics_amount_by_interval", + "SELECT *" + " FROM merchant_statistic_interval_amount_get($1,$2)" + " JOIN merchant_statistic_bucket_meta" + " ON slug=$2"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "lookup_statistics_amount_by_interval", + params, + &lookup_statistics_amount_by_interval_cb, + &context); + /* If there was an error inside the cb, return a hard error. */ + if (context.extract_failed) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} diff --git a/src/backenddb/pg_lookup_statistics_amount_by_interval.h b/src/backenddb/pg_lookup_statistics_amount_by_interval.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_statistics_amount_by_interval.h + * @brief implementation of the lookup_statistics_amount_by_interval function for Postgres + * @author Martin Schanzenbach + */ +#ifndef PG_LOOKUP_STATISTICS_AMOUNT_BY_INTERVAL_H +#define PG_LOOKUP_STATISTICS_AMOUNT_BY_INTERVAL_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Lookup statistics where the values are amounts. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug slug to lookup statistics for + * @param cb function to call on all statistics found + * @param cb_cls closure for @a cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_amount_by_interval (void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_AmountByIntervalStatisticsCallback + cb, + void *cb_cls); + +#endif diff --git a/src/backenddb/pg_lookup_statistics_counter_by_bucket.c b/src/backenddb/pg_lookup_statistics_counter_by_bucket.c @@ -0,0 +1,167 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_statistics_counter_by_bucket.c + * @brief Implementation of the lookup_statistics_counter_by_bucket function for Postgres + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_statistics_counter_by_bucket.h" +#include "pg_helper.h" +#include "taler_merchantdb_plugin.h" + + +/** + * Context used for TMH_PG_lookup_statistics_counter(). + */ +struct LookupCounterStatisticsContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_CounterByBucketStatisticsCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Did database result extraction fail? + */ + bool extract_failed; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about token families. + * + * @param[in,out] cls of type `struct LookupTokenFamiliesContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_statistics_counter_by_bucket_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupCounterStatisticsContext *tflc = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + char *description; + char *bucket_range; + uint64_t cumulative_number; + uint64_t bucket_start_epoch; + uint64_t bucket_end_epoch; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("description", + &description), + GNUNET_PQ_result_spec_uint64 ("bucket_start", + &bucket_start_epoch), + GNUNET_PQ_result_spec_uint64 ("bucket_end", + &bucket_end_epoch), + GNUNET_PQ_result_spec_string ("bucket_range", + &bucket_range), + GNUNET_PQ_result_spec_uint64 ("cumulative_number", + &cumulative_number), + GNUNET_PQ_result_spec_end + }; + struct GNUNET_TIME_Timestamp bucket_start; + struct GNUNET_TIME_Timestamp bucket_end; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + tflc->extract_failed = true; + return; + } + + bucket_start = GNUNET_TIME_timestamp_from_s (bucket_start_epoch); + bucket_end = GNUNET_TIME_timestamp_from_s (bucket_end_epoch); + tflc->cb (tflc->cb_cls, + description, + bucket_start, + bucket_end, + bucket_range, + cumulative_number); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_counter_by_bucket ( + void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_CounterByBucketStatisticsCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupCounterStatisticsContext context = { + .cb = cb, + .cb_cls = cb_cls, + /* Can be overwritten by the lookup_statistics_counter_by_bucket_cb */ + .extract_failed = false, + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_string (slug), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_statistics_counter_by_bucket", + "SELECT" + " description" + ",bucket_start" + ",bucket_range::TEXT" + ",merchant_statistics_bucket_end(bucket_start, bucket_range) AS bucket_end" + ",cumulative_number" + " FROM merchant_statistic_bucket_counter" + " JOIN merchant_statistic_bucket_meta" + " USING (bmeta_serial_id)" + " JOIN merchant_instances" + " USING (merchant_serial)" + " WHERE merchant_instances.merchant_id=$1" + " AND " + " merchant_statistic_bucket_meta.slug=$2" + " AND " + " merchant_statistic_bucket_meta.stype = 'number'"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "lookup_statistics_counter_by_bucket", + params, + &lookup_statistics_counter_by_bucket_cb, + &context); + /* If there was an error inside the cb, return a hard error. */ + if (context.extract_failed) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} diff --git a/src/backenddb/pg_lookup_statistics_counter_by_bucket.h b/src/backenddb/pg_lookup_statistics_counter_by_bucket.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_statistics_counter_by_bucket.h + * @brief implementation of the lookup_statistics_counter_by_bucket function for Postgres + * @author Martin Schanzenbach + */ +#ifndef PG_LOOKUP_STATISTICS_COUNTER_BY_BUCKET_H +#define PG_LOOKUP_STATISTICS_COUNTER_BY_BUCKET_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Lookup statistics where the values are counters. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug slug to lookup statistics for + * @param cb function to call on all statistics found + * @param cb_cls closure for @a cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_counter_by_bucket (void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_CounterByBucketStatisticsCallback + cb, + void *cb_cls); + +#endif diff --git a/src/backenddb/pg_lookup_statistics_counter_by_interval.c b/src/backenddb/pg_lookup_statistics_counter_by_interval.c @@ -0,0 +1,145 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_statistics_counter_by_interval.c + * @brief Implementation of the lookup_statistics_counter_by_interval function for Postgres + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_statistics_counter_by_interval.h" +#include "pg_helper.h" +#include "taler_merchantdb_plugin.h" + + +/** + * Context used for TMH_PG_lookup_statistics_counter(). + */ +struct LookupCounterStatisticsContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_CounterByIntervalStatisticsCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Did database result extraction fail? + */ + bool extract_failed; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about token families. + * + * @param[in,out] cls of type `struct LookupTokenFamiliesContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_statistics_counter_by_interval_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupCounterStatisticsContext *tflc = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + char *description; + uint64_t cumulative_number; + uint64_t interval_start_epoch; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("description", + &description), + GNUNET_PQ_result_spec_uint64 ("start_time", + &interval_start_epoch), + GNUNET_PQ_result_spec_uint64 ("cumulative_number", + &cumulative_number), + GNUNET_PQ_result_spec_end + }; + struct GNUNET_TIME_Timestamp interval_start; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + tflc->extract_failed = true; + return; + } + + interval_start = GNUNET_TIME_timestamp_from_s (interval_start_epoch); + tflc->cb (tflc->cb_cls, + description, + interval_start, + cumulative_number); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_counter_by_interval ( + void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_CounterByIntervalStatisticsCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupCounterStatisticsContext context = { + .cb = cb, + .cb_cls = cb_cls, + /* Can be overwritten by the lookup_token_families_cb */ + .extract_failed = false, + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_string (slug), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_statistics_counter_by_interval", + "SELECT *" + " FROM merchant_statistic_interval_number_get($1,$2)" + " JOIN merchant_statistic_bucket_meta" + " ON slug=$2"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "lookup_statistics_counter_by_interval", + params, + &lookup_statistics_counter_by_interval_cb, + &context); + /* If there was an error inside the cb, return a hard error. */ + if (context.extract_failed) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} diff --git a/src/backenddb/pg_lookup_statistics_counter_by_interval.h b/src/backenddb/pg_lookup_statistics_counter_by_interval.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_statistics_counter_by_interval.h + * @brief implementation of the lookup_statistics_by_interval function for Postgres + * @author Martin Schanzenbach + */ +#ifndef PG_LOOKUP_STATISTICS_COUNTER_BY_INTERVAL_H +#define PG_LOOKUP_STATISTICS_COUNTER_BY_INTERVAL_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Lookup statistics where the values are counters. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug slug to lookup statistics for + * @param cb function to call on all statistics found + * @param cb_cls closure for @a cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_counter_by_interval (void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_CounterByIntervalStatisticsCallback + cb, + void *cb_cls); + +#endif diff --git a/src/backenddb/pg_statistics_helpers.sql b/src/backenddb/pg_statistics_helpers.sql @@ -437,7 +437,7 @@ COMMENT ON PROCEDURE merchant_do_bump_amount_stat DROP FUNCTION IF EXISTS merchant_statistic_interval_number_get; -CREATE OR REPLACE FUNCTION merchant_statistic_interval_number_get ( +CREATE FUNCTION merchant_statistic_interval_number_get ( IN in_slug TEXT, IN in_instance_id TEXT ) @@ -582,7 +582,7 @@ COMMENT ON FUNCTION merchant_statistic_interval_number_get DROP FUNCTION IF EXISTS merchant_statistic_interval_amount_get; -CREATE OR REPLACE FUNCTION merchant_statistic_interval_amount_get ( +CREATE FUNCTION merchant_statistic_interval_amount_get ( IN in_slug TEXT, IN in_instance_id TEXT ) @@ -1070,3 +1070,23 @@ COMMENT ON PROCEDURE merchant_statistic_bucket_gc IS 'Performs garbage collection of the merchant_statistic_bucket_counter and merchant_statistic_bucket_amount tables'; + +-- The date_trunc may not be necessary if we assume it is already truncated +DROP FUNCTION IF EXISTS merchant_statistics_bucket_end; +CREATE FUNCTION merchant_statistics_bucket_end ( + IN in_bucket_start INT8, + IN in_range statistic_range, + OUT out_bucket_end INT8 +) +LANGUAGE plpgsql +AS $$ +BEGIN + IF in_range='quarter' + THEN + out_bucket_end = EXTRACT(EPOCH FROM CAST(date_trunc('quarter', to_timestamp(in_bucket_start)::date) + interval '3 months' AS date)); + ELSE + out_bucket_end = EXTRACT(EPOCH FROM CAST(to_timestamp(in_bucket_start)::date + ('1 ' || in_range)::interval AS date)); + END IF; +END $$; +COMMENT ON FUNCTION merchant_statistics_bucket_end +IS 'computes the end time of the bucket for an event at the current time given the desired bucket range'; diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -155,6 +155,10 @@ #include "pg_insert_spent_token.h" #include "pg_insert_issued_token.h" #include "pg_lookup_spent_tokens_by_order.h" +#include "pg_lookup_statistics_amount_by_bucket.h" +#include "pg_lookup_statistics_amount_by_interval.h" +#include "pg_lookup_statistics_counter_by_bucket.h" +#include "pg_lookup_statistics_counter_by_interval.h" /** @@ -631,6 +635,14 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_insert_issued_token; plugin->lookup_spent_tokens_by_order = &TMH_PG_lookup_spent_tokens_by_order; + plugin->lookup_statistics_amount_by_bucket + = &TMH_PG_lookup_statistics_amount_by_bucket; + plugin->lookup_statistics_counter_by_bucket + = &TMH_PG_lookup_statistics_counter_by_bucket; + plugin->lookup_statistics_counter_by_interval + = &TMH_PG_lookup_statistics_counter_by_interval; + plugin->lookup_statistics_amount_by_interval + = &TMH_PG_lookup_statistics_amount_by_interval; plugin->gc = &TMH_PG_gc; diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -5763,5 +5763,341 @@ void TALER_MERCHANT_webhook_delete_cancel ( struct TALER_MERCHANT_WebhookDeleteHandle *wdh); +/* ********************* /statistics-[counter,amount] ************************** */ + +/** + * Statistic type that can be filtered by + */ +enum TALER_MERCHANT_StatisticsType +{ + + /** + * Get all statistics + */ + TALER_MERCHANT_STATISTICS_ALL, + + /** + * Get statistics by interval only + */ + TALER_MERCHANT_STATISTICS_BY_INTERVAL, + + /** + * Get statistics by bucket only + */ + TALER_MERCHANT_STATISTICS_BY_BUCKET, + +}; + +/** + * Handle for a GET /statistics-counter/$SLUG operation. + */ +struct TALER_MERCHANT_StatisticsCounterGetHandle; + +/** + * Counter by interval result object + */ +struct TALER_MERCHANT_StatisticCounterByInterval +{ + + /** + * Start time of the interval (inclusive). + * The interval always ends at the response + * generation time. + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * Sum of all counters falling under the given + * SLUG within this timeframe. + */ + uint64_t cumulative_counter; + +}; + +/** + * Counter by bucket result object + */ +struct TALER_MERCHANT_StatisticCounterByBucket +{ + + /** + * Start time of the bucket (inclusive). + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * End time of the bucket (exclusive). + */ + struct GNUNET_TIME_Timestamp end_time; + + /** + * Range of the bucket + */ + const char *range; + + /** + * Sum of all counters falling under the given + * SLUG within this timeframe. + */ + uint64_t cumulative_counter; + +}; + +/** + * Response to GET /statistics-counter/$SLUG operation. + */ +struct TALER_MERCHANT_StatisticsCounterGetResponse +{ + /** + * HTTP response details + */ + struct TALER_MERCHANT_HttpResponse hr; + + /** + * Details depending on HTTP status. + */ + union + { + /** + * Details for #MHD_HTTP_OK. + */ + struct + { + /** + * length of the @a buckets array + */ + unsigned int buckets_length; + + /** + * array of statistics in this bucket + */ + const struct TALER_MERCHANT_StatisticCounterByBucket *buckets; + + /** + * description of the statistic of the buckets + */ + const char *buckets_description; + + /** + * length of the @a intervals array + */ + unsigned int intervals_length; + + /** + * array of statistics in this interval + */ + const struct TALER_MERCHANT_StatisticCounterByInterval *intervals; + + /** + * description of the statistic of the intervals + */ + const char *intervals_description; + + } ok; + + } details; + +}; + +/** + * Cancel GET /statistics-counter/$SLUG operation. + * + * @param handle operation to cancel + */ +void +TALER_MERCHANT_statistic_counter_get_cancel ( + struct TALER_MERCHANT_StatisticsCounterGetHandle *handle); + + +/** + * Function called with the result of the GET /statistics-counter/$SLUG operation. + * + * @param cls closure + * @param scgr response details + */ +typedef void +(*TALER_MERCHANT_StatisticsCounterGetCallback)( + void *cls, + const struct TALER_MERCHANT_StatisticsCounterGetResponse *scgr); + +/** + * Make a GET /statistics-counter/$SLUG request. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend + * @param slug short, url-safe identifier for the statistic + * @param stype the type of statistic to get, see #TALER_MERCHANT_StatisticType + * @param cb function to call with the statistic information + * @param cb_cls closure for @a cb + * @return the request handle; NULL upon error + */ +struct TALER_MERCHANT_StatisticsCounterGetHandle * +TALER_MERCHANT_statistic_counter_get ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *slug, + enum TALER_MERCHANT_StatisticsType stype, + TALER_MERCHANT_StatisticsCounterGetCallback cb, + void *cb_cls); + +/** + * Handle for a GET /statistics-amount/$SLUG operation. + */ +struct TALER_MERCHANT_StatisticsAmountGetHandle; + +/** + * Amount by interval result object + */ +struct TALER_MERCHANT_StatisticAmountByInterval +{ + /** + * Start time of the interval (inclusive). + * The interval always ends at the response + * generation time. + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * Sum of all amounts falling under the given + * SLUG within this timeframe. + */ + struct TALER_Amount *cumulative_amounts; + + /** + * Length of array @a cumulative_amounts + */ + unsigned int cumulative_amount_len; + +}; + +/** + * Amount by bucket result object + */ +struct TALER_MERCHANT_StatisticAmountByBucket +{ + /** + * Start time of the bucket (inclusive). + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * End time of the bucket (exclusive). + */ + struct GNUNET_TIME_Timestamp end_time; + + /** + * Range of the bucket + */ + const char *range; + + /** + * Sum of all amounts falling under the given + * SLUG within this timeframe. + */ + struct TALER_Amount *cumulative_amounts; + + /** + * Length of array @a cumulative_amounts + */ + unsigned int cumulative_amount_len; +}; + +/** + * Response to GET /statistics-amount/$SLUG operation. + */ +struct TALER_MERCHANT_StatisticsAmountGetResponse +{ + /** + * HTTP response details + */ + struct TALER_MERCHANT_HttpResponse hr; + + /** + * Details depending on HTTP status. + */ + union + { + /** + * Details for #MHD_HTTP_OK. + */ + struct + { + /** + * length of the @a buckets array + */ + unsigned int buckets_length; + + /** + * array of statistics in this bucket + */ + const struct TALER_MERCHANT_StatisticAmountByBucket *buckets; + + /** + * description of the statistic of the buckets + */ + const char *buckets_description; + + /** + * length of the @a intervals array + */ + unsigned int intervals_length; + + /** + * array of statistics in this Interval + */ + const struct TALER_MERCHANT_StatisticAmountByInterval *intervals; + + /** + * description of the statistic of the intervals + */ + const char *intervals_description; + + } ok; + + } details; + +}; + +/** + * Cancel GET /statistics-amount/$SLUG operation. + * + * @param handle operation to cancel + */ +void +TALER_MERCHANT_statistic_amount_get_cancel ( + struct TALER_MERCHANT_StatisticsAmountGetHandle *handle); + + +/** + * Function called with the result of the GET /statistics-amount/$SLUG operation. + * + * @param cls closure + * @param sagr response details + */ +typedef void +(*TALER_MERCHANT_StatisticsAmountGetCallback)( + void *cls, + const struct TALER_MERCHANT_StatisticsAmountGetResponse *sagr); + +/** + * Make a GET /statistics-amount request. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend + * @param slug short, url-safe identifier for the statistic + * @param stype the type of statistic to get, see #TALER_MERCHANT_StatisticType + * @param cb function to call with the statistic information + * @param cb_cls closure for @a cb + * @return the request handle; NULL upon error + */ +struct TALER_MERCHANT_StatisticsAmountGetHandle * +TALER_MERCHANT_statistic_amount_get ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *slug, + enum TALER_MERCHANT_StatisticsType stype, + TALER_MERCHANT_StatisticsAmountGetCallback cb, + void *cb_cls); + #endif /* _TALER_MERCHANT_SERVICE_H */ diff --git a/src/include/taler_merchant_testing_lib.h b/src/include/taler_merchant_testing_lib.h @@ -1791,6 +1791,42 @@ TALER_TESTING_cmd_checkserver2 (const char *label, const char *expected_header, const char *expected_body); +/** + * This function is used to check the statistics counter API + * + * @param label command label + * @param merchant_url base URL of the merchant serving the API + * @param slug base statistics slug + * @param buckets_length expected length of buckets array + * @param intervals_length expected length of intervals array + * @param http_status expected HTTP response code. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_get_statisticscounter (const char *label, + const char *merchant_url, + const char *slug, + uint64_t buckets_length, + uint64_t intervals_length, + unsigned int http_status); + +/** + * This function is used to check the statistics amount API + * + * @param label command label + * @param merchant_url base URL of the merchant serving the API + * @param slug base statistics slug + * @param buckets_length expected length of buckets array + * @param intervals_length expected length of intervals array + * @param http_status expected HTTP response code. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_get_statisticsamount (const char *label, + const char *merchant_url, + const char *slug, + uint64_t buckets_length, + uint64_t intervals_length, + unsigned int http_status); + /* ****** Specific traits supported by this component ******* */ diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -1293,17 +1293,17 @@ struct TALER_MERCHANTDB_SpentTokenDetails { /** * Public key of the spent token. - */ + */ struct TALER_TokenUsePublicKeyP pub; /** * Signature that this token was spent on the specified order. - */ + */ struct TALER_TokenUseSignatureP sig; /** * Blind signature for the spent token to prove validity of it. - */ + */ struct TALER_BlindedTokenIssueSignature blind_sig; }; @@ -1329,6 +1329,137 @@ typedef void const struct TALER_TokenUseSignatureP *use_sig, const struct TALER_TokenIssueSignature *issue_sig); + +/** + * Returns amount-valued statistics by bucket. + * Called by `lookup_statistics_amount_by_bucket`. + * + * @param cls closure + * @param description description of the statistic + * @param bucket_start start time of the bucket + * @param bucket_end end time of the bucket + * @param bucket_range range of the bucket + * @param cumulative_amounts_len the length of @a cumulative_amounts + * @param cumulative_amounts the cumulative amounts array + */ +typedef void +(*TALER_MERCHANTDB_AmountByBucketStatisticsCallback)( + void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + struct GNUNET_TIME_Timestamp bucket_end, + const char *bucket_range, + unsigned int cumulative_amounts_len, + const struct TALER_Amount cumulative_amounts[static cumulative_amounts_len]); + + +/** + * Returns amount-valued statistics over a particular time interval. + * Called by `lookup_statistics_amount_by_interval`. + * + * @param cls closure + * @param description description of the statistic + * @param interval_start start time of the bucket + * @param cumulative_amounts_len the length of @a cumulative_amounts + * @param cumulative_amounts the cumulative amounts array + */ +typedef void +(*TALER_MERCHANTDB_AmountByIntervalStatisticsCallback)( + void *cls, + const char *description, + struct GNUNET_TIME_Timestamp interval_start, + unsigned int cumulative_amounts_len, + const struct TALER_Amount cumulative_amounts[static cumulative_amounts_len]); + + +/** + * Function returning integer-valued statistics for a bucket. + * Called by `lookup_statistics_counter_by_bucket`. + * + * @param cls closure + * @param description description of the statistic + * @param bucket_start start time of the bucket + * @param bucket_end end time of the bucket + * @param bucket_range range of the bucket + * @param cumulative_counter counter value + */ +typedef void +(*TALER_MERCHANTDB_CounterByBucketStatisticsCallback)( + void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + struct GNUNET_TIME_Timestamp bucket_end, + const char *bucket_range, + uint64_t cumulative_counter); + +/** + * Details about a statistic with counter. + */ +struct TALER_MERCHANTDB_StatisticsCounterByBucketDetails +{ + /** + * Start time of the bucket (inclusive). + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * End time of the bucket (exclusive). + */ + struct GNUNET_TIME_Timestamp end_time; + + /** + * Description of the statistic + */ + char*description; + + /** + * Range of the bucket + */ + char *range; + + /** + * Sum of all counters falling under the given + * SLUG within this timeframe + */ + uint64_t cumulative_number; +}; + +/** + * Details about a statistic with counter. + */ +struct TALER_MERCHANTDB_StatisticsCounterByIntervalDetails +{ + /** + * Start time of the interval. + * The interval always ends at the response generation time. + */ + struct GNUNET_TIME_Timestamp start_time; + + /** + * Sum of all counters falling under the given + * SLUG within this timeframe + */ + uint64_t cumulative_counter; +}; + + +/** + * Function returning integer-valued statistics for a time interval. + * Called by `lookup_statistics_counter_by_interval`. + * + * @param cls closure + * @param description description of the statistic + * @param interval_start start time of the interval + * @param cumulative_counter counter value + */ +typedef void +(*TALER_MERCHANTDB_CounterByIntervalStatisticsCallback)( + void *cls, + const char *description, + struct GNUNET_TIME_Timestamp interval_start, + uint64_t cumulative_counter); + + /** * Handle to interact with the database. * @@ -3879,6 +4010,78 @@ struct TALER_MERCHANTDB_Plugin struct GNUNET_TIME_Timestamp future_retry, struct GNUNET_TIME_Relative retry_backoff, const char *emsg); + + /** + * Lookup amount statistics for instance and slug by bucket. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug instance to lookup statistics for + * @param cb function to call on all token families found + * @param cb_cls closure for @a cb + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*lookup_statistics_amount_by_bucket)( + void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_AmountByBucketStatisticsCallback cb, + void *cb_cls); + + + /** + * Lookup counter statistics for instance and slug by bucket. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug instance to lookup statistics for + * @param cb function to call on all token families found + * @param cb_cls closure for @a cb + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*lookup_statistics_counter_by_bucket)( + void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_CounterByBucketStatisticsCallback cb, + void *cb_cls); + + /** + * Lookup amount statistics for instance and slug by interval. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug instance to lookup statistics for + * @param cb function to call on all token families found + * @param cb_cls closure for @a cb + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*lookup_statistics_amount_by_interval)( + void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_AmountByIntervalStatisticsCallback cb, + void *cb_cls); + /** + * Lookup counter statistics for instance and slug by interval. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug instance to lookup statistics for + * @param cb function to call on all token families found + * @param cb_cls closure for @a cb + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*lookup_statistics_counter_by_interval)(void *cls, + const char *instance_id, + const char *slug, + TALER_MERCHANTDB_CounterByIntervalStatisticsCallback + cb, + void *cb_cls); }; #endif diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -35,6 +35,7 @@ libtalermerchant_la_SOURCES = \ merchant_api_get_otp_devices.c \ merchant_api_get_product.c \ merchant_api_get_products.c \ + merchant_api_get_statistics.c \ merchant_api_get_transfers.c \ merchant_api_get_template.c \ merchant_api_get_templates.c \ diff --git a/src/lib/merchant_api_get_statistics.c b/src/lib/merchant_api_get_statistics.c @@ -0,0 +1,718 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_statistics.c + * @brief Implementation of the GET /statistics-[counter,amount]/$SLUG request of the merchant's HTTP API + * @author Martin Schanzenbach + */ +#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> + +/** + * Maximum number of statistics we return + */ +#define MAX_STATISTICS 1024 + +/** + * Handle for a GET /statistics-amount/$SLUG operation. + */ +struct TALER_MERCHANT_StatisticsAmountGetHandle +{ + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_StatisticsAmountGetCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + +}; + +/** + * Handle for a GET /statistics-counter/$SLUG operation. + */ +struct TALER_MERCHANT_StatisticsCounterGetHandle +{ + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_StatisticsCounterGetCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + +}; + + +/** + * Parse interval information from buckets and intervals. + * + * @param json overall JSON reply + * @param jbuckets JSON array (or NULL!) with bucket data + * @param jintervals JSON array (or NULL!) with bucket data + * @param scgh operation handle + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_intervals_and_buckets_amt ( + const json_t *json, + const json_t *jbuckets, + const char *buckets_description, + const json_t *jintervals, + const char *intervals_description, + struct TALER_MERCHANT_StatisticsAmountGetHandle *sgh + ) +{ + unsigned int resp_buckets_len = json_array_size (jbuckets); + unsigned int resp_intervals_len = json_array_size (jintervals); + + if ( (json_array_size (jbuckets) != (size_t) resp_buckets_len) || + (json_array_size (jintervals) != (size_t) resp_intervals_len) || + (resp_intervals_len = resp_buckets_len > MAX_STATISTICS) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + { + struct TALER_MERCHANT_StatisticAmountByBucket resp_buckets[ + GNUNET_NZL (resp_buckets_len)]; + struct TALER_MERCHANT_StatisticAmountByInterval resp_intervals[ + GNUNET_NZL (resp_intervals_len)]; + size_t index; + json_t *value; + enum GNUNET_GenericReturnValue ret; + + ret = GNUNET_OK; + json_array_foreach (jintervals, index, value) { + struct TALER_MERCHANT_StatisticAmountByInterval *jinterval + = &resp_intervals[index]; + const json_t *amounts_arr; + size_t amounts_len; + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("start_time", + &jinterval->start_time), + GNUNET_JSON_spec_array_const ("cumulative_amount", + &amounts_arr), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (value, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + ret = GNUNET_SYSERR; + continue; + } + if (GNUNET_SYSERR == ret) + break; + amounts_len = json_array_size (amounts_arr); + if (0 > amounts_len) + { + GNUNET_break_op (0); + ret = GNUNET_SYSERR; + break; + } + { + struct TALER_Amount amt_arr[amounts_len]; + size_t aindex; + json_t *avalue; + jinterval->cumulative_amount_len = amounts_len; + jinterval->cumulative_amounts = amt_arr; + json_array_foreach (amounts_arr, aindex, avalue) { + if (! json_is_string (avalue)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_string_to_amount (json_string_value (avalue), + &amt_arr[aindex])) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + } + } + ret = GNUNET_OK; + json_array_foreach (jbuckets, index, value) { + struct TALER_MERCHANT_StatisticAmountByBucket *jbucket + = &resp_buckets[index]; + const json_t *amounts_arr; + size_t amounts_len; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("start_time", + &jbucket->start_time), + GNUNET_JSON_spec_timestamp ("end_time", + &jbucket->end_time), + GNUNET_JSON_spec_string ("range", + &jbucket->range), + GNUNET_JSON_spec_array_const ("cumulative_amount", + &amounts_arr), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (value, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + ret = GNUNET_SYSERR; + continue; + } + if (GNUNET_SYSERR == ret) + break; + amounts_len = json_array_size (amounts_arr); + if (0 > amounts_len) + { + GNUNET_break_op (0); + ret = GNUNET_SYSERR; + break; + } + { + struct TALER_Amount amt_arr[amounts_len]; + size_t aindex; + json_t *avalue; + jbucket->cumulative_amount_len = amounts_len; + jbucket->cumulative_amounts = amt_arr; + json_array_foreach (amounts_arr, aindex, avalue) { + if (! json_is_string (avalue)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_string_to_amount (json_string_value (avalue), + &amt_arr[aindex])) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + } + } + if (GNUNET_OK == ret) + { + struct TALER_MERCHANT_StatisticsAmountGetResponse gsr = { + .hr.http_status = MHD_HTTP_OK, + .hr.reply = json, + .details.ok.buckets_length = resp_buckets_len, + .details.ok.buckets = resp_buckets, + .details.ok.buckets_description = buckets_description, + .details.ok.intervals_length = resp_intervals_len, + .details.ok.intervals = resp_intervals, + .details.ok.intervals_description = intervals_description, + }; + sgh->cb (sgh->cb_cls, + &gsr); + sgh->cb = NULL; /* just to be sure */ + } + return ret; + } +} + + +/** + * Function called when we're done processing the + * HTTP GET /statistics-amount/$SLUG request. + * + * @param cls the `struct TALER_MERCHANT_StatisticsAmountGetHandle` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_get_statistics_amount_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_MERCHANT_StatisticsAmountGetHandle *handle = cls; + const json_t *json = response; + struct TALER_MERCHANT_StatisticsAmountGetResponse res = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + handle->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Got /statistics-amount/$SLUG response with status code %u\n", + (unsigned int) response_code); + switch (response_code) + { + case MHD_HTTP_OK: + { + const json_t *buckets; + const json_t *intervals; + const char *buckets_description = NULL; + const char *intervals_description = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("buckets", + &buckets), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("buckets_description", + &buckets_description), + NULL), + GNUNET_JSON_spec_array_const ("intervals", + &intervals), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("intervals_description", + &intervals_description), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + res.hr.http_status = 0; + res.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + if (GNUNET_OK == + parse_intervals_and_buckets_amt (json, + buckets, + buckets_description, + intervals, + intervals_description, + handle)) + { + TALER_MERCHANT_statistic_amount_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; + } +} + + +/** + * Parse interval information from @a ia. + * + * @param json overall JSON reply + * @param jbuckets JSON array (or NULL!) with bucket data + * @param jintervals JSON array (or NULL!) with bucket data + * @param scgh operation handle + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_intervals_and_buckets ( + const json_t *json, + const json_t *jbuckets, + const char *buckets_description, + const json_t *jintervals, + const char *intervals_description, + struct TALER_MERCHANT_StatisticsCounterGetHandle *scgh) +{ + unsigned int resp_buckets_len = json_array_size (jbuckets); + unsigned int resp_intervals_len = json_array_size (jintervals); + + if ( (json_array_size (jbuckets) != (size_t) resp_buckets_len) || + (json_array_size (jintervals) != (size_t) resp_intervals_len) || + (resp_intervals_len = resp_buckets_len > MAX_STATISTICS) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + { + struct TALER_MERCHANT_StatisticCounterByBucket resp_buckets[ + GNUNET_NZL (resp_buckets_len)]; + struct TALER_MERCHANT_StatisticCounterByInterval resp_intervals[ + GNUNET_NZL (resp_intervals_len)]; + size_t index; + json_t *value; + enum GNUNET_GenericReturnValue ret; + + ret = GNUNET_OK; + json_array_foreach (jintervals, index, value) { + struct TALER_MERCHANT_StatisticCounterByInterval *jinterval + = &resp_intervals[index]; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("start_time", + &jinterval->start_time), + GNUNET_JSON_spec_uint64 ("cumulative_counter", + &jinterval->cumulative_counter), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (value, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + ret = GNUNET_SYSERR; + continue; + } + if (GNUNET_SYSERR == ret) + break; + } + ret = GNUNET_OK; + json_array_foreach (jbuckets, index, value) { + struct TALER_MERCHANT_StatisticCounterByBucket *jbucket = &resp_buckets[ + index]; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("start_time", + &jbucket->start_time), + GNUNET_JSON_spec_timestamp ("end_time", + &jbucket->end_time), + GNUNET_JSON_spec_string ("range", + &jbucket->range), + GNUNET_JSON_spec_uint64 ("cumulative_counter", + &jbucket->cumulative_counter), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (value, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + ret = GNUNET_SYSERR; + continue; + } + if (GNUNET_SYSERR == ret) + break; + } + if (GNUNET_OK == ret) + { + struct TALER_MERCHANT_StatisticsCounterGetResponse gsr = { + .hr.http_status = MHD_HTTP_OK, + .hr.reply = json, + .details.ok.buckets_length = resp_buckets_len, + .details.ok.buckets = resp_buckets, + .details.ok.buckets_description = buckets_description, + .details.ok.intervals_length = resp_intervals_len, + .details.ok.intervals = resp_intervals, + .details.ok.intervals_description = intervals_description, + }; + scgh->cb (scgh->cb_cls, + &gsr); + scgh->cb = NULL; /* just to be sure */ + } + return ret; + } +} + + +/** + * Function called when we're done processing the + * HTTP GET /statistics-counter/$SLUG request. + * + * @param cls the `struct TALER_MERCHANT_StatisticsCounterGetHandle` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_get_statistics_counter_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_MERCHANT_StatisticsCounterGetHandle *handle = cls; + const json_t *json = response; + struct TALER_MERCHANT_StatisticsCounterGetResponse res = { + .hr.http_status = (unsigned int) response_code, + .hr.reply = json + }; + + handle->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Got /statistics-counter/$SLUG response with status code %u\n", + (unsigned int) response_code); + switch (response_code) + { + case MHD_HTTP_OK: + { + const json_t *buckets; + const json_t *intervals; + const char *buckets_description; + const char *intervals_description; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("buckets", + &buckets), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("buckets_description", + &buckets_description), + NULL), + GNUNET_JSON_spec_array_const ("intervals", + &intervals), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("intervals_description", + &intervals_description), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + res.hr.http_status = 0; + res.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "%s\n", json_dumps (json, JSON_INDENT (1))); + if (GNUNET_OK == + parse_intervals_and_buckets (json, + buckets, + buckets_description, + intervals, + intervals_description, + handle)) + { + TALER_MERCHANT_statistic_counter_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_StatisticsCounterGetHandle * +TALER_MERCHANT_statistic_counter_get ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *slug, + enum TALER_MERCHANT_StatisticsType stype, + TALER_MERCHANT_StatisticsCounterGetCallback cb, + void *cb_cls) +{ + struct TALER_MERCHANT_StatisticsCounterGetHandle *handle; + CURL *eh; + + handle = GNUNET_new (struct TALER_MERCHANT_StatisticsCounterGetHandle); + handle->ctx = ctx; + handle->cb = cb; + handle->cb_cls = cb_cls; + { + const char *filter = NULL; + char *path; + + switch (stype) + { + case TALER_MERCHANT_STATISTICS_BY_BUCKET: + filter = "bucket"; + break; + case TALER_MERCHANT_STATISTICS_BY_INTERVAL: + filter = "interval"; + break; + case TALER_MERCHANT_STATISTICS_ALL: + filter = NULL; + break; + } + GNUNET_asprintf (&path, + "private/statistics-counter/%s", + slug); + handle->url = TALER_url_join (backend_url, + path, + "by", + filter, + 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_statistics_counter_finished, + handle); + return handle; +} + + +void +TALER_MERCHANT_statistic_counter_get_cancel ( + struct TALER_MERCHANT_StatisticsCounterGetHandle *handle) +{ + if (NULL != handle->job) + GNUNET_CURL_job_cancel (handle->job); + GNUNET_free (handle->url); + GNUNET_free (handle); +} + + +struct TALER_MERCHANT_StatisticsAmountGetHandle * +TALER_MERCHANT_statistic_amount_get ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *slug, + enum TALER_MERCHANT_StatisticsType stype, + TALER_MERCHANT_StatisticsAmountGetCallback cb, + void *cb_cls) +{ + struct TALER_MERCHANT_StatisticsAmountGetHandle *handle; + CURL *eh; + + handle = GNUNET_new (struct TALER_MERCHANT_StatisticsAmountGetHandle); + handle->ctx = ctx; + handle->cb = cb; + handle->cb_cls = cb_cls; + { + const char *filter = NULL; + char *path; + + switch (stype) + { + case TALER_MERCHANT_STATISTICS_BY_BUCKET: + filter = "bucket"; + break; + case TALER_MERCHANT_STATISTICS_BY_INTERVAL: + filter = "interval"; + break; + case TALER_MERCHANT_STATISTICS_ALL: + filter = NULL; + break; + } + GNUNET_asprintf (&path, + "private/statistics-amount/%s", + slug); + handle->url = TALER_url_join (backend_url, + path, + "by", + filter, + 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_statistics_amount_finished, + handle); + return handle; +} + + +void +TALER_MERCHANT_statistic_amount_get_cancel ( + struct TALER_MERCHANT_StatisticsAmountGetHandle *handle) +{ + if (NULL != handle->job) + GNUNET_CURL_job_cancel (handle->job); + GNUNET_free (handle->url); + GNUNET_free (handle); +} diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am @@ -39,6 +39,8 @@ libtalermerchanttesting_la_SOURCES = \ testing_api_cmd_get_otp_devices.c \ testing_api_cmd_get_product.c \ testing_api_cmd_get_products.c \ + testing_api_cmd_get_statisticsamount.c \ + testing_api_cmd_get_statisticscounter.c \ testing_api_cmd_get_transfers.c \ testing_api_cmd_get_templates.c \ testing_api_cmd_get_template.c \ diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -2141,6 +2141,18 @@ run (void *cls, repurchase), TALER_TESTING_cmd_batch ("tokens", tokens), + TALER_TESTING_cmd_merchant_get_statisticsamount ("stats-refund", + merchant_url, + "refunds-granted", 6, 0, + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_get_statisticscounter ("stats-tokens-issued", + merchant_url, + "tokens-issued", 6, 0, + MHD_HTTP_OK), + TALER_TESTING_cmd_merchant_get_statisticscounter ("stats-tokens-used", + merchant_url, + "tokens-used", 6, 0, + MHD_HTTP_OK), /** * End the suite. */ diff --git a/src/testing/testing_api_cmd_get_statisticsamount.c b/src/testing/testing_api_cmd_get_statisticsamount.c @@ -0,0 +1,220 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_get_statisticsamount.c + * @brief command to test GET /statistics-amount/$SLUG + * @author Martin Schanzenbach + */ +#include "platform.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 "GET statistics-amount" CMD. + */ +struct GetStatisticsAmountState +{ + + /** + * Handle for a "GET statistics-amount" request. + */ + struct TALER_MERCHANT_StatisticsAmountGetHandle *scgh; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Base URL of the merchant serving the request. + */ + const char *merchant_url; + + /** + * Slug of the statistic to get. + */ + const char *slug; + + /** + * Expected HTTP response code. + */ + unsigned int http_status; + + /** + * Expected bucket size. + */ + uint64_t buckets_length; + + /** + * Expected intervals size. + */ + uint64_t intervals_length; + +}; + + +/** + * Callback for a GET /statistics-amount operation. + * + * @param cls closure for this function + * @param gpr response details + */ +static void +get_statisticsamount_cb (void *cls, + const struct + TALER_MERCHANT_StatisticsAmountGetResponse *scgr) +{ + struct GetStatisticsAmountState *scs = cls; + const struct TALER_MERCHANT_HttpResponse *hr = &scgr->hr; + + scs->scgh = NULL; + if (scs->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 (scs->is)); + TALER_TESTING_interpreter_fail (scs->is); + return; + } + switch (hr->http_status) + { + case MHD_HTTP_OK: + { + if (scgr->details.ok.buckets_length != scs->buckets_length) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Length of buckets found does not match (Got %llu, expected %llu)\n", + (unsigned long long) scgr->details.ok.buckets_length, + (unsigned long long) scs->buckets_length); + TALER_TESTING_interpreter_fail (scs->is); + return; + } + if (scgr->details.ok.intervals_length != scs->intervals_length) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Length of intervals found does not match (Got %llu, expected %llu)\n", + (unsigned long long) scgr->details.ok.intervals_length, + (unsigned long long) scs->intervals_length); + TALER_TESTING_interpreter_fail (scs->is); + return; + } + } + break; + case MHD_HTTP_UNAUTHORIZED: + break; + case MHD_HTTP_NOT_FOUND: + /* instance does not exist */ + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unhandled HTTP status %u (%d).\n", + hr->http_status, + hr->ec); + } + TALER_TESTING_interpreter_next (scs->is); +} + + +/** + * Run the "GET /products" CMD. + * + * + * @param cls closure. + * @param cmd command being run now. + * @param is interpreter state. + */ +static void +get_statisticsamount_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct GetStatisticsAmountState *scs = cls; + + scs->is = is; + scs->scgh = TALER_MERCHANT_statistic_amount_get ( + TALER_TESTING_interpreter_get_context (is), + scs->merchant_url, + scs->slug, + TALER_MERCHANT_STATISTICS_ALL, + &get_statisticsamount_cb, + scs); + GNUNET_assert (NULL != scs->scgh); +} + + +/** + * Free the state of a "GET statistics-amount" CMD, and possibly + * cancel a pending operation thereof. + * + * @param cls closure. + * @param cmd command being run. + */ +static void +get_statisticsamount_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct GetStatisticsAmountState *scs = cls; + + if (NULL != scs->scgh) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "GET /statistics-amount operation did not complete\n"); + TALER_MERCHANT_statistic_amount_get_cancel (scs->scgh); + } + GNUNET_free (scs); +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_get_statisticsamount (const char *label, + const char *merchant_url, + const char *slug, + uint64_t + expected_buckets_length, + uint64_t + expected_intervals_length, + unsigned int http_status) +{ + struct GetStatisticsAmountState *scs; + + scs = GNUNET_new (struct GetStatisticsAmountState); + scs->merchant_url = merchant_url; + scs->slug = slug; + scs->buckets_length = expected_buckets_length; + scs->intervals_length = expected_intervals_length; + scs->http_status = http_status; + { + struct TALER_TESTING_Command cmd = { + .cls = scs, + .label = label, + .run = &get_statisticsamount_run, + .cleanup = &get_statisticsamount_cleanup + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_get_statisticsamount.c */ diff --git a/src/testing/testing_api_cmd_get_statisticscounter.c b/src/testing/testing_api_cmd_get_statisticscounter.c @@ -0,0 +1,220 @@ +/* + This file is part of TALER + Copyright (C) 2025 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_get_statisticscounter.c + * @brief command to test GET /statistics-counter/$SLUG + * @author Martin Schanzenbach + */ +#include "platform.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 "GET statistics-counter" CMD. + */ +struct GetStatisticsCounterState +{ + + /** + * Handle for a "GET statistics-counter" request. + */ + struct TALER_MERCHANT_StatisticsCounterGetHandle *scgh; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Base URL of the merchant serving the request. + */ + const char *merchant_url; + + /** + * Slug of the statistic to get. + */ + const char *slug; + + /** + * Expected HTTP response code. + */ + unsigned int http_status; + + /** + * Expected bucket size. + */ + uint64_t buckets_length; + + /** + * Expected intervals size. + */ + uint64_t intervals_length; + +}; + + +/** + * Callback for a GET /statistics-counter operation. + * + * @param cls closure for this function + * @param gpr response details + */ +static void +get_statisticscounter_cb (void *cls, + const struct + TALER_MERCHANT_StatisticsCounterGetResponse *scgr) +{ + struct GetStatisticsCounterState *scs = cls; + const struct TALER_MERCHANT_HttpResponse *hr = &scgr->hr; + + scs->scgh = NULL; + if (scs->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 (scs->is)); + TALER_TESTING_interpreter_fail (scs->is); + return; + } + switch (hr->http_status) + { + case MHD_HTTP_OK: + { + if (scgr->details.ok.buckets_length != scs->buckets_length) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Length of buckets found does not match (Got %llu, expected %llu)\n", + (unsigned long long) scgr->details.ok.buckets_length, + (unsigned long long) scs->buckets_length); + TALER_TESTING_interpreter_fail (scs->is); + return; + } + if (scgr->details.ok.intervals_length != scs->intervals_length) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Length of intervals found does not match (Got %llu, expected %llu)\n", + (unsigned long long) scgr->details.ok.intervals_length, + (unsigned long long) scs->intervals_length); + TALER_TESTING_interpreter_fail (scs->is); + return; + } + } + break; + case MHD_HTTP_UNAUTHORIZED: + break; + case MHD_HTTP_NOT_FOUND: + /* instance does not exist */ + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unhandled HTTP status %u (%d).\n", + hr->http_status, + hr->ec); + } + TALER_TESTING_interpreter_next (scs->is); +} + + +/** + * Run the "GET /products" CMD. + * + * + * @param cls closure. + * @param cmd command being run now. + * @param is interpreter state. + */ +static void +get_statisticscounter_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct GetStatisticsCounterState *scs = cls; + + scs->is = is; + scs->scgh = TALER_MERCHANT_statistic_counter_get ( + TALER_TESTING_interpreter_get_context (is), + scs->merchant_url, + scs->slug, + TALER_MERCHANT_STATISTICS_ALL, + &get_statisticscounter_cb, + scs); + GNUNET_assert (NULL != scs->scgh); +} + + +/** + * Free the state of a "GET statistics-counter" CMD, and possibly + * cancel a pending operation thereof. + * + * @param cls closure. + * @param cmd command being run. + */ +static void +get_statisticscounter_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct GetStatisticsCounterState *scs = cls; + + if (NULL != scs->scgh) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "GET /statistics-counter operation did not complete\n"); + TALER_MERCHANT_statistic_counter_get_cancel (scs->scgh); + } + GNUNET_free (scs); +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_get_statisticscounter (const char *label, + const char *merchant_url, + const char *slug, + uint64_t + expected_buckets_length, + uint64_t + expected_intervals_length, + unsigned int http_status) +{ + struct GetStatisticsCounterState *scs; + + scs = GNUNET_new (struct GetStatisticsCounterState); + scs->merchant_url = merchant_url; + scs->slug = slug; + scs->buckets_length = expected_buckets_length; + scs->intervals_length = expected_intervals_length; + scs->http_status = http_status; + { + struct TALER_TESTING_Command cmd = { + .cls = scs, + .label = label, + .run = &get_statisticscounter_run, + .cleanup = &get_statisticscounter_cleanup + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_get_statisticscounter.c */