commit 9f427ed5ee6e3fd33031f49f2d62d97e90754ded parent 61a43265656d1530b745606c15615c871d284394 Author: Christian Grothoff <christian@grothoff.org> Date: Thu, 1 Jan 2026 17:32:37 +0100 implement first PDF generation statistics endpoint (#10487), completely untested Diffstat:
20 files changed, 1393 insertions(+), 40 deletions(-)
diff --git a/contrib/typst/transactions.typ b/contrib/typst/transactions.typ @@ -258,7 +258,7 @@ label-key: "start_date_mini", value-key: "values", x-label: [Time], - y-label: dchart.y-label, + y-label: dchart.y_label, x-tick-step: none, y-tick-step: auto, mode : if dchart.cummulative { "stacked" } else { "clustered" }, @@ -317,7 +317,7 @@ bucket_period: (d_us: 86400000000), charts: ( (chart_name: "Transaction volume", - y-label: "Volume", + y_label: "Volume", data_groups: ( (start_date: (t_s: 1766790000), values: (10, 20, 30)), @@ -334,7 +334,7 @@ cummulative: false, ), (chart_name: "Transaction rate", - y-label: "Rate", + y_label: "Rate", data_groups: ( (start_date: (t_s: 1764967786), values: (10, 20, 30)), diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -242,6 +242,8 @@ taler_merchant_httpd_SOURCES = \ 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_private-get-statistics-report-transactions.c \ + taler-merchant-httpd_private-get-statistics-report-transactions.h \ taler-merchant-httpd_qr.c \ taler-merchant-httpd_qr.h \ taler-merchant-httpd_spa.c \ diff --git a/src/backend/merchant.conf b/src/backend/merchant.conf @@ -60,6 +60,10 @@ WIRE_TRANSFER_DELAY = 3 week # proposal be valid? DEFAULT_PAY_DEADLINE = 1 day +# Where are the Typst templates for form rendering. +TYPST_TEMPLATES = ${DATADIR}typst-forms/ + + [merchant-kyccheck] # How long do we wait between AML status requests to the diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -42,6 +42,7 @@ #include "taler-merchant-httpd_spa.h" #include "taler-merchant-httpd_terms.h" #include "taler-merchant-httpd_private-get-instances-ID-kyc.h" +#include "taler-merchant-httpd_private-get-statistics-report-transactions.h" #include "taler-merchant-httpd_private-post-donau-instance.h" #include "taler-merchant-httpd_private-get-orders-ID.h" #include "taler-merchant-httpd_private-get-orders.h" @@ -137,7 +138,7 @@ static int global_ret; /** * Our configuration. */ -static const struct GNUNET_CONFIGURATION_Handle *cfg; +const struct GNUNET_CONFIGURATION_Handle *TMH_cfg; void TMH_wire_method_free (struct TMH_WireMethod *wm) @@ -208,6 +209,7 @@ do_shutdown (void *cls) { (void) cls; TALER_MHD_daemons_halt (); + TMH_handler_statistic_report_transactions_cleanup (); TMH_force_orders_resume (); TMH_force_ac_resume (); TMH_force_pc_resume (); @@ -1001,7 +1003,7 @@ run (void *cls, (void) cls; (void) args; (void) cfgfile; - cfg = config; + TMH_cfg = config; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Starting taler-merchant-httpd\n"); go = TALER_MHD_GO_NONE; @@ -1032,7 +1034,7 @@ run (void *cls, "Taler-Correlation-Id"); if (GNUNET_SYSERR == - TALER_config_get_currency (cfg, + TALER_config_get_currency (TMH_cfg, "merchant", &TMH_currency)) { @@ -1041,7 +1043,7 @@ run (void *cls, return; } if (GNUNET_OK != - TALER_CONFIG_parse_currencies (cfg, + TALER_CONFIG_parse_currencies (TMH_cfg, TMH_currency, &TMH_num_cspecs, &TMH_cspecs)) @@ -1053,7 +1055,7 @@ run (void *cls, if (GNUNET_SYSERR == (TMH_strict_v19 - = GNUNET_CONFIGURATION_get_value_yesno (cfg, + = GNUNET_CONFIGURATION_get_value_yesno (TMH_cfg, "merchant", "STRICT_PROTOCOL_V19"))) { @@ -1063,7 +1065,7 @@ run (void *cls, TMH_strict_v19 = GNUNET_NO; } if (GNUNET_SYSERR == - (TMH_auth_disabled = GNUNET_CONFIGURATION_get_value_yesno (cfg, + (TMH_auth_disabled = GNUNET_CONFIGURATION_get_value_yesno (TMH_cfg, "merchant", "DISABLE_AUTHENTICATION"))) { @@ -1077,7 +1079,7 @@ run (void *cls, if (GNUNET_SYSERR == (TMH_have_self_provisioning - = GNUNET_CONFIGURATION_get_value_yesno (cfg, + = GNUNET_CONFIGURATION_get_value_yesno (TMH_cfg, "merchant", "ENABLE_SELF_PROVISIONING"))) { @@ -1088,7 +1090,7 @@ run (void *cls, } if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (cfg, + GNUNET_CONFIGURATION_get_value_time (TMH_cfg, "merchant", "LEGAL_PRESERVATION", &TMH_legal_expiration)) @@ -1102,7 +1104,7 @@ run (void *cls, } if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (cfg, + GNUNET_CONFIGURATION_get_value_time (TMH_cfg, "merchant", "DEFAULT_PAY_DELAY", &TMH_default_pay_delay)) @@ -1123,7 +1125,7 @@ run (void *cls, return; } if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (cfg, + GNUNET_CONFIGURATION_get_value_time (TMH_cfg, "merchant", "DEFAULT_REFUND_DELAY", &TMH_default_refund_delay)) @@ -1147,7 +1149,7 @@ run (void *cls, } if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_time (cfg, + GNUNET_CONFIGURATION_get_value_time (TMH_cfg, "merchant", "DEFAULT_WIRE_TRANSFER_DELAY", &TMH_default_wire_transfer_delay)) @@ -1173,7 +1175,7 @@ run (void *cls, if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string ( - cfg, + TMH_cfg, "merchant", "DEFAULT_WIRE_TRANSFER_ROUNDING_INTERVAL", &dwtri)) @@ -1204,10 +1206,10 @@ run (void *cls, } } - TMH_load_terms (cfg); + TMH_load_terms (TMH_cfg); if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (cfg, + GNUNET_CONFIGURATION_get_value_string (TMH_cfg, "merchant", "PAYMENT_TARGET_TYPES", &TMH_allowed_payment_targets)) @@ -1216,7 +1218,7 @@ run (void *cls, } if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (cfg, + GNUNET_CONFIGURATION_get_value_string (TMH_cfg, "merchant", "DEFAULT_PERSONA", &TMH_default_persona)) @@ -1225,7 +1227,7 @@ run (void *cls, } if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (cfg, + GNUNET_CONFIGURATION_get_value_string (TMH_cfg, "merchant", "PAYMENT_TARGET_REGEX", &TMH_payment_target_regex)) @@ -1257,7 +1259,7 @@ run (void *cls, } if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (cfg, + GNUNET_CONFIGURATION_get_value_string (TMH_cfg, "merchant", "HELPER_SMS", &TMH_helper_sms)) @@ -1269,7 +1271,7 @@ run (void *cls, } if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (cfg, + GNUNET_CONFIGURATION_get_value_string (TMH_cfg, "merchant", "HELPER_EMAIL", &TMH_helper_email)) @@ -1284,7 +1286,7 @@ run (void *cls, char *tan_channels; if (GNUNET_OK == - GNUNET_CONFIGURATION_get_value_string (cfg, + GNUNET_CONFIGURATION_get_value_string (TMH_cfg, "merchant", "MANDATORY_TAN_CHANNELS", &tan_channels)) @@ -1344,7 +1346,7 @@ run (void *cls, } if (GNUNET_OK == - GNUNET_CONFIGURATION_get_value_string (cfg, + GNUNET_CONFIGURATION_get_value_string (TMH_cfg, "merchant", "BASE_URL", &TMH_base_url)) @@ -1361,7 +1363,7 @@ run (void *cls, } } if (GNUNET_OK == - GNUNET_CONFIGURATION_get_value_string (cfg, + GNUNET_CONFIGURATION_get_value_string (TMH_cfg, "merchant", "BACKOFFICE_SPA_DIR", &TMH_spa_dir)) @@ -1377,7 +1379,7 @@ run (void *cls, } if (GNUNET_YES == - GNUNET_CONFIGURATION_get_value_yesno (cfg, + GNUNET_CONFIGURATION_get_value_yesno (TMH_cfg, "merchant", "FORCE_AUDIT")) TMH_force_audit = GNUNET_YES; @@ -1410,7 +1412,7 @@ run (void *cls, return; } if (NULL == - (TMH_db = TALER_MERCHANTDB_plugin_load (cfg))) + (TMH_db = TALER_MERCHANTDB_plugin_load (TMH_cfg))) { global_ret = EXIT_NOTINSTALLED; GNUNET_SCHEDULER_shutdown (); @@ -1459,7 +1461,7 @@ run (void *cls, { enum GNUNET_GenericReturnValue ret; - ret = TALER_MHD_listen_bind (cfg, + ret = TALER_MHD_listen_bind (TMH_cfg, "merchant", &start_daemon, NULL); diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -773,6 +773,11 @@ extern struct TALER_CurrencySpecification *TMH_cspecs; extern int TMH_force_audit; /** + * Our configuration. + */ +extern const struct GNUNET_CONFIGURATION_Handle *TMH_cfg; + +/** * Context for all CURL operations (useful to the event loop) */ extern struct GNUNET_CURL_Context *TMH_curl_ctx; diff --git a/src/backend/taler-merchant-httpd_dispatcher.c b/src/backend/taler-merchant-httpd_dispatcher.c @@ -58,6 +58,7 @@ #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-statistics-report-transactions.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" @@ -898,6 +899,14 @@ determine_handler_group (const char **urlp, .have_id_segment = true, .handler = &TMH_private_get_statistics_amount_SLUG, }, + /* GET /statistics-report/transactions: */ + { + .url_prefix = "/statistics-report/", + .url_suffix = "transactions", + .method = MHD_HTTP_METHOD_GET, + .permission = "statistics-read", + .handler = &TMH_private_get_statistics_report_transactions, + }, { .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 @@ -192,7 +192,7 @@ TMH_private_get_statistics_amount_SLUG (const struct TMH_RequestHandler *rh, GNUNET_break_op (0); return TALER_MHD_reply_with_error ( connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, + MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "by"); } 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 @@ -165,7 +165,7 @@ TMH_private_get_statistics_counter_SLUG (const struct TMH_RequestHandler *rh, GNUNET_break_op (0); return TALER_MHD_reply_with_error ( connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, + MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "by"); } diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.c b/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.c @@ -0,0 +1,740 @@ +/* + 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-report-transactions.c + * @brief implement GET /statistics-report/transactions + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-get-statistics-report-transactions.h" +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_mhd_lib.h> + + +/** + * Closure for the detail_cb(). + */ +struct ResponseContext +{ + /** + * Format of the response we are to generate. + */ + enum + { + RCF_JSON, + RCF_PDF + } format; + + /** + * Stored in a DLL while suspended. + */ + struct ResponseContext *next; + + /** + * Stored in a DLL while suspended. + */ + struct ResponseContext *prev; + + /** + * Context for this request. + */ + struct TMH_HandlerContext *hc; + + /** + * Async context used to run Typst. + */ + struct TALER_MHD_TypstContext *tc; + + /** + * Response to return. + */ + struct MHD_Response *response; + + /** + * Time when we started processing the request. + */ + struct GNUNET_TIME_Timestamp now; + + /** + * Period of each bucket. + */ + struct GNUNET_TIME_Relative period; + + /** + * Granularity of the buckets. Matches @e period. + */ + const char *granularity; + + /** + * Number of buckets to return. + */ + uint64_t count; + + /** + * HTTP status to use with @e response. + */ + unsigned int http_status; + + /** + * Length of the @e labels array. + */ + unsigned int labels_cnt; + + /** + * Array of labels for the chart. + */ + char **labels; + + /** + * Data groups for the chart. + */ + json_t *data_groups; + +}; + + +/** + * DLL of requests awaiting Typst. + */ +static struct ResponseContext *rctx_head; + +/** + * DLL of requests awaiting Typst. + */ +static struct ResponseContext *rctx_tail; + + +void +TMH_handler_statistic_report_transactions_cleanup () +{ + struct ResponseContext *rctx; + + while (NULL != (rctx = rctx_head)) + { + GNUNET_CONTAINER_DLL_remove (rctx_head, + rctx_tail, + rctx); + MHD_resume_connection (rctx->hc->connection); + } +} + + +/** + * Free resources from @a ctx + * + * @param[in] ctx the `struct ResponseContext` to clean up + */ +static void +free_rc (void *ctx) +{ + struct ResponseContext *rctx = ctx; + + if (NULL != rctx->tc) + { + TALER_MHD_typst_cancel (rctx->tc); + rctx->tc = NULL; + } + if (NULL != rctx->response) + { + MHD_destroy_response (rctx->response); + rctx->response = NULL; + } + for (unsigned int i = 0; i<rctx->labels_cnt; i++) + GNUNET_free (rctx->labels[i]); + GNUNET_array_grow (rctx->labels, + rctx->labels_cnt, + 0); + json_decref (rctx->data_groups); + GNUNET_free (rctx); +} + + +/** + * Function called with the result of a #TALER_MHD_typst() operation. + * + * @param cls closure + * @param tr result of the operation + */ +static void +pdf_cb (void *cls, + const struct TALER_MHD_TypstResponse *tr) +{ + struct ResponseContext *rctx = cls; + + rctx->tc = NULL; + GNUNET_CONTAINER_DLL_remove (rctx_head, + rctx_tail, + rctx); + MHD_resume_connection (rctx->hc->connection); + TALER_MHD_daemon_trigger (); + if (TALER_EC_NONE != tr->ec) + { + rctx->http_status + = TALER_ErrorCode_get_http_status (tr->ec); + rctx->response + = TALER_MHD_make_error (tr->ec, + tr->details.hint); + return; + } + rctx->http_status + = MHD_HTTP_OK; + rctx->response + = TALER_MHD_response_from_pdf_file (tr->details.filename); +} + + +/** + * Typically called by `lookup_statistics_amount_by_bucket2`. + * + * @param[in,out] cls our `struct ResponseContext` to update + * @param bucket_start start time of the bucket + * @param amounts_len the length of @a amounts array + * @param amounts the cumulative amounts in the bucket + */ +static void +amount_by_bucket (void *cls, + struct GNUNET_TIME_Timestamp bucket_start, + unsigned int amounts_len, + const struct TALER_Amount amounts[static amounts_len]) +{ + struct ResponseContext *rctx = cls; + json_t *values; + + for (unsigned int i = 0; i<amounts_len; i++) + { + bool found = false; + + for (unsigned int j = 0; j<rctx->labels_cnt; j++) + { + if (0 == strcmp (amounts[i].currency, + rctx->labels[j])) + { + found = true; + break; + } + } + if (! found) + { + GNUNET_array_append (rctx->labels, + rctx->labels_cnt, + GNUNET_strdup (amounts[i].currency)); + } + } + + values = json_array (); + GNUNET_assert (NULL != values); + for (unsigned int i = 0; i<rctx->labels_cnt; i++) + { + const char *label = rctx->labels[i]; + double d = 0.0; + + for (unsigned int j = 0; j<amounts_len; j++) + { + const struct TALER_Amount *a = &amounts[j]; + + if (0 != strcmp (amounts[j].currency, + label)) + continue; + d = a->value * 1.0 + + (a->fraction * 1.0 / TALER_AMOUNT_FRAC_BASE); + break; + } /* for all amounts */ + GNUNET_assert (0 == + json_array_append_new (values, + json_real (d))); + } /* for all labels */ + + { + json_t *dg; + + dg = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("start_date", + bucket_start), + GNUNET_JSON_pack_array_steal ("values", + values)); + GNUNET_assert (0 == + json_array_append_new (rctx->data_groups, + dg)); + + } +} + + +/** + * Create the transaction volume report. + * + * @param[in,out] rctx request context to use + * @param[in,out] charts JSON chart array to expand + * @return #GNUNET_OK on success, + * #GNUNET_NO to end with #MHD_YES, + * #GNUNET_NO to end with #MHD_NO. + */ +static enum GNUNET_GenericReturnValue +make_transaction_volume_report (struct ResponseContext *rctx, + json_t *charts) +{ + // FIXME: this is from example-statistics, we probably want to hard-wire the stats from this endpoint! + const char *bucket_name = "sales (before refunds)"; + enum GNUNET_DB_QueryStatus qs; + json_t *chart; + json_t *labels; + + rctx->data_groups = json_array (); + GNUNET_assert (NULL != rctx->data_groups); + qs = TMH_db->lookup_statistics_amount_by_bucket2 ( + TMH_db->cls, + rctx->hc->instance->settings.id, + bucket_name, + rctx->granularity, + rctx->count, + &amount_by_bucket, + rctx); + if (0 > qs) + { + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error ( + rctx->hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_amount_by_bucket")) + ? GNUNET_NO : GNUNET_SYSERR; + } + + labels = json_array (); + GNUNET_assert (NULL != labels); + for (unsigned int i=0; i<rctx->labels_cnt; i++) + { + GNUNET_assert (0 == + json_array_append_new (labels, + json_string (rctx->labels[i]))); + GNUNET_free (rctx->labels[i]); + } + GNUNET_array_grow (rctx->labels, + rctx->labels_cnt, + 0); + chart = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("chart_name", + "Sales volume"), + GNUNET_JSON_pack_string ("y_label", + "Sales"), + GNUNET_JSON_pack_array_steal ("data_groups", + rctx->data_groups), + GNUNET_JSON_pack_array_steal ("labels", + labels), + GNUNET_JSON_pack_bool ("cumulative", + false)); + rctx->data_groups = NULL; + GNUNET_assert (0 == + json_array_append_new (charts, + chart)); + return GNUNET_OK; +} + + +/** + * Typically called by `lookup_statistics_counter_by_bucket2`. + * + * @param[in,out] cls our `struct ResponseContext` to update + * @param bucket_start start time of the bucket + * @param counters_len the length of @a cumulative_amounts + * @param descriptions description for the counter in the bucket + * @param counters the counters in the bucket + */ +static void +count_by_bucket (void *cls, + struct GNUNET_TIME_Timestamp bucket_start, + unsigned int counters_len, + const char *descriptions[static counters_len], + uint64_t counters[static counters_len]) +{ + struct ResponseContext *rctx = cls; + json_t *values; + + for (unsigned int i = 0; i<counters_len; i++) + { + bool found = false; + + for (unsigned int j = 0; j<rctx->labels_cnt; j++) + { + if (0 == strcmp (descriptions[i], + rctx->labels[j])) + { + found = true; + break; + } + } + if (! found) + { + GNUNET_array_append (rctx->labels, + rctx->labels_cnt, + GNUNET_strdup (descriptions[i])); + } + } + + values = json_array (); + GNUNET_assert (NULL != values); + for (unsigned int i = 0; i<rctx->labels_cnt; i++) + { + const char *label = rctx->labels[i]; + uint64_t v = 0; + + for (unsigned int j = 0; j<counters_len; j++) + { + if (0 != strcmp (descriptions[j], + label)) + continue; + v = counters[j]; + break; + } /* for all amounts */ + GNUNET_assert (0 == + json_array_append_new (values, + json_integer (v))); + } /* for all labels */ + + { + json_t *dg; + + dg = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("start_date", + bucket_start), + GNUNET_JSON_pack_array_steal ("values", + values)); + GNUNET_assert (0 == + json_array_append_new (rctx->data_groups, + dg)); + + } +} + + +/** + * Create the transaction count report. + * + * @param[in,out] rctx request context to use + * @param[in,out] charts JSON chart array to expand + * @return #GNUNET_OK on success, + * #GNUNET_NO to end with #MHD_YES, + * #GNUNET_NO to end with #MHD_NO. + */ +static enum GNUNET_GenericReturnValue +make_transaction_count_report (struct ResponseContext *rctx, + json_t *charts) +{ + const char *prefix = "transaction-state-"; + enum GNUNET_DB_QueryStatus qs; + json_t *chart; + json_t *labels; + + rctx->data_groups = json_array (); + GNUNET_assert (NULL != rctx->data_groups); + qs = TMH_db->lookup_statistics_counter_by_bucket2 ( + TMH_db->cls, + rctx->hc->instance->settings.id, + prefix, /* prefix to match against bucket name */ + rctx->granularity, + rctx->count, + &count_by_bucket, + rctx); + if (0 > qs) + { + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error ( + rctx->hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_XXX")) + ? GNUNET_NO : GNUNET_SYSERR; + } + + labels = json_array (); + GNUNET_assert (NULL != labels); + for (unsigned int i=0; i<rctx->labels_cnt; i++) + { + const char *label = rctx->labels[i]; + + /* This condition should always hold. */ + if (0 == + strncmp (prefix, + label, + strlen (prefix))) + label += strlen (prefix); + GNUNET_assert (0 == + json_array_append_new (labels, + json_string (label))); + GNUNET_free (rctx->labels[i]); + } + GNUNET_array_grow (rctx->labels, + rctx->labels_cnt, + 0); + chart = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("chart_name", + "Transaction counts"), + GNUNET_JSON_pack_string ("y_label", + "Number of transactions"), + GNUNET_JSON_pack_array_steal ("data_groups", + rctx->data_groups), + GNUNET_JSON_pack_array_steal ("labels", + labels), + GNUNET_JSON_pack_bool ("cumulative", + false)); + rctx->data_groups = NULL; + GNUNET_assert (0 == + json_array_append_new (charts, + chart)); + return GNUNET_OK; +} + + +/** + * Handle a GET "/private/statistics-report/transactions" 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_report_transactions ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct ResponseContext *rctx = hc->ctx; + struct TMH_MerchantInstance *mi = hc->instance; + json_t *charts; + + if (NULL != rctx) + { + if (NULL == rctx->response) + { + GNUNET_break (0); + return MHD_NO; + } + return MHD_queue_response (connection, + rctx->http_status, + rctx->response); + } + rctx = GNUNET_new (struct ResponseContext); + rctx->hc = hc; + rctx->now = GNUNET_TIME_timestamp_get (); + hc->ctx = rctx; + hc->cc = &free_rc; + GNUNET_assert (NULL != mi); + + rctx->granularity = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "granularity"); + if (NULL == rctx->granularity) + { + rctx->granularity = "day"; + rctx->period = GNUNET_TIME_UNIT_DAYS; + rctx->count = 95; + } + else + { + const struct + { + const char *name; + struct GNUNET_TIME_Relative period; + uint64_t default_counter; + } map[] = { + { + .name = "second", + .period = GNUNET_TIME_UNIT_SECONDS, + .default_counter = 120, + }, + { + .name = "minute", + .period = GNUNET_TIME_UNIT_MINUTES, + .default_counter = 120, + }, + { + .name = "hour", + .period = GNUNET_TIME_UNIT_HOURS, + .default_counter = 48, + }, + { + .name = "day", + .period = GNUNET_TIME_UNIT_DAYS, + .default_counter = 95, + }, + { + .name = "month", + .period = GNUNET_TIME_UNIT_MONTHS, + .default_counter = 36, + }, + { + .name = "quarter", + .period = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MONTHS, + 3), + .default_counter = 40, + }, + { + .name = "year", + .period = GNUNET_TIME_UNIT_YEARS, + .default_counter = 10 + }, + { + .name = NULL + } + }; + + rctx->count = 0; + for (unsigned int i = 0; map[i].name != NULL; i++) + { + if (0 == strcasecmp (map[i].name, + rctx->granularity)) + { + rctx->count = map[i].default_counter; + rctx->period = map[i].period; + break; + } + } + if (0 == rctx->count) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "granularity"); + } + } /* end handling granularity */ + + /* Figure out desired output format */ + { + const char *mime; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "application/json"; + if (0 == strcmp (mime, + "application/json")) + { + rctx->format = RCF_JSON; + } + else if (0 == strcmp (mime, + "application/pdf")) + { + + rctx->format = RCF_PDF; + } + else + { + GNUNET_break_op (0); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_NOT_ACCEPTABLE, + GNUNET_JSON_pack_string ("hint", + mime)); + } + } /* end of determine output format */ + + TALER_MHD_parse_request_number (connection, + "count", + &rctx->count); + + /* create charts */ + charts = json_array (); + GNUNET_assert (NULL != charts); + { + enum GNUNET_GenericReturnValue ret; + + ret = make_transaction_volume_report (rctx, + charts); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + ret = make_transaction_count_report (rctx, + charts); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + } + + /* generate response */ + { + struct GNUNET_TIME_Timestamp start_date; + struct GNUNET_TIME_Timestamp end_date; + json_t *root; + + end_date = rctx->now; + start_date + = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_subtract ( + end_date.abs_time, + GNUNET_TIME_relative_multiply (rctx->period, + rctx->count))); + root = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("business_name", + mi->settings.name), + GNUNET_JSON_pack_timestamp ("start_date", + start_date), + GNUNET_JSON_pack_timestamp ("end_date", + end_date), + GNUNET_JSON_pack_time_rel ("bucket_period", + rctx->period), + GNUNET_JSON_pack_array_steal ("charts", + charts)); + + switch (rctx->format) + { + case RCF_JSON: + return TALER_MHD_reply_json (connection, + root, + MHD_HTTP_OK); + case RCF_PDF: + { + struct TALER_MHD_TypstDocument doc = { + .form_name = "transactions", + .data = root + }; + + GNUNET_CONTAINER_DLL_insert (rctx_head, + rctx_tail, + rctx); + MHD_suspend_connection (connection); + rctx->tc = TALER_MHD_typst (TMH_cfg, + false, /* remove on exit */ + "merchant", + 1, /* one document, length of "array"! */ + &doc, + &pdf_cb, + rctx); + json_decref (root); + if (NULL == rctx->tc) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client requested PDF, but Typst is unavailable\n"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_IMPLEMENTED, + TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK, + NULL); + } + return MHD_YES; + } + } /* end switch */ + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_private-get-statistics-report-transactions.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.h b/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.h @@ -0,0 +1,49 @@ +/* + 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-report-transactions.h + * @brief implement GET /statistics-report/transactions + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_REPORT_TRANSACTIONS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_REPORT_TRANSACTIONS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/statistics-report/transactions" 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_report_transactions ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Cleanup ongoing report requests. + */ +void +TMH_handler_statistic_report_transactions_cleanup (void); + +/* end of taler-merchant-httpd_private-get-statistics-report-transactions.h */ +#endif diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am @@ -197,6 +197,8 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_lookup_refunds.h pg_lookup_refunds.c \ pg_lookup_refunds_detailed.h pg_lookup_refunds_detailed.c \ pg_lookup_spent_tokens_by_order.h pg_lookup_spent_tokens_by_order.c \ + pg_lookup_statistics_amount_by_bucket2.h pg_lookup_statistics_amount_by_bucket2.c \ + pg_lookup_statistics_counter_by_bucket2.h pg_lookup_statistics_counter_by_bucket2.c \ pg_lookup_template.h pg_lookup_template.c \ pg_lookup_templates.h pg_lookup_templates.c \ pg_lookup_token_families.h pg_lookup_token_families.c \ diff --git a/src/backenddb/example-statistics-0001.sql b/src/backenddb/example-statistics-0001.sql @@ -43,8 +43,8 @@ VALUES ('deposits' ,'sales (before refunds)' ,'amount' - ,ARRAY['second'::statistic_range, 'minute' 'day', 'month', 'quarter', 'year'] - ,ARRAY[120, 120, 95, 36, 40, 100] -- track last 120 s, 120 minutes, 95 days, 36 months, 40 quarters & 100 years + ,ARRAY['second'::statistic_range, 'minute', 'hour', 'day', 'month', 'quarter', 'year'] + ,ARRAY[120, 120, 48, 95, 36, 40, 100] -- track last 120 s, 120 minutes, 48 hours, 95 days, 36 months, 40 quarters & 10 years ); INSERT INTO merchant.merchant_statistic_interval_meta @@ -58,7 +58,7 @@ VALUES ,'sales (before refunds)' ,'amount' ,ARRAY[1,60, 24 * 60 * 60, 30 * 24 * 60 * 60, 365 * 24 * 60 * 60] -- second, minute, day, month, year - ,ARRAY[1,1, 60, 60 * 60, 24 * 60 * 60] -- second, second, minute, hour, day + ,ARRAY[1,1, 60, 60 * 60, 24 * 60 * 60] -- second, second, minute, hour, day ); CREATE FUNCTION merchant_deposits_statistics_trigger() diff --git a/src/backenddb/pg_lookup_statistics_amount_by_bucket.c b/src/backenddb/pg_lookup_statistics_amount_by_bucket.c @@ -28,7 +28,7 @@ /** - * Context used for TMH_PG_lookup_statistics_amount(). + * Context used for TMH_PG_lookup_statistics_amount_by_bucket(). */ struct LookupAmountStatisticsContext { @@ -58,7 +58,7 @@ struct LookupAmountStatisticsContext * 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[in,out] cls of type `struct LookupAmountStatisticsContext *` * @param result the postgres result * @param num_results the number of results in @a result */ diff --git a/src/backenddb/pg_lookup_statistics_amount_by_bucket2.c b/src/backenddb/pg_lookup_statistics_amount_by_bucket2.c @@ -0,0 +1,168 @@ +/* + 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_bucket2.c + * @brief Implementation of the lookup_statistics_amount_by_bucket2 function for Postgres + * @author Christian Grothoff + */ +#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_bucket2.h" +#include "pg_helper.h" + + +/** + * Context used for TMH_PG_lookup_statistics_amount_by_bucket2(). + */ +struct LookupAmountStatisticsContext2 +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_AmountByBucketStatisticsCallback2 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 LookupAmountStatisticsContext2 *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_statistics_amount_by_bucket_cb2 (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupAmountStatisticsContext2 *tflc = cls; + struct PostgresClosure *pg = tflc->pg; + + for (unsigned int i = 0; i < num_results; i++) + { + struct TALER_Amount *amounts; + size_t num_amounts; + uint64_t bucket_start_epoch; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("bucket_start", + &bucket_start_epoch), + TALER_PQ_result_spec_array_amount_with_currency (pg->conn, + "cumulative_amounts", + &num_amounts, + &amounts), + GNUNET_PQ_result_spec_end + }; + struct GNUNET_TIME_Timestamp bucket_start; + + 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); + tflc->cb (tflc->cb_cls, + bucket_start, + num_amounts, + amounts); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_amount_by_bucket2 ( + void *cls, + const char *instance_id, + const char *slug, + const char *granularity, + uint64_t counter, + TALER_MERCHANTDB_AmountByBucketStatisticsCallback2 cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupAmountStatisticsContext2 context = { + .cb = cb, + .cb_cls = cb_cls, + /* Can be overwritten by the lookup_statistics_amount_by_bucket_cb2 */ + .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_string (granularity), + GNUNET_PQ_query_param_uint64 (&counter), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_statistics_amount_by_bucket2", + "SELECT" + " bucket_start" + " ARRAY_AGG(" + " ROW(cumulative_value, cumulative_frac, curr)::taler_amount_currency" + ") AS cumulative_amounts" + " FROM merchant_statistic_bucket_meta" + " JOIN merchant_instances" + " USING (merchant_serial)" + " JOIN merchant_statistic_bucket_amount" + " USING (bmeta_serial_id)" + " WHERE merchant_instances.merchant_id=$1" + " AND merchant_statistic_bucket_meta.slug=$2" + " AND merchant_statistic_bucket_meta.bucket_range=$3::TEXT::bucket_range" + " AND merchant_statistic_bucket_meta.stype='amount'" + " GROUP BY bucket_start" + " ORDER BY bucket_start DESC" + " LIMIT $4;"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "lookup_statistics_amount_by_bucket2", + params, + &lookup_statistics_amount_by_bucket_cb2, + &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_bucket2.h b/src/backenddb/pg_lookup_statistics_amount_by_bucket2.h @@ -0,0 +1,52 @@ +/* + 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_bucket2.h + * @brief implementation of the lookup_statistics_amount_by_bucket2 function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_LOOKUP_STATISTICS_AMOUNT_BY_BUCKET2_H +#define PG_LOOKUP_STATISTICS_AMOUNT_BY_BUCKET2_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + + +/** + * Lookup amount statistics for instance and slug by bucket, + * restricting to a fixed number of buckets at a given granularity. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug instance to lookup statistics for + * @param granularity limit to buckets of this size + * @param counter requested number of buckets + * @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 +TMH_PG_lookup_statistics_amount_by_bucket2 ( + void *cls, + const char *instance_id, + const char *slug, + const char *granularity, + uint64_t counter, + TALER_MERCHANTDB_AmountByBucketStatisticsCallback2 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 @@ -28,7 +28,7 @@ /** - * Context used for TMH_PG_lookup_statistics_counter(). + * Context used for TMH_PG_lookup_statistics_counter_by_bucket(). */ struct LookupCounterStatisticsContext { @@ -53,7 +53,7 @@ struct LookupCounterStatisticsContext * 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[in,out] cls of type `struct LookupCounterStatisticsContext *` * @param result the postgres result * @param num_results the number of results in @a result */ @@ -147,10 +147,8 @@ TMH_PG_lookup_statistics_counter_by_bucket ( " 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'"); + " 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", diff --git a/src/backenddb/pg_lookup_statistics_counter_by_bucket2.c b/src/backenddb/pg_lookup_statistics_counter_by_bucket2.c @@ -0,0 +1,181 @@ +/* + 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_bucket2.c + * @brief Implementation of the lookup_statistics_counter_by_bucket2 function for Postgres + * @author Christian Grothoff + */ +#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_bucket2.h" +#include "pg_helper.h" + +/** + * Context used for TMH_PG_lookup_statistics_counter_by_bucket2(). + */ +struct LookupCounterStatisticsContext2 +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_CounterByBucketStatisticsCallback2 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 LookupCounterStatisticsContext2 *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_statistics_counter_by_bucket_cb2 (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupCounterStatisticsContext2 *tflc = cls; + struct PostgresClosure *pg = tflc->pg; + + for (unsigned int i = 0; i < num_results; i++) + { + size_t num_slugs; + char *slugs; + size_t num_counters; + uint64_t *counters; + uint64_t bucket_start_epoch; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("bucket_start", + &bucket_start_epoch), + GNUNET_PQ_result_spec_array_uint64 (pg->conn, + "counters", + &num_counters, + &counters), + GNUNET_PQ_result_spec_array_string (pg->conn, + "slugs", + &num_slugs, + &slugs), + GNUNET_PQ_result_spec_end + }; + struct GNUNET_TIME_Timestamp bucket_start; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + tflc->extract_failed = true; + return; + } + if (num_slugs != num_counters) + { + GNUNET_break (0); + tflc->extract_failed = true; + GNUNET_PQ_cleanup_result (rs); + return; + } + + bucket_start = GNUNET_TIME_timestamp_from_s (bucket_start_epoch); + tflc->cb (tflc->cb_cls, + bucket_start, + num_slugs, + (const char **) slugs, + counters); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_statistics_counter_by_bucket2 ( + void *cls, + const char *instance_id, + const char *prefix, + const char *granularity, + uint64_t counter, + TALER_MERCHANTDB_CounterByBucketStatisticsCallback2 cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct LookupCounterStatisticsContext2 context = { + .cb = cb, + .cb_cls = cb_cls, + /* Can be overwritten by the lookup_statistics_counter_by_bucket_cb2 */ + .extract_failed = false, + .pg = pg + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_string (prefix), + GNUNET_PQ_query_param_string (granularity), + GNUNET_PQ_query_param_uint64 (&counter), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_statistics_counter_by_bucket2", + "SELECT" + " bucket_start" + " ARRAY_AGG(slug) AS slugs" + " ARRAY_AGG(cumulative_number) AS counters" + " 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 LIKE $1 || '%'" + " AND merchant_statistic_bucket_meta.bucket_range=$3::TEXT::bucket_range" + " AND merchant_statistic_bucket_meta.stype = 'number'" + " GROUP BY bucket_start" + " ORDER BY bucket_start DESC" + " LIMIT $4"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "lookup_statistics_counter_by_bucket2", + params, + &lookup_statistics_counter_by_bucket_cb2, + &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_bucket2.h b/src/backenddb/pg_lookup_statistics_counter_by_bucket2.h @@ -0,0 +1,52 @@ +/* + 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_bucket2.h + * @brief implementation of the lookup_statistics_counter_by_bucket2 function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_LOOKUP_STATISTICS_COUNTER_BY_BUCKET2_H +#define PG_LOOKUP_STATISTICS_COUNTER_BY_BUCKET2_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + + +/** + * Lookup counter statistics for instance and slug-prefix by bucket. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param prefix slug prefix to lookup statistics under + * @param granularity limit to buckets of this size + * @param counter requested number of buckets + * @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 +TMH_PG_lookup_statistics_counter_by_bucket2 ( + void *cls, + const char *instance_id, + const char *prefix, + const char *granularity, + uint64_t counter, + TALER_MERCHANTDB_CounterByBucketStatisticsCallback2 cb, + void *cb_cls); + + +#endif diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -105,6 +105,8 @@ #include "pg_lookup_all_products.h" #include "pg_lookup_product.h" #include "pg_lookup_product_image.h" +#include "pg_lookup_statistics_amount_by_bucket2.h" +#include "pg_lookup_statistics_counter_by_bucket2.h" #include "pg_delete_product.h" #include "pg_insert_product.h" #include "pg_update_product.h" @@ -705,6 +707,10 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_lookup_reports_pending; plugin->update_report_status = &TMH_PG_update_report_status; + plugin->lookup_statistics_amount_by_bucket2 + = &TMH_PG_lookup_statistics_amount_by_bucket2; + plugin->lookup_statistics_counter_by_bucket2 + = &TMH_PG_lookup_statistics_counter_by_bucket2; plugin->select_report = &TMH_PG_select_report; plugin->insert_product_group diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -1629,6 +1629,23 @@ typedef void /** + * Returns amount-valued statistics by bucket. + * Called by `lookup_statistics_amount_by_bucket2`. + * + * @param cls closure + * @param bucket_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_AmountByBucketStatisticsCallback2)( + void *cls, + struct GNUNET_TIME_Timestamp bucket_start, + 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`. * @@ -1736,6 +1753,25 @@ typedef void /** + * Function returning integer-valued statistics for a bucket. + * Called by `lookup_statistics_counter_by_bucket2`. + * + * @param cls closure + * @param bucket_start start time of the bucket + * @param counters_len the length of @a cumulative_amounts + * @param descriptions description for the counter in the bucket + * @param counters the counters in the bucket + */ +typedef void +(*TALER_MERCHANTDB_CounterByBucketStatisticsCallback2)( + void *cls, + struct GNUNET_TIME_Timestamp bucket_start, + unsigned int counters_len, + const char *descriptions[static counters_len], + uint64_t counters[static counters_len]); + + +/** * Handle to interact with the database. * * Functions ending with "_TR" run their OWN transaction scope @@ -4882,6 +4918,30 @@ struct TALER_MERCHANTDB_Plugin void *cb_cls); /** + * Lookup amount statistics for instance and slug by bucket, + * restricting to a fixed number of buckets at a given granularity. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param slug instance to lookup statistics for + * @param granularity limit to buckets of this size + * @param counter requested number of buckets + * @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_bucket2)( + void *cls, + const char *instance_id, + const char *slug, + const char *granularity, + uint64_t counter, + TALER_MERCHANTDB_AmountByBucketStatisticsCallback2 cb, + void *cb_cls); + + + /** * Lookup counter statistics for instance and slug by bucket. * * @param cls closure @@ -4900,6 +4960,29 @@ struct TALER_MERCHANTDB_Plugin void *cb_cls); /** + * Lookup counter statistics for instance and slug-prefix by bucket. + * + * @param cls closure + * @param instance_id instance to lookup statistics for + * @param prefix slug prefix to lookup statistics under + * @param granularity limit to buckets of this size + * @param counter requested number of buckets + * @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_bucket2)( + void *cls, + const char *instance_id, + const char *prefix, + const char *granularity, + uint64_t counter, + TALER_MERCHANTDB_CounterByBucketStatisticsCallback2 cb, + void *cb_cls); + + + /** * Lookup amount statistics for instance and slug by interval. * * @param cls closure