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