commit 0b9eed10e4c11b5566bc9f23de2edf1c24dc639f parent 39e0e5efa399dc340124923befba529aa032b1e9 Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com> Date: Sat, 21 Jun 2025 14:42:59 +0200 Merge branch 'master' into dev/bohdan-potuzhnyi/donau-integration Diffstat:
78 files changed, 3888 insertions(+), 417 deletions(-)
diff --git a/configure.ac b/configure.ac @@ -18,7 +18,7 @@ # This configure file is in the public domain AC_PREREQ([2.69]) -AC_INIT([taler-merchant],[0.14.11],[taler-bug@gnunet.org]) +AC_INIT([taler-merchant],[1.0.3],[taler-bug@gnunet.org]) AC_CONFIG_SRCDIR([src/backend/taler-merchant-httpd.c]) AC_CONFIG_HEADERS([taler_merchant_config.h]) # support for non-recursive builds @@ -321,17 +321,18 @@ AS_CASE([$with_exchange], CPPFLAGS="-I$with_exchange/include $CPPFLAGS $POSTGRESQL_CPPFLAGS"]) AC_CHECK_HEADERS([taler/taler_mhd_lib.h], - [AC_CHECK_LIB([talermhd], [TALER_MHD_arg_to_yna], libtalermhd=1)]) + [AC_CHECK_LIB([talermhd], [TALER_MHD_listen_bind], libtalermhd=1)]) AM_CONDITIONAL(HAVE_TALERMHD, test x$libtalermhd = x1) AS_IF([test $libtalermhd != 1], [AC_MSG_ERROR([[ *** -*** You need libtalermhd >= 0.14.7 (API v3) to build this program. +*** You need libtalermhd >= 1.1.0 (API v6) to build this program. *** This library is part of the GNU Taler exchange, available at *** https://taler.net *** ]])]) libtalerjson=0 +AC_MSG_CHECKING([for libtalerjson]) AC_CHECK_HEADERS([taler/taler_json_lib.h], [AC_CHECK_LIB([talerjson], [TALER_JSON_spec_otp_type], libtalerjson=1)]) AM_CONDITIONAL(HAVE_TALERJSON, test x$libtalerjson = x1) diff --git a/contrib/bump-taler-version.sh b/contrib/bump-taler-version.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# This file is in the public domain. set -eu if [ $# != 1 ]; then diff --git a/contrib/check-prebuilt b/contrib/check-prebuilt @@ -9,7 +9,7 @@ merchant_spa_ver_lock = open(contrib + "/" + "merchant-spa.lock").read().strip() merchant_spa_ver_prebuilt = open(contrib + "/" + "wallet-core/backoffice/version.txt").read().strip() if merchant_spa_ver_lock != merchant_spa_ver_prebuilt: - print("merchant SPA version mismatch") + print("merchant SPA version mismatch: merchant-spa.lock") print("lockfile has version", merchant_spa_ver_lock) print("prebuilt has version", merchant_spa_ver_prebuilt) sys.exit(1) diff --git a/contrib/ci/Containerfile b/contrib/ci/Containerfile @@ -41,7 +41,9 @@ RUN pip3 install --break-system-packages requests click poetry uwsgi htmlark RUN apt-get update -yqq && \ apt-get install -yqq \ graphviz \ + lcov \ doxygen \ + rsync \ && rm -rf /var/lib/apt/lists/* # Install Taler (and friends) packages diff --git a/contrib/ci/jobs/1-build/build.sh b/contrib/ci/jobs/1-build/build.sh @@ -4,7 +4,9 @@ set -exuo pipefail ./bootstrap ./configure CFLAGS="-ggdb -O0" \ --prefix=/usr \ + --enable-coverage \ --enable-logging=verbose \ --disable-doc make + diff --git a/contrib/ci/jobs/2-test/1-build.sh b/contrib/ci/jobs/2-test/1-build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -evu + +apt-get update +apt-get upgrade -yqq + +./bootstrap +./configure CFLAGS="-ggdb -O0" \ + --prefix=/usr \ + --enable-coverage \ + --enable-logging=verbose \ + --disable-doc diff --git a/contrib/ci/jobs/2-test/2-install.sh b/contrib/ci/jobs/2-test/2-install.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -evux + +make install diff --git a/contrib/ci/jobs/2-test/3-startdb.sh b/contrib/ci/jobs/2-test/3-startdb.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -evux + +sudo -u postgres /usr/lib/postgresql/15/bin/postgres -D /etc/postgresql/15/main -h localhost -p 5432 & +sleep 10 +sudo -u postgres createuser -p 5432 root +sudo -u postgres createdb -p 5432 -O root talercheck diff --git a/contrib/ci/jobs/2-test/4-test.sh b/contrib/ci/jobs/2-test/4-test.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -evux + +check_command() +{ + # Set LD_LIBRARY_PATH so tests can find the installed libs + LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/taler:/usr/lib:/usr/lib/taler PGPORT=5432 make check +} + +print_logs() +{ + for i in src/*/test-suite.log + do + for FAILURE in $(grep '^FAIL:' ${i} | cut -d' ' -f2) + do + echo "Printing ${FAILURE}.log" + cat "$(dirname $i)/${FAILURE}.log" + done + done +} + +if ! check_command ; then + print_logs + exit 1 +fi diff --git a/contrib/ci/jobs/2-test/5-coverage.sh b/contrib/ci/jobs/2-test/5-coverage.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -evux + +ARTIFACT_PATH="/artifacts/merchant/${CI_COMMIT_REF}/merchant" +mkdir -p /artifacts/merchant/lcov/${CI_COMMIT_REF}/merchant # Variable comes from CI environment +lcov --capture --directory . --output-file coverage.info || exit 1 +genhtml coverage.info --output-directory ${ARTIFACT_PATH} || exit 1 + + +RSYNC_HOST="taler.host.internal" +RSYNC_PORT=424243 +RSYNC_PATH="incoming_taler/" +RSYNC_DEST="rsync://${RSYNC_HOST}/${RSYNC_PATH}" + + +rsync -rvP \ + --port ${RSYNC_PORT} \ + ${ARTIFACT_PATH} ${RSYNC_DEST} || exit 1 + + diff --git a/contrib/ci/jobs/2-test/job.sh b/contrib/ci/jobs/2-test/job.sh @@ -3,4 +3,8 @@ set -exuo pipefail job_dir=$(dirname "${BASH_SOURCE[0]}") -"${job_dir}"/test.sh +. "${job_dir}"/1-build.sh +. "${job_dir}"/2-install.sh +. "${job_dir}"/3-startdb.sh +. "${job_dir}"/4-test.sh +. "${job_dir}"/5-coverage.sh diff --git a/contrib/ci/jobs/2-test/test.sh b/contrib/ci/jobs/2-test/test.sh @@ -1,40 +0,0 @@ -#!/bin/bash -set -evu - -apt-get update -apt-get upgrade -yqq - -./bootstrap -./configure CFLAGS="-ggdb -O0" \ - --prefix=/usr \ - --enable-logging=verbose \ - --disable-doc -make -j install - -sudo -u postgres /usr/lib/postgresql/15/bin/postgres -D /etc/postgresql/15/main -h localhost -p 5432 & -sleep 10 -sudo -u postgres createuser -p 5432 root -sudo -u postgres createdb -p 5432 -O root talercheck - -check_command() -{ - # Set LD_LIBRARY_PATH so tests can find the installed libs - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/taler:/usr/lib:/usr/lib/taler PGPORT=5432 make check -} - -print_logs() -{ - for i in src/*/test-suite.log - do - for FAILURE in $(grep '^FAIL:' ${i} | cut -d' ' -f2) - do - echo "Printing ${FAILURE}.log" - cat "$(dirname $i)/${FAILURE}.log" - done - done -} - -if ! check_command ; then - print_logs - exit 1 -fi diff --git a/contrib/merchant-spa.lock b/contrib/merchant-spa.lock @@ -1 +1 @@ -0.14.15-dev.3 +1.0.18 diff --git a/debian/changelog b/debian/changelog @@ -1,3 +1,51 @@ +taler-merchant (1.0.3) unstable; urgency=low + + * Release 1.0.3. + + -- Florian Dold <florian@dold.me> Mon, 16 Jun 2025 22:46:38 +0200 + +taler-merchant (1.0.2) unstable; urgency=low + + * Release 1.0.2. + + -- Florian Dold <florian@dold.me> Wed, 04 Jun 2025 23:07:09 +0200 + +taler-merchant (1.0.1) unstable; urgency=low + + * Release 1.0.1. + + -- Florian Dold <florian@dold.me> Mon, 26 May 2025 14:03:37 +0200 + +taler-merchant (1.0.0) unstable; urgency=low + + * Release 1.0.0. + + -- Christian Grothoff <christian@grothoff.org> Fri, 09 May 2025 23:44:27 +0200 + +taler-merchant (0.14.15) unstable; urgency=low + + * Release 0.14.15. + + -- Christian Grothoff <christian@grothoff.org> Thu, 08 May 2025 17:29:25 +0200 + +taler-merchant (0.14.14) unstable; urgency=low + + * Release 0.14.14. + + -- Florian Dold <florian@dold.me> Wed, 07 May 2025 21:45:12 +0200 + +taler-merchant (0.14.13) unstable; urgency=low + + * Update to latest SPA + + -- Christian Grothoff <grothoff@gnu.org> Thu, 01 May 2025 20:31:25 +0200 + +taler-merchant (0.14.12) unstable; urgency=low + + * Disable long polling in test mode. + + -- Christian Grothoff <grothoff@gnu.org> Thu, 01 May 2025 15:51:25 +0200 + taler-merchant (0.14.11) unstable; urgency=low * Fix retries when updating exchange keys in database diff --git a/debian/control b/debian/control @@ -9,7 +9,7 @@ Build-Depends: debhelper-compat (= 12), gettext, libgnunet-dev (>=0.24.0), - libtalerexchange-dev (>=0.14.7), + libtalerexchange-dev (>=1.0.0), libpq-dev (>=15.0), po-debconf, libqrencode-dev, @@ -48,7 +48,7 @@ Pre-Depends: ${misc:Pre-Depends} Depends: libtalermerchant (= ${binary:Version}), - libtalerexchange (>= 0.14.7), + libtalerexchange (>= 1.0.0), adduser, lsb-base, netbase, @@ -69,7 +69,7 @@ Package: libtalermerchant-dev Section: libdevel Architecture: any Depends: - libtalerexchange-dev (>= 0.14.7), + libtalerexchange-dev (>= 1.0.0), libgnunet-dev (>=0.24.0), ${misc:Depends}, ${shlibs:Depends} diff --git a/debian/taler-merchant.prerm b/debian/taler-merchant.prerm @@ -1,22 +1,20 @@ #!/bin/sh - set -e MARKER="/run/taler/merchant.was-enabled" -if [ -d /run/systemd/system ] && [ "$1" = remove ]; then - deb-systemd-invoke stop 'taler-merchant*' >/dev/null || true -fi - -if [ -d /run/systemd/system ] && [ "$1" = upgrade ]; then - - if systemctl is-enabled taler-merchant-httpd.service >/dev/null 2>&1; then - echo "taler-merchant-httpd was enabled before upgrade/remove." - mkdir -p /run/taler - echo "enabled" > "$MARKER" - fi - - systemctl disable --now taler-merchant.target || true +if [ -d /run/systemd/system ]; then + case "$1" in + remove|upgrade|deconfigure) + if systemctl is-enabled --quiet taler-merchant.target; then + echo "taler-merchant.target was enabled before $1." + mkdir -p /run/taler + echo enabled > "$MARKER" + systemctl disable --now taler-merchant.target || true + fi + ;; + esac fi +#DEBHELPER# exit 0 diff --git a/debian/taler-merchant.taler-merchant-dbinit-gc.service b/debian/taler-merchant.taler-merchant-dbinit-gc.service @@ -1,9 +1,10 @@ [Unit] Description=Job to remove stale data from the taler-merchant database (run as a timer) +PartOf=taler-merchant.target [Service] Type=simple ExecStart=taler-merchant-dbinit -c /etc/taler-merchant/taler-merchant.conf -g StandardOutput=journal -StandardError=journal -\ No newline at end of file +StandardError=journal diff --git a/debian/taler-merchant.taler-merchant-depositcheck.service b/debian/taler-merchant.taler-merchant-depositcheck.service @@ -1,6 +1,7 @@ [Unit] Description=GNU Taler payment system merchant deposit check service After=postgres.service +PartOf=taler-merchant.target [Service] User=taler-merchant-httpd @@ -17,4 +18,4 @@ RuntimeMaxSec=3600s Slice=taler-merchant.slice StandardOutput=journal -StandardError=journal -\ No newline at end of file +StandardError=journal diff --git a/debian/taler-merchant.taler-merchant-exchangekeyupdate.service b/debian/taler-merchant.taler-merchant-exchangekeyupdate.service @@ -1,6 +1,7 @@ [Unit] Description=GNU Taler merchant exchange configuration data download service After=postgres.service +PartOf=taler-merchant.target [Service] User=taler-merchant-httpd diff --git a/debian/taler-merchant.taler-merchant-kyccheck.service b/debian/taler-merchant.taler-merchant-kyccheck.service @@ -1,6 +1,7 @@ [Unit] Description=GNU Taler merchant KYC status check service After=postgres.service +PartOf=taler-merchant.target [Service] User=taler-merchant-httpd @@ -17,4 +18,4 @@ RuntimeMaxSec=3600s Slice=taler-merchant.slice StandardOutput=journal -StandardError=journal -\ No newline at end of file +StandardError=journal diff --git a/debian/taler-merchant.taler-merchant-reconciliation.service b/debian/taler-merchant.taler-merchant-reconciliation.service @@ -1,6 +1,7 @@ [Unit] Description=GNU Taler merchant transaction reconciliation service After=postgres.service +PartOf=taler-merchant.target [Service] User=taler-merchant-httpd diff --git a/debian/taler-merchant.taler-merchant-webhook.service b/debian/taler-merchant.taler-merchant-webhook.service @@ -1,6 +1,7 @@ [Unit] Description=GNU Taler payment system merchant backend webhook trigger service After=postgres.service +PartOf=taler-merchant.target [Service] User=taler-merchant-httpd diff --git a/debian/taler-merchant.taler-merchant-wirewatch.service b/debian/taler-merchant.taler-merchant-wirewatch.service @@ -1,6 +1,7 @@ [Unit] Description=GNU Taler payment system merchant bank transfer import service After=postgres.service +PartOf=taler-merchant.target [Service] User=taler-merchant-httpd @@ -18,4 +19,4 @@ Slice=taler-merchant.slice StandardOutput=journal -StandardError=journal -\ No newline at end of file +StandardError=journal diff --git a/doc/doxygen/taler.doxy b/doc/doxygen/taler.doxy @@ -5,7 +5,7 @@ #--------------------------------------------------------------------------- DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "GNU Taler: Merchant" -PROJECT_NUMBER = 0.14.11 +PROJECT_NUMBER = 1.0.3 PROJECT_LOGO = logo.svg OUTPUT_DIRECTORY = . CREATE_SUBDIRS = YES diff --git a/m4/mhd.m4 b/m4/mhd.m4 @@ -1,7 +1,7 @@ # mhd.m4 # This file is part of TALER -# Copyright (C) 2022 Taler Systems SA +# Copyright (C) 2022, 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 @@ -14,36 +14,56 @@ # You should have received a copy of the GNU General Public License along with # TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/license> -# serial 1 +# serial 2 dnl MHD_VERSION_AT_LEAST([VERSION]) dnl -dnl Check that microhttpd.h can be used to build a program that prints out -dnl the MHD_VERSION tuple in X.Y.Z format, and that X.Y.Z is greater or equal -dnl to VERSION. If not, display message and cause the configure script to -dnl exit failurefully. +dnl Check that microhttpd.h defines MHD_VERSION and compare it against +dnl the required version without executing compiled code. +dnl This allows for cross-compilation scenarios to work correctly. dnl -dnl This uses AX_COMPARE_VERSION to do the job. -dnl It sets shell var mhd_cv_version, as well. +dnl If the check fails, display error message and exit. +dnl Uses AX_COMPARE_VERSION to compare versions. +dnl Sets shell var mhd_cv_version with detected version. dnl AC_DEFUN([MHD_VERSION_AT_LEAST], -[AC_CACHE_CHECK([libmicrohttpd version],[mhd_cv_version], - [AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - #include <stdio.h> - #include <microhttpd.h> -]],[[ - int v = MHD_VERSION; - printf ("%x.%x.%x\n", - (v >> 24) & 0xff, - (v >> 16) & 0xff, - (v >> 8) & 0xff); -]])], - [mhd_cv_version=$(./conftest)], - [mhd_cv_version=0])]) -AX_COMPARE_VERSION([$mhd_cv_version],[ge],[$1],, - [AC_MSG_ERROR([[ +[ + AC_MSG_CHECKING([for libmicrohttpd version >= $1]) + + # Try to get version by compiling & running a test program (won't work for cross-compilation) + AC_RUN_IFELSE([AC_LANG_PROGRAM([[ + #include <stdio.h> + #include <microhttpd.h> + ]],[[ + int v = MHD_VERSION; + FILE *f = fopen("conftest.out", "w"); + if (f == NULL) return 1; + fprintf(f, "%d.%d.%d\n", + (v >> 24) & 0xff, + (v >> 16) & 0xff, + (v >> 8) & 0xff); + fclose(f); + return 0; + ]])], + [mhd_cv_version=$(cat conftest.out) + rm -f conftest.out], + [mhd_cv_version=0], + [ + # Cross compiling mode - compute version via macros + AC_COMPUTE_INT([mhd_major], [(MHD_VERSION >> 24) & 0xff], [[#include <microhttpd.h>]], [mhd_major=0]) + AC_COMPUTE_INT([mhd_minor], [(MHD_VERSION >> 16) & 0xff], [[#include <microhttpd.h>]], [mhd_minor=0]) + AC_COMPUTE_INT([mhd_micro], [(MHD_VERSION >> 8) & 0xff], [[#include <microhttpd.h>]], [mhd_micro=0]) + mhd_cv_version="$mhd_major.$mhd_minor.$mhd_micro" + ]) + + # Compare version and report result + AX_COMPARE_VERSION([$mhd_cv_version],[ge],[$1], + [AC_MSG_RESULT([yes ($mhd_cv_version)])], + [AC_MSG_RESULT([no ($mhd_cv_version)]) + AC_MSG_ERROR([[ *** *** You need libmicrohttpd >= $1 to build this program. -*** ]])])]) +*** ]])]) +]) # mhd.m4 ends here diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -10,7 +10,8 @@ pkgcfgdir = $(prefix)/share/taler-merchant/config.d/ pkgcfg_DATA = \ kudos.conf \ - merchant.conf + merchant.conf \ + tops.conf EXTRA_DIST = \ $(pkgcfg_DATA) @@ -201,6 +202,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/merchant.conf b/src/backend/merchant.conf @@ -59,3 +59,13 @@ WIRE_TRANSFER_DELAY = 3 week # How fast do we want customers to pay, i.e. how long will our # proposal be valid? DEFAULT_PAY_DEADLINE = 1 day + +[merchant-kyccheck] + +# How long do we wait between AML status requests to the +# exchange if we expect a status change? +AML_FREQ = 6h + +# How long do we wait between AML status requests to the +# exchange if we do not expect any AML status changes? +AML_LOW_FREQ = 7d diff --git a/src/backend/taler-merchant-depositcheck.c b/src/backend/taler-merchant-depositcheck.c @@ -823,6 +823,8 @@ child_done_cb (void *cls, struct Child *c = cls; c->cwh = NULL; + GNUNET_OS_process_destroy (c->process); + c->process = NULL; if ( (GNUNET_OS_PROCESS_EXITED != type) || (0 != exit_code) ) { @@ -835,8 +837,6 @@ child_done_cb (void *cls, global_ret = EXIT_NOTINSTALLED; return; } - GNUNET_OS_process_destroy (c->process); - c->process = NULL; if (test_mode && (! GNUNET_TIME_relative_is_zero (c->rd)) ) { diff --git a/src/backend/taler-merchant-exchangekeyupdate.c b/src/backend/taler-merchant-exchangekeyupdate.c @@ -529,6 +529,7 @@ cert_cb ( e->exchange_url, keys->currency, e->currency); + TALER_EXCHANGE_keys_decref (keys); break; } if (0 != GNUNET_memcmp (&keys->master_pub, @@ -537,6 +538,7 @@ cert_cb ( GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Master public key in %skeys response does not match. Ignoring response.\n", e->exchange_url); + TALER_EXCHANGE_keys_decref (keys); break; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -548,9 +550,10 @@ cert_cb ( first_retry)) { GNUNET_break (0); + TALER_EXCHANGE_keys_decref (keys); break; } - e->keys = TALER_EXCHANGE_keys_incref (keys); + e->keys = keys; /* Reset back-off */ e->retry_delay = EXCHANGE_MAXFREQ; /* limit retry */ @@ -566,6 +569,7 @@ cert_cb ( end_inquiry (); return; default: + GNUNET_break (NULL == keys); break; } /* Try again (soon-ish) */ 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" @@ -172,9 +174,9 @@ unsigned int TMH_num_cspecs; struct TALER_CurrencySpecification *TMH_cspecs; /** - * The port we are running on + * True if we started any HTTP daemon. */ -static uint16_t port; +static bool have_daemons; /** * Should a "Connection: close" header be added to each HTTP response? @@ -398,8 +400,8 @@ TMH_instance_free_cb (void *cls, /** - * Shutdown task (magically invoked when the application is being - * quit) + * Shutdown task (invoked when the application is being + * terminated for any reason) * * @param cls NULL */ @@ -407,6 +409,7 @@ static void do_shutdown (void *cls) { (void) cls; + TALER_MHD_daemons_halt (); TMH_force_orders_resume (); TMH_force_ac_resume (); TMH_force_pc_resume (); @@ -414,13 +417,7 @@ do_shutdown (void *cls) TMH_force_gorc_resume (); TMH_force_wallet_get_order_resume (); TMH_force_wallet_refund_order_resume (); - { - struct MHD_Daemon *mhd; - - mhd = TALER_MHD_daemon_stop (); - if (NULL != mhd) - MHD_stop_daemon (mhd); - } + TALER_MHD_daemons_destroy (); if (NULL != instance_eh) { TMH_db->event_listen_cancel (instance_eh); @@ -1365,6 +1362,20 @@ url_handler (void *cls, .handler = &TMH_private_delete_donau_instance_ID }, #endif + /* 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 } @@ -2224,6 +2235,46 @@ TMH_reload_instances (const char *id) /** + * Callback invoked on every listen socket to start the + * respective MHD HTTP daemon. + * + * @param cls unused + * @param lsock the listen socket + */ +static void +start_daemon (void *cls, + int lsock) +{ + struct MHD_Daemon *mhd; + + (void) cls; + GNUNET_assert (-1 != lsock); + mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK + | MHD_USE_AUTO, + 0 /* port */, + NULL, NULL, + &url_handler, NULL, + MHD_OPTION_LISTEN_SOCKET, lsock, + MHD_OPTION_URI_LOG_CALLBACK, + &full_url_track_callback, NULL, + MHD_OPTION_NOTIFY_COMPLETED, + &handle_mhd_completion_callback, NULL, + MHD_OPTION_CONNECTION_TIMEOUT, + (unsigned int) 10 /* 10s */, + MHD_OPTION_END); + if (NULL == mhd) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to launch HTTP service.\n"); + GNUNET_SCHEDULER_shutdown (); + return; + } + have_daemons = true; + TALER_MHD_daemon_start (mhd); +} + + +/** * Main function that will be run by the scheduler. * * @param cls closure @@ -2238,7 +2289,6 @@ run (void *cls, const char *cfgfile, const struct GNUNET_CONFIGURATION_Handle *config) { - int fh; enum TALER_MHD_GlobalOptions go; int elen; const char *tok; @@ -2412,42 +2462,34 @@ run (void *cls, load_instances (NULL, NULL, 0); - fh = TALER_MHD_bind (cfg, - "merchant", - &port); - if ( (0 == port) && - (-1 == fh) ) { - GNUNET_SCHEDULER_shutdown (); - return; - } + enum GNUNET_GenericReturnValue ret; - { - struct MHD_Daemon *mhd; - - mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK - | MHD_USE_AUTO, - port, - NULL, NULL, - &url_handler, NULL, - MHD_OPTION_LISTEN_SOCKET, fh, - MHD_OPTION_URI_LOG_CALLBACK, - &full_url_track_callback, NULL, - MHD_OPTION_NOTIFY_COMPLETED, - &handle_mhd_completion_callback, NULL, - MHD_OPTION_CONNECTION_TIMEOUT, - (unsigned int) 10 /* 10s */, - MHD_OPTION_END); - if (NULL == mhd) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to launch HTTP service. Is the port in use?\n"); + ret = TALER_MHD_listen_bind (cfg, + "merchant", + &start_daemon, + NULL); + switch (ret) + { + case GNUNET_SYSERR: + global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; + case GNUNET_NO: + if (! have_daemons) + { + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Could not open all configured listen sockets\n"); + break; + case GNUNET_OK: + break; } - global_ret = EXIT_SUCCESS; - TALER_MHD_daemon_start (mhd); } + global_ret = EXIT_SUCCESS; } diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c @@ -43,7 +43,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "18:0:15" +#define MERCHANT_PROTOCOL_VERSION "18:1:15" /** @@ -83,36 +83,16 @@ MH_handler_config (const struct TMH_RequestHandler *rh, struct TMH_HandlerContext *hc) { static struct MHD_Response *response; - static struct GNUNET_TIME_Absolute a; (void) rh; (void) hc; - if ( (GNUNET_TIME_absolute_is_past (a)) && - (NULL != response) ) - { - MHD_destroy_response (response); - response = NULL; - } if (NULL == response) { json_t *specs = json_object (); json_t *exchanges = json_array (); - struct GNUNET_TIME_Timestamp km; - char dat[128]; GNUNET_assert (NULL != specs); GNUNET_assert (NULL != exchanges); - a = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_DAYS); - /* Round up to next full day to ensure the expiration - time does not become a fingerprint! */ - a = GNUNET_TIME_absolute_round_down (a, - GNUNET_TIME_UNIT_DAYS); - a = GNUNET_TIME_absolute_add (a, - GNUNET_TIME_UNIT_DAYS); - /* => /config response stays at most 48h in caches! */ - km = GNUNET_TIME_absolute_to_timestamp (a); - TALER_MHD_get_date_string (km.abs_time, - dat); TMH_exchange_get_trusted (&add_exchange, exchanges); for (unsigned int i = 0; i<TMH_num_cspecs; i++) @@ -121,11 +101,11 @@ MH_handler_config (const struct TMH_RequestHandler *rh, if (TMH_test_exchange_configured_for_currency (cspec->currency)) GNUNET_assert (0 == - json_object_set_new (specs, - cspec->currency, - TALER_CONFIG_currency_specs_to_json - ( - cspec))); + json_object_set_new ( + specs, + cspec->currency, + TALER_CONFIG_currency_specs_to_json + (cspec))); } response = TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_string ("currency", @@ -141,14 +121,6 @@ MH_handler_config (const struct TMH_RequestHandler *rh, "taler-merchant"), GNUNET_JSON_pack_string ("version", MERCHANT_PROTOCOL_VERSION)); - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_EXPIRES, - dat)); - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_CACHE_CONTROL, - "public,max-age=21600")); /* 6h */ } return MHD_queue_response (connection, MHD_HTTP_OK, diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c @@ -676,11 +676,7 @@ TMH_exchange_check_debit ( struct TALER_Amount *max_amount) { const struct TALER_EXCHANGE_Keys *keys = exchange->keys; - struct TALER_NormalizedPayto np; - bool account_ok; bool have_kyc = false; - struct TALER_Amount kyc_limit; - bool unlimited = true; bool no_access_token = true; if (NULL == keys) @@ -695,15 +691,22 @@ TMH_exchange_check_debit ( max_amount->currency); return GNUNET_SYSERR; } + { + struct TALER_NormalizedPayto np; + bool account_ok; + + np = TALER_payto_normalize (wm->payto_uri); + account_ok = TALER_EXCHANGE_keys_test_account_allowed (keys, + false, + np); + GNUNET_free (np.normalized_payto); + if (! account_ok) + return GNUNET_NO; + } + if (! keys->kyc_enabled) + return GNUNET_YES; - np = TALER_payto_normalize (wm->payto_uri); - account_ok = TALER_EXCHANGE_keys_test_account_allowed (keys, - false, - np); - GNUNET_free (np.normalized_payto); - if (keys->kyc_enabled) { - bool kyc_ok = false; json_t *jlimits = NULL; enum GNUNET_DB_QueryStatus qs; @@ -711,7 +714,7 @@ TMH_exchange_check_debit ( wm->payto_uri, instance_id, exchange->url, - &kyc_ok, + &have_kyc, &no_access_token, &jlimits); GNUNET_break (qs >= 0); @@ -719,13 +722,15 @@ TMH_exchange_check_debit ( "get_kyc_limits for %s at %s returned %s/%s\n", wm->payto_uri.full_payto, exchange->url, - kyc_ok ? "KYC OK" : "KYC missing", + have_kyc ? "KYC OK" : "KYC missing", NULL == jlimits ? "default limits" : "custom limits"); if ( (qs > 0) && (NULL != jlimits) ) { json_t *jlimit; size_t idx; + struct TALER_Amount kyc_limit; + bool unlimited = true; json_array_foreach (jlimits, idx, jlimit) { @@ -774,64 +779,62 @@ TMH_exchange_check_debit ( } } json_decref (jlimits); - } - if (kyc_ok) - have_kyc = true; + /* We had custom rules, do not evaluate default rules */ + if (! unlimited) + TALER_amount_min (max_amount, + max_amount, + &kyc_limit); + return GNUNET_YES; + } /* END of if qs > 0, NULL != jlimits */ } - if (! unlimited) - TALER_amount_min (max_amount, - max_amount, - &kyc_limit); - if (keys->kyc_enabled) - { - /* apply both deposit and transaction limits */ - if ( (no_access_token) || - ( (! have_kyc) && - (TALER_EXCHANGE_keys_evaluate_zero_limits ( - keys, - TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) || - TALER_EXCHANGE_keys_evaluate_zero_limits ( - keys, - TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION)) ) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC requirements of %s not satisfied\n", - exchange->url); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero ( - max_amount->currency, - max_amount)); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Evaluating default limits of %s\n", - exchange->url); - TALER_EXCHANGE_keys_evaluate_hard_limits ( - keys, - TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT, - max_amount); - TALER_EXCHANGE_keys_evaluate_hard_limits ( - keys, - TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION, - max_amount); - if (TALER_EXCHANGE_keys_evaluate_zero_limits ( + /* Check zero limits *only* if we did no KYC process at all yet. + Because if we did, there is at least a chance that those have + been lifted. */ + if ( (no_access_token) || + ( (! have_kyc) && + (TALER_EXCHANGE_keys_evaluate_zero_limits ( keys, TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) || TALER_EXCHANGE_keys_evaluate_zero_limits ( keys, - TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Operation is zero-limited by default\n"); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (max_amount->currency, - max_amount)); - } - } + TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION)) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC requirements of %s not satisfied\n", + exchange->url); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ( + max_amount->currency, + max_amount)); + return GNUNET_YES; + } + /* In any case, abide by hard limits (unless we have custom rules). */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Evaluating default hard limits of %s\n", + exchange->url); + TALER_EXCHANGE_keys_evaluate_hard_limits ( + keys, + TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT, + max_amount); + TALER_EXCHANGE_keys_evaluate_hard_limits ( + keys, + TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION, + max_amount); + if (TALER_EXCHANGE_keys_evaluate_zero_limits ( + keys, + TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) || + TALER_EXCHANGE_keys_evaluate_zero_limits ( + keys, + TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Operation is zero-limited by default\n"); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (max_amount->currency, + max_amount)); } - return account_ok ? GNUNET_YES : GNUNET_NO; + return GNUNET_YES; } diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c @@ -302,11 +302,8 @@ suspend_god (struct GetOrderData *god) if (NULL != god->contract_terms_json) { json_decref (god->contract_terms_json); - god->contract_terms->fulfillment_url = NULL; god->contract_terms_json = NULL; god->contract_parsed = false; - god->contract_terms->merchant_base_url = NULL; - god->contract_terms->public_reorder_url = NULL; } if (NULL != god->contract_terms) { @@ -731,6 +728,7 @@ phase_lookup_terms (struct GetOrderData *god) static void phase_parse_contract (struct GetOrderData *god) { + GNUNET_break (NULL == god->contract_terms); god->contract_terms = TALER_MERCHANT_contract_parse ( god->contract_terms_json, true); diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -59,19 +59,23 @@ #define MAX_RETRIES 5 /** - * Maximum number of coins that we allow per transaction + * Maximum number of coins that we allow per transaction. + * Note that the limit for each batch deposit request to + * the exchange is lower, so we may break a very large + * number of coins up into multiple smaller requests to + * the exchange. */ #define MAX_COIN_ALLOWED_COINS 1024 /** * Maximum number of tokens that we allow as inputs per transaction */ -#define MAX_TOKEN_ALLOWED_INPUTs 128 +#define MAX_TOKEN_ALLOWED_INPUTs 64 /** * Maximum number of tokens that we allow as outputs per transaction */ -#define MAX_TOKEN_ALLOWED_OUTPUTs 128 +#define MAX_TOKEN_ALLOWED_OUTPUTs 64 /** * How often do we ask the exchange again about our @@ -1498,7 +1502,8 @@ AGE_FAIL: } return; } - + if (group_size > TALER_MAX_COINS) + group_size = TALER_MAX_COINS; { struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size]; struct TALER_EXCHANGE_DepositContractDetail dcd = { @@ -1526,8 +1531,9 @@ AGE_FAIL: if (0 != strcmp (dc->exchange_url, eg->exchange_url)) continue; - GNUNET_assert (off < group_size); cdds[off++] = dc->cdd; + if (off >= group_size) + break; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Initiating batch deposit with %u coins\n", @@ -2323,8 +2329,7 @@ check_payment_sufficient (struct PayContext *pc) "Total refunded amount: %s\n", TALER_amount2s (&pc->pay_transaction.total_refunded)); - /* Now compare exchange wire fee compared to - * what we are willing to pay */ + /* Now compare exchange wire fee compared to what we are willing to pay */ if (GNUNET_YES != TALER_amount_cmp_currency (&total_wire_fee, &acc_fee)) @@ -4274,6 +4279,13 @@ pay_context_cleanup (void *cls) GNUNET_free (dc->exchange_url); } GNUNET_free (pc->parse_pay.dc); + for (unsigned int i = 0; i<pc->parse_pay.tokens_cnt; i++) + { + struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; + + TALER_token_issue_sig_free (&tuc->unblinded_sig); + } + GNUNET_free (pc->parse_pay.tokens); for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) { struct ExchangeGroup *eg = pc->parse_pay.egs[i]; diff --git a/src/backend/taler-merchant-httpd_post-using-templates.c b/src/backend/taler-merchant-httpd_post-using-templates.c @@ -46,6 +46,10 @@ struct UseContext */ struct TALER_MERCHANTDB_TemplateDetails etp; + /** + * True once @e etp was initialized. + */ + bool have_etp; }; @@ -79,7 +83,6 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, const char *fulfillment_message = NULL; struct TALER_Amount amount; bool no_amount; - json_t *fake_body; bool no_summary; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( @@ -117,6 +120,7 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, } } + if (! uc->have_etp) { enum GNUNET_DB_QueryStatus qs; @@ -154,10 +158,11 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, template_id); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: /* all good */ + uc->have_etp = true; break; } /* End of the switch */ } - + if (NULL == uc->ihc.request_body) { /* template */ const char *tsummary = NULL; @@ -185,6 +190,7 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, &pay_duration), GNUNET_JSON_spec_end () }; + json_t *fake_body; { enum GNUNET_GenericReturnValue res; @@ -291,11 +297,11 @@ TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, fulfillment_message)) )) ); + uc->ihc.request_body = fake_body; } - uc->ihc.request_body = fake_body; return TMH_private_post_orders ( - NULL, /* not even used */ + NULL, /* not even used */ connection, &uc->ihc); } diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c @@ -31,25 +31,6 @@ #include <regex.h> /** - * We do not re-check an acceptable KYC status for - * a month, as usually a KYC never expires. - */ -#define STALE_KYC_TIMEOUT GNUNET_TIME_UNIT_MONTHS - -/** - * How long should clients cache a KYC failure response? - */ -#define EXPIRATION_KYC_FAILURE GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_MINUTES, 5) - -/** - * How long should clients cache a KYC success response? - */ -#define EXPIRATION_KYC_SUCCESS GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_HOURS, 1) - - -/** * Information we keep per /kyc request. */ struct KycContext; @@ -363,24 +344,10 @@ kyc_context_cleanup (void *cls) static void resume_kyc_with_response (struct KycContext *kc) { - char dat[128]; - kc->response_code = MHD_HTTP_OK; kc->response = TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_array_incref ("kyc_data", kc->kycs_data)); - /* KYC failed, cache briefly */ - TALER_MHD_get_date_string (GNUNET_TIME_relative_to_absolute ( - EXPIRATION_KYC_FAILURE), - dat); - GNUNET_break (MHD_YES == - MHD_add_response_header (kc->response, - MHD_HTTP_HEADER_EXPIRES, - dat)); - GNUNET_break (MHD_YES == - MHD_add_response_header (kc->response, - MHD_HTTP_HEADER_CACHE_CONTROL, - "max-age=300")); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Resuming /kyc handling as exchange interaction is done (%u)\n", MHD_HTTP_OK); @@ -564,7 +531,15 @@ map_to_status (const struct ExchangeKycRequest *ekr) if ( (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) || (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_AGGREGATE) || (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION) ) + { + if (! ekr->auth_ok) + { + if (ekr->kyc_auth_conflict) + return "kyc-wire-impossible"; + return "kyc-wire-required"; + } return "kyc-required"; + } } } if (NULL == ekr->jlimits) @@ -580,7 +555,15 @@ map_to_status (const struct ExchangeKycRequest *ekr) if ( (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) || (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_AGGREGATE) || (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION) ) + { + if (! ekr->auth_ok) + { + if (ekr->kyc_auth_conflict) + return "kyc-wire-impossible"; + return "kyc-wire-required"; + } return "kyc-required"; + } } } return "ready"; diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -474,6 +474,16 @@ static void gorc_cleanup (void *cls) { struct GetOrderRequestContext *gorc = cls; + struct TransferQuery *tq; + + while (NULL != (tq = gorc->tq_head)) + { + GNUNET_CONTAINER_DLL_remove (gorc->tq_head, + gorc->tq_tail, + tq); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + } if (NULL != gorc->contract_terms_json) json_decref (gorc->contract_terms_json); @@ -937,6 +947,7 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc) gorc->contract_terms->summary), GNUNET_JSON_pack_timestamp ("creation_time", gorc->contract_terms->timestamp)); + GNUNET_free (order_status_url); GNUNET_free (taler_pay_uri); GNUNET_free (already_paid_order_id); phase_end (gorc, @@ -998,6 +1009,7 @@ phase_unpaid_finish (struct GetOrderRequestContext *gorc) gorc->contract_terms_json), GNUNET_JSON_pack_string ("order_status", "claimed"))); + GNUNET_free (order_status_url); return; } taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con, @@ -1082,25 +1094,27 @@ process_refunds_cb ( NULL != tq; tq = tq->next) { - if (0 == + if (0 != + strcmp (exchange_url, + tq->exchange_url)) + continue; + if (0 != GNUNET_memcmp (&tq->coin_pub, coin_pub)) + continue; + if (GNUNET_OK != + TALER_amount_cmp_currency ( + &gorc->deposit_fees_total, + &tq->deposit_fee)) { - if (GNUNET_OK != - TALER_amount_cmp_currency ( - &gorc->deposit_fees_total, - &tq->deposit_fee)) - { - gorc->refund_currency_mismatch = true; - return; - } - - GNUNET_assert ( - 0 <= - TALER_amount_subtract (&gorc->deposit_fees_total, - &gorc->deposit_fees_total, - &tq->deposit_fee)); + gorc->refund_currency_mismatch = true; + return; } + GNUNET_assert ( + 0 <= + TALER_amount_subtract (&gorc->deposit_fees_total, + &gorc->deposit_fees_total, + &tq->deposit_fee)); } if (GNUNET_OK != TALER_amount_cmp_currency ( 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 (0 == + 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/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -78,7 +78,7 @@ * refuses a forced download. */ #define MAX_KEYS_WAIT \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500) + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500) /** * Generate the base URL for the given merchant instance. @@ -184,7 +184,7 @@ struct OrderContext /** * Shared key to use with @e pos_algorithm. */ - const char *pos_key; + char *pos_key; /** * Selected algorithm (by template) when we are to @@ -846,6 +846,7 @@ clean_order (void *cls) GNUNET_array_grow (oc->parse_request.uuids, oc->parse_request.uuids_length, 0); + GNUNET_free (oc->parse_request.pos_key); json_decref (oc->parse_request.order); json_decref (oc->serialize_order.contract); GNUNET_free (oc->parse_order.order_id); @@ -3887,6 +3888,9 @@ parse_request (struct OrderContext *oc) struct TALER_MERCHANTDB_OtpDeviceDetails td; enum GNUNET_DB_QueryStatus qs; + memset (&td, + 0, + sizeof (td)); qs = TMH_db->select_otp (TMH_db->cls, oc->hc->instance->settings.id, otp_id, @@ -3912,12 +3916,13 @@ parse_request (struct OrderContext *oc) MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, otp_id); - break; + return; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } oc->parse_request.pos_key = td.otp_key; oc->parse_request.pos_algorithm = td.otp_algorithm; + GNUNET_free (td.otp_description); } if (create_token) { diff --git a/src/backend/taler-merchant-kyccheck.c b/src/backend/taler-merchant-kyccheck.c @@ -43,21 +43,37 @@ /** * How long do we wait between requests if all we wait * for is a change in the AML investigation status? + * Default value. */ #define AML_FREQ GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_HOURS, \ 6) /** + * How long do we wait between requests if all we wait + * for is a change in the AML investigation status? + */ +static struct GNUNET_TIME_Relative aml_freq; + +/** * How frequently do we check for updates to our KYC status * if there is no actual reason to check? Set to a very low * frequency, just to ensure we eventually notice. + * Default value. */ #define AML_LOW_FREQ GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_DAYS, \ 7) /** + * How frequently do we check for updates to our KYC status + * if there is no actual reason to check? Set to a very low + * frequency, just to ensure we eventually notice. + */ +static struct GNUNET_TIME_Relative aml_low_freq; + + +/** * How many inquiries do we process concurrently at most. */ #define OPEN_INQUIRY_LIMIT 1024 @@ -526,12 +542,15 @@ exchange_check_cb ( if (i->aml_review || i->zero_limited) { if (! progress) - i->due = GNUNET_TIME_relative_to_absolute (AML_FREQ); + i->due = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_randomize (aml_freq)); } else { /* KYC is OK, only check again if triggered */ - i->due = GNUNET_TIME_relative_to_absolute (AML_LOW_FREQ); + i->due = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_randomize ( + aml_low_freq)); } break; case MHD_HTTP_ACCEPTED: @@ -558,7 +577,8 @@ exchange_check_cb ( json_decref (i->jlimits); i->jlimits = NULL; /* KYC is OK, only check again if triggered */ - i->due = GNUNET_TIME_relative_to_absolute (AML_LOW_FREQ); + i->due = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_randomize (aml_low_freq)); break; case MHD_HTTP_FORBIDDEN: /* bad signature */ i->last_kyc_check = GNUNET_TIME_timestamp_get (); @@ -712,7 +732,7 @@ inquiry_work (void *cls) &i->a->ap, i->rule_gen, lpt, - i->not_first_time + i->not_first_time && (! test_mode) ? EXCHANGE_TIMEOUT : GNUNET_TIME_UNIT_ZERO, &exchange_check_cb, @@ -827,7 +847,8 @@ start_inquiry (struct Exchange *e, /* KYC is OFF, only check again if triggered */ if (GNUNET_YES != test_mode) { - i->due = GNUNET_TIME_relative_to_absolute (AML_LOW_FREQ); + i->due = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_randomize (aml_low_freq)); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC was disabled, randomizing inquiry to start at %s\n", GNUNET_TIME_absolute2s (i->due)); @@ -1013,6 +1034,8 @@ account_cb ( /** * The set of bank accounts has changed, update our * list of active inquiries. + * + * @param cls unused */ static void find_accounts (void *cls) @@ -1353,6 +1376,7 @@ shutdown_task (void *cls) a_tail, a); GNUNET_free (a->merchant_account_uri.full_payto); + GNUNET_free (a->instance_id); GNUNET_free (a); } if (NULL != eh_accounts) @@ -1409,6 +1433,41 @@ run (void *cls, (void) cfgfile; cfg = c; + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + "merchant-kyccheck", + "AML_FREQ", + &aml_freq)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + "merchant-kyccheck", + "AML_FREQ"); + /* use default */ + aml_freq = AML_FREQ; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + "merchant-kyccheck", + "AML_LOW_FREQ", + &aml_low_freq)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + "merchant-kyccheck", + "AML_LOW_FREQ"); + /* use default */ + aml_low_freq = AML_LOW_FREQ; + } + if (GNUNET_TIME_relative_cmp (aml_low_freq, + <, + aml_freq)) + { + aml_low_freq = GNUNET_TIME_relative_multiply (aml_freq, + 10); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "AML_LOW_FREQ was set to less than AML_FREQ. Using %s instead\n", + GNUNET_TIME_relative2s (aml_low_freq, + true)); + } GNUNET_SCHEDULER_add_shutdown (&shutdown_task, NULL); ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, diff --git a/src/backend/tops.conf b/src/backend/tops.conf @@ -0,0 +1,5 @@ +# CHF operated by Taler Operations AG +[merchant-exchange-chf] +EXCHANGE_BASE_URL = https://exchange.taler-ops.ch/ +MASTER_KEY = "9V0G82S7JQW2ZRYF7BMGKKQ1TNR1VNVXZJSNQ2VSDGWC80D9W0YG" +CURRENCY = CHF diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am @@ -75,7 +75,7 @@ libtalermerchantdb_la_LIBADD = \ libtalermerchantdb_la_LDFLAGS = \ $(POSTGRESQL_LDFLAGS) \ - -version-info 4:0:2 \ + -version-info 4:1:2 \ -no-undefined libtaler_plugin_merchantdb_postgres_la_SOURCES = \ @@ -205,6 +205,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 if HAVE_DONAU diff --git a/src/backenddb/merchant-0013.sql b/src/backenddb/merchant-0013.sql @@ -102,7 +102,7 @@ BEGIN url, http_method, body_template - FROM merchant_webhook + FROM merchant.merchant_webhook WHERE event_type = 'category_added' AND merchant_serial = my_merchant_serial LOOP @@ -122,7 +122,7 @@ BEGIN my_merchant_serial::TEXT); -- Insert into pending webhooks for this webhook - INSERT INTO merchant_pending_webhooks + INSERT INTO merchant.merchant_pending_webhooks (merchant_serial, webhook_serial, url, http_method, body) VALUES (webhook.merchant_serial, @@ -144,7 +144,7 @@ BEGIN url, http_method, body_template - FROM merchant_webhook + FROM merchant.merchant_webhook WHERE event_type = 'category_updated' AND merchant_serial = my_merchant_serial LOOP @@ -172,7 +172,7 @@ BEGIN 'escape')); -- Insert into pending webhooks for this webhook - INSERT INTO merchant_pending_webhooks + INSERT INTO merchant.merchant_pending_webhooks (merchant_serial, webhook_serial, url, http_method, body) VALUES (webhook.merchant_serial, @@ -194,7 +194,7 @@ BEGIN url, http_method, body_template - FROM merchant_webhook + FROM merchant.merchant_webhook WHERE event_type = 'category_deleted' AND merchant_serial = my_merchant_serial LOOP @@ -211,7 +211,7 @@ BEGIN OLD.category_name); -- Insert into pending webhooks for this webhook - INSERT INTO merchant_pending_webhooks + INSERT INTO merchant.merchant_pending_webhooks (merchant_serial, webhook_serial, url, http_method, body) VALUES (webhook.merchant_serial, @@ -258,7 +258,7 @@ BEGIN url, http_method, body_template - FROM merchant_webhook + FROM merchant.merchant_webhook WHERE event_type = 'inventory_added' AND merchant_serial = my_merchant_serial LOOP @@ -314,7 +314,7 @@ BEGIN NEW.minimum_age::TEXT); -- Insert into pending webhooks for this webhook - INSERT INTO merchant_pending_webhooks + INSERT INTO merchant.merchant_pending_webhooks (merchant_serial, webhook_serial, url, http_method, body) VALUES (webhook.merchant_serial, @@ -336,7 +336,7 @@ BEGIN url, http_method, body_template - FROM merchant_webhook + FROM merchant.merchant_webhook WHERE event_type = 'inventory_updated' AND merchant_serial = my_merchant_serial LOOP @@ -431,7 +431,7 @@ BEGIN NEW.minimum_age::TEXT); -- Insert into pending webhooks for this webhook - INSERT INTO merchant_pending_webhooks + INSERT INTO merchant.merchant_pending_webhooks (merchant_serial, webhook_serial, url, http_method, body) VALUES (webhook.merchant_serial, @@ -453,7 +453,7 @@ BEGIN url, http_method, body_template - FROM merchant_webhook + FROM merchant.merchant_webhook WHERE event_type = 'inventory_deleted' AND merchant_serial = my_merchant_serial LOOP @@ -509,7 +509,7 @@ BEGIN OLD.minimum_age::TEXT); -- Insert into pending webhooks for this webhook - INSERT INTO merchant_pending_webhooks + INSERT INTO merchant.merchant_pending_webhooks (merchant_serial, webhook_serial, url, http_method, body) VALUES (webhook.merchant_serial, diff --git a/src/backenddb/merchant-0018.sql b/src/backenddb/merchant-0018.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2024 Taler Systems SA +-- 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 @@ -15,9 +15,8 @@ -- -- @file merchant-0018.sql --- @brief Create table to store donau related information --- @author Bohdan Potuzhnyi --- @author Vlada Svirsh +-- @brief Tables for statistics +-- @author Christian Grothoff BEGIN; @@ -26,63 +25,12 @@ SELECT _v.register_patch('merchant-0018', NULL, NULL); SET search_path TO merchant; -CREATE TABLE IF NOT EXISTS merchant_donau_keys - (donau_keys_serial BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE - ,donau_url TEXT PRIMARY KEY - ,keys_json TEXT NOT NULL - ); +-- See #9865: inventory locks should not block product or even instance DELETION. +-- So we need to add the "ON DELETE CASCADE" after all. +ALTER TABLE merchant_inventory_locks + DROP CONSTRAINT merchant_inventory_locks_product_serial_fkey, + ADD CONSTRAINT merchant_inventory_locks_product_serial_fkey + FOREIGN KEY (product_serial) REFERENCES merchant_inventory(product_serial) + ON DELETE CASCADE; -COMMENT ON TABLE merchant_donau_keys - IS 'Here we store the cached /keys response from Donau in JSON format'; -COMMENT ON COLUMN merchant_donau_keys.donau_keys_serial - IS 'Unique serial identifier for each cached key entry'; -COMMENT ON COLUMN merchant_donau_keys.donau_url - IS 'Base URL of Donau associated with these keys'; -COMMENT ON COLUMN merchant_donau_keys.keys_json - IS 'JSON string of the /keys as generated by Donau'; - -CREATE TABLE IF NOT EXISTS merchant_donau_instances - (donau_instances_serial BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY - ,donau_url TEXT NOT NULL - ,charity_name TEXT NOT NULL - ,charity_pub_key BYTEA CHECK (LENGTH(charity_pub_key)=32) - ,charity_id BIGINT NOT NULL - ,charity_max_per_year taler_amount_currency NOT NULL - ,charity_receipts_to_date taler_amount_currency NOT NULL - ,current_year INT8 NOT NULL - ); - -COMMENT ON TABLE merchant_donau_instances - IS 'Here we store information about individual Donau instances, including details about associated charities and donation limits'; -COMMENT ON COLUMN merchant_donau_instances.donau_instances_serial - IS 'Unique serial identifier for each Donau instance'; -COMMENT ON COLUMN merchant_donau_instances.donau_url - IS 'The URL associated with the Donau system for this instance'; -COMMENT ON COLUMN merchant_donau_instances.charity_pub_key - IS 'The public key of the charity organization linked to this instance, with a 32-byte length constraint'; -COMMENT ON COLUMN merchant_donau_instances.charity_id - IS 'The unique identifier for the charity organization linked to this Donau instance'; -COMMENT ON COLUMN merchant_donau_instances.charity_max_per_year - IS 'Maximum allowable donation amount per year for the charity associated with this instance, stored in taler_amount_currency'; -COMMENT ON COLUMN merchant_donau_instances.charity_receipts_to_date - IS 'The total amount of donations received to date for this instance, stored in taler_amount_currency'; -COMMENT ON COLUMN merchant_donau_instances.current_year - IS 'The current year for tracking donations for this instance, stored as an 8-byte integer'; - -CREATE TABLE IF NOT EXISTS merchant_order_donau -(order_donau_serial BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY - ,order_serial BIGINT NOT NULL - REFERENCES merchant_orders (order_serial) ON DELETE CASCADE - ,donau_budis TEXT NOT NULL -); - -COMMENT ON TABLE merchant_order_donau - IS 'Table linking merchant orders with Donau BUDIS information'; -COMMENT ON COLUMN merchant_order_donau.order_donau_serial - IS 'Unique serial identifier for Donau order linkage'; -COMMENT ON COLUMN merchant_order_donau.order_serial - IS 'Foreign key linking to the corresponding merchant order'; -COMMENT ON COLUMN merchant_order_donau.donau_budis - IS 'Donau BUDIs json associated with the order'; - -COMMIT; -\ No newline at end of file +COMMIT; diff --git a/src/backenddb/merchantdb_helper.c b/src/backenddb/merchantdb_helper.c @@ -30,10 +30,13 @@ TALER_MERCHANTDB_product_details_free ( { GNUNET_free (pd->description); json_decref (pd->description_i18n); + pd->description_i18n = NULL; GNUNET_free (pd->unit); json_decref (pd->taxes); + pd->taxes = NULL; GNUNET_free (pd->image); json_decref (pd->address); + pd->address = NULL; } @@ -44,7 +47,9 @@ TALER_MERCHANTDB_template_details_free ( GNUNET_free (tp->template_description); GNUNET_free (tp->otp_id); json_decref (tp->editable_defaults); + tp->editable_defaults = NULL; json_decref (tp->template_contract); + tp->template_contract = NULL; } @@ -79,7 +84,9 @@ TALER_MERCHANTDB_token_family_details_free ( GNUNET_free (tf->name); GNUNET_free (tf->description); json_decref (tf->description_i18n); + tf->description_i18n = NULL; json_decref (tf->extra_data); + tf->extra_data = NULL; GNUNET_free (tf->cipher_spec); } @@ -90,6 +97,7 @@ TALER_MERCHANTDB_category_details_free ( { GNUNET_free (cd->category_name); json_decref (cd->category_name_i18n); + cd->category_name_i18n = NULL; } diff --git a/src/backenddb/pg_insert_transfer_details.sql b/src/backenddb/pg_insert_transfer_details.sql @@ -240,7 +240,10 @@ LOOP ,mw.webhook_serial ,mw.url ,mw.http_method - ,replace_placeholder(mw.body_template, 'order_id', my_order_id)::TEXT + ,replace_placeholder( + replace_placeholder(mw.body_template, 'order_id', my_order_id), + 'wtid', encode(in_wtid, 'hex') + )::TEXT FROM merchant_webhook mw WHERE mw.event_type = 'order_settled' AND mw.merchant_serial = my_merchant_serial; diff --git a/src/backenddb/pg_lookup_product.c b/src/backenddb/pg_lookup_product.c @@ -25,6 +25,7 @@ #include "pg_lookup_product.h" #include "pg_helper.h" + enum GNUNET_DB_QueryStatus TMH_PG_lookup_product (void *cls, const char *instance_id, @@ -83,17 +84,24 @@ TMH_PG_lookup_product (void *cls, } else { + char *my_description = NULL; + json_t *my_description_i18n = NULL; + char *my_unit = NULL; + char *my_image = NULL; + json_t *my_address = NULL; + json_t *my_taxes = NULL; + uint64_t *my_categories = NULL; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_string ("description", - &pd->description), + &my_description), TALER_PQ_result_spec_json ("description_i18n", - &pd->description_i18n), + &my_description_i18n), GNUNET_PQ_result_spec_string ("unit", - &pd->unit), + &my_unit), TALER_PQ_result_spec_amount_with_currency ("price", &pd->price), TALER_PQ_result_spec_json ("taxes", - &pd->taxes), + &my_taxes), GNUNET_PQ_result_spec_uint64 ("total_stock", &pd->total_stock), GNUNET_PQ_result_spec_uint64 ("total_sold", @@ -101,9 +109,9 @@ TMH_PG_lookup_product (void *cls, GNUNET_PQ_result_spec_uint64 ("total_lost", &pd->total_lost), GNUNET_PQ_result_spec_string ("image", - &pd->image), + &my_image), TALER_PQ_result_spec_json ("address", - &pd->address), + &my_address), GNUNET_PQ_result_spec_timestamp ("next_restock", &pd->next_restock), GNUNET_PQ_result_spec_uint32 ("minimum_age", @@ -111,14 +119,32 @@ TMH_PG_lookup_product (void *cls, GNUNET_PQ_result_spec_array_uint64 (pg->conn, "categories", num_categories, - categories), + &my_categories), GNUNET_PQ_result_spec_end }; + enum GNUNET_DB_QueryStatus qs; check_connection (pg); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_product", - params, - rs); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "lookup_product", + params, + rs); + pd->description = my_description; + pd->description_i18n = my_description_i18n; + pd->unit = my_unit; + pd->taxes = my_taxes; + pd->image = my_image; + pd->address = my_address; + *categories = my_categories; + /* Clear original pointers to that cleanup_result doesn't squash them */ + my_description = NULL; + my_description_i18n = NULL; + my_unit = NULL; + my_taxes = NULL; + my_image = NULL; + my_address = NULL; + my_categories = NULL; + GNUNET_PQ_cleanup_result (rs); + return qs; } } 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_lookup_token_family_keys.c b/src/backenddb/pg_lookup_token_family_keys.c @@ -150,8 +150,7 @@ lookup_token_keys_cb (void *cls, } ctx->cb (ctx->cb_cls, &details); - if (NULL != details.pub.public_key) /* guard against GNUnet 0.23 bug! */ - GNUNET_PQ_cleanup_result (rs); + GNUNET_PQ_cleanup_result (rs); } } 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 @@ -156,6 +156,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" #ifdef HAVE_DONAU_DONAU_SERVICE_H #include "donau/donau_service.h" @@ -643,6 +647,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; #ifdef HAVE_DONAU_DONAU_SERVICE_H diff --git a/src/bank/Makefile.am b/src/bank/Makefile.am @@ -10,7 +10,7 @@ lib_LTLIBRARIES = \ libtalermerchantbank.la libtalermerchantbank_la_LDFLAGS = \ - -version-info 0:0:0 \ + -version-info 0:1:0 \ -no-undefined libtalermerchantbank_la_SOURCES = \ mb_common.c mb_common.h \ diff --git a/src/bank/mb_credit.c b/src/bank/mb_credit.c @@ -310,10 +310,12 @@ TALER_MERCHANT_BANK_credit_history ( CURLOPT_TIMEOUT_MS, (long) (tms + 100L))); } +#if DEBUG GNUNET_break (CURLE_OK == curl_easy_setopt (eh, CURLOPT_VERBOSE, 1L)); +#endif hh->job = GNUNET_CURL_job_add2 (ctx, eh, NULL, diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -5762,5 +5762,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 @@ -1909,6 +1909,41 @@ TALER_TESTING_cmd_merchant_delete_donau_instance(const char *label, uint64_t charity_id, unsigned int expected_http_status); +/** + * 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 @@ -1298,17 +1298,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; }; @@ -1334,6 +1334,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. * @@ -3938,6 +4069,77 @@ struct TALER_MERCHANTDB_Plugin const uint64_t charity_id ); #endif + /** + * 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 @@ -24,7 +24,7 @@ lib_LTLIBRARIES = \ libtalermerchanttesting.la libtalermerchanttesting_la_LDFLAGS = \ - -version-info 4:0:1 \ + -version-info 4:1:1 \ -no-undefined libtalermerchanttesting_la_SOURCES = \ @@ -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-walletharness.sh b/src/testing/test-merchant-walletharness.sh @@ -36,7 +36,7 @@ taler-harness --help >/dev/null </dev/null || exit_skip " MISSING" echo " FOUND" -export WITH_LIBEUFIN=1 +export WITH_LIBEUFIN=0 res=0 taler-harness run-integrationtests --dry --suites merchant 2&>/dev/null || res=$? diff --git a/src/testing/test_kyc_api.conf b/src/testing/test_kyc_api.conf @@ -74,7 +74,7 @@ FALLBACK = manual-freeze # This check runs on oauth2 PROVIDER_ID = test-oauth2 # Outputs from this check -OUTPUTS = full_name birthdate +OUTPUTS = FULL_NAME DATE_OF_BIRTH [kyc-check-test-form] VOLUNTARY = NO @@ -89,7 +89,7 @@ FALLBACK = manual-freeze # This check runs on oauth2 FORM_NAME = full_name_and_birthdate # Outputs from this check -OUTPUTS = full_name birthdate +OUTPUTS = FULL_NAME DATE_OF_BIRTH # This is the "default" setting for an account if # it has not yet triggered anything. diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -237,7 +237,7 @@ run (void *cls, "4", GNUNET_TIME_UNIT_ZERO_TS, GNUNET_TIME_UNIT_FOREVER_TS, - "CHF:5.0"), + "XXX:5.0"), TALER_TESTING_cmd_merchant_post_orders_no_claim ( "create-proposal-4", merchant_url, @@ -2216,11 +2216,26 @@ run (void *cls, repurchase), TALER_TESTING_cmd_batch ("tokens", tokens), + #ifdef HAVE_DONAU_DONAU_SERVICE_H // TALER_TESTING_cmd_sleep("dream", 30), TALER_TESTING_cmd_batch ("donau", donau), #endif + + 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/test_merchant_api_twisted.c b/src/testing/test_merchant_api_twisted.c @@ -404,12 +404,10 @@ run (void *cls, "EUR:5", "EUR:4.99", NULL), - TALER_TESTING_cmd_malform_response ("malform-abort-merchant-exchange", - PROXY_EXCHANGE_config_file), TALER_TESTING_cmd_merchant_order_abort ("pay-abort-1", merchant_url, "deposit-2", - MHD_HTTP_BAD_GATEWAY), + MHD_HTTP_OK), TALER_TESTING_cmd_end () }; diff --git a/src/testing/test_merchant_product_creation.sh b/src/testing/test_merchant_product_creation.sh @@ -51,6 +51,9 @@ setup -c "test_template.conf" \ -r "merchant-exchange-default" \ -em \ $BANK_FLAGS + +bash + LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX) WALLET_DB=$(mktemp -p "${TMPDIR:-/tmp}" test_wallet.json-XXXXXX) CONF="test_template.conf.edited" diff --git a/src/testing/test_merchant_wirewatch.sh b/src/testing/test_merchant_wirewatch.sh @@ -45,7 +45,7 @@ else ACCOUNT="exchange-account-1" WIRE_METHOD="iban" - BANK_FLAGS="-ns -d $WIRE_METHOD -u $ACCOUNT" + BANK_FLAGS="-n -d $WIRE_METHOD -u $ACCOUNT" BANK_URL="http://localhost:18082/" fi 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 */ diff --git a/src/util/Makefile.am b/src/util/Makefile.am @@ -50,7 +50,7 @@ libtalermerchantutil_la_LIBADD = \ -ltalerutil \ $(XLIB) libtalermerchantutil_la_LDFLAGS = \ - -version-info 1:0:1 \ + -version-info 1:1:1 \ -export-dynamic -no-undefined test_contract_SOURCES = \