merchant

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

commit 9a07bb7df6e733c8c0d7b7eeb9f03bd0a9fa60e8
parent 9bce6572cf4e8760695dc3d5f33b2b60891341e4
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun,  4 Jan 2026 16:14:56 +0100

fix report generation logic

Diffstat:
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 5++++-
Msrc/backend/taler-merchant-httpd_private-get-statistics-report-transactions.c | 9++++-----
Asrc/backend/taler-merchant-report-generator-file | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/example-statistics-0001.sql | 32+-------------------------------
Msrc/backenddb/merchant-0028.sql | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_lookup_statistics_counter_by_bucket2.c | 26++++++++++++++++++--------
6 files changed, 237 insertions(+), 45 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -1152,6 +1152,8 @@ handle_batch_deposit_ok (struct ExchangeGroup *eg, resume_pay_with_error (pc, TALER_EC_GENERIC_DB_COMMIT_FAILED, "batch_deposit_transaction"); + TMH_db->rollback (TMH_db->cls); + return; } qs = TMH_db->commit (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -1250,7 +1252,8 @@ batch_deposit_cb ( case MHD_HTTP_OK: handle_batch_deposit_ok (eg, dr); - if (0 == pc->batch_deposits.pending_at_eg) + if ( (GNUNET_YES == pc->suspended) && + (0 == pc->batch_deposits.pending_at_eg) ) { pc->phase = PP_COMPUTE_MONEY_POTS; pay_resume (pc); diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.c b/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.c @@ -295,8 +295,7 @@ static enum GNUNET_GenericReturnValue make_transaction_volume_report (struct ResponseContext *rctx, json_t *charts) { - // FIXME: this is from example-statistics, we probably want to hard-wire the stats from this endpoint! - const char *bucket_name = "sales (before refunds)"; + const char *bucket_name = "deposits-received"; enum GNUNET_DB_QueryStatus qs; json_t *chart; json_t *labels; @@ -319,7 +318,7 @@ make_transaction_volume_report (struct ResponseContext *rctx, rctx->hc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_statistics_amount_by_bucket")) + "lookup_statistics_amount_by_bucket2")) ? GNUNET_NO : GNUNET_SYSERR; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) @@ -449,7 +448,7 @@ static enum GNUNET_GenericReturnValue make_transaction_count_report (struct ResponseContext *rctx, json_t *charts) { - const char *prefix = "transaction-state-"; + const char *prefix = "orders-paid"; enum GNUNET_DB_QueryStatus qs; json_t *chart; json_t *labels; @@ -472,7 +471,7 @@ make_transaction_count_report (struct ResponseContext *rctx, rctx->hc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_statistics_XXX")) + "lookup_statistics_counter_by_bucket2")) ? GNUNET_NO : GNUNET_SYSERR; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) diff --git a/src/backend/taler-merchant-report-generator-file b/src/backend/taler-merchant-report-generator-file @@ -0,0 +1,114 @@ +#!/bin/bash +# +# 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/> +# + +# +# Script to write Taler merchant reports to a file. +# Reads report data from stdin and writes it to a file. +# +# Usage: taler-merchant-report-generator-file -d DESCRIPTION -m MIME_TYPE -t TARGET_ADDRESS +# + +set -eu + +DESCRIPTION="" +MIME_TYPE="" +TARGET_ADDRESS="" +TMPDIR="${TMPDIR:-/tmp}" + +while getopts "d:m:t:h" opt; do + case $opt in + d) + DESCRIPTION="$OPTARG" + ;; + m) + MIME_TYPE="$OPTARG" + ;; + t) + TARGET_ADDRESS="$OPTARG" + ;; + h) + echo "Usage: $0 -d DESCRIPTION -m MIME_TYPE -t EMAIL_ADDRESS" + echo "" + echo "Sends reports via email." + echo "" + echo "Options:" + echo " -d DESCRIPTION Subject line for the email" + echo " -m MIME_TYPE MIME type of the report (e.g., text/plain, application/pdf)" + echo " -t EMAIL_ADDRESS Email address to send the report to" + echo " -h Show this help message" + echo "" + echo "The report data is read from stdin." + exit 0 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + echo "Use -h for help" >&2 + exit 1 + ;; + esac +done + +if [ -z "$DESCRIPTION" ]; +then + echo "Error: Description (-d) is required" >&2 + exit 1 +fi + +if [ -z "$MIME_TYPE" ]; +then + echo "Error: MIME type (-m) is required" >&2 + exit 1 +fi + +if [ -z "$TARGET_ADDRESS" ]; +then + echo "Error: Target address (-t) is required" >&2 + exit 1 +fi + +# Normalize MIME type to lowercase for comparison +MIME_TYPE=$(echo "$MIME_TYPE" | tr '[:upper:]' '[:lower:]') + +# Handle different MIME types +case "$MIME_TYPE" in + text/plain) + cat - > "$TARGET_ADDRESS.txt" + ;; + application/pdf) + cat - > "$TARGET_ADDRESS.pdf" + ;; + application/json) + cat - > "$TARGET_ADDRESS.json" + ;; + application/xml) + cat - > "$TARGET_ADDRESS.xml" + ;; + text/csv) + cat - > "$TARGET_ADDRESS.csv" + ;; + image/jpeg) + cat - > "$TARGET_ADDRESS.jpeg" + ;; + image/png) + cat - > "$TARGET_ADDRESS.png" + ;; + *) + cat - > "$TARGET_ADDRESS" + ;; +esac + +exit 0 diff --git a/src/backenddb/example-statistics-0001.sql b/src/backenddb/example-statistics-0001.sql @@ -61,37 +61,6 @@ VALUES ,ARRAY[1,1, 60, 60 * 60, 24 * 60 * 60] -- second, second, minute, hour, day ); -CREATE FUNCTION merchant_deposits_statistics_trigger() -RETURNS trigger -LANGUAGE plpgsql -AS $$ -DECLARE - my_instance INT8; -BEGIN - SELECT mct.merchant_serial - INTO my_instance - FROM merchant_contract_terms mct - JOIN merchant_deposit_confirmations mdc - USING (order_serial) - WHERE mdc.deposit_confirmation_serial = NEW.deposit_confirmation_serial; - CALL merchant_do_bump_amount_stat - ('deposits' - ,my_instance - ,NEW.amount_with_fee); - RETURN NEW; -END $$; -COMMENT ON FUNCTION merchant_deposits_statistics_trigger - IS 'adds the deposited amount to the deposit statistics'; - --- Whenever a deposit is made, call our trigger to bump statistics -CREATE TRIGGER merchant_deposits_on_insert - AFTER INSERT - ON merchant.merchant_deposits - FOR EACH ROW EXECUTE FUNCTION merchant_deposits_statistics_trigger(); - --- This is for now just an example for how to use the API. --- END EXAMPLE - - -- This is just another example for how to use the API. -- BEGIN EXAMPLE @@ -124,6 +93,7 @@ BEGIN CALL merchant_do_bump_number_stat ('products-sold' ,NEW.merchant_serial + ,CURRENT_TIMESTAMP(0)::TIMESTAMP ,my_sold); END IF; RETURN NEW; diff --git a/src/backenddb/merchant-0028.sql b/src/backenddb/merchant-0028.sql @@ -130,6 +130,102 @@ COMMENT ON COLUMN merchant_inventory.price_is_net IS 'If true, the price given is the net price; if false, it is the gross price.'; +CREATE FUNCTION merchant_deposits_insert_statistics_trigger() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + my_instance INT8; +BEGIN + SELECT mct.merchant_serial + INTO my_instance + FROM merchant_contract_terms mct + JOIN merchant_deposit_confirmations mdc + USING (order_serial) + WHERE mdc.deposit_confirmation_serial = NEW.deposit_confirmation_serial; + CALL merchant_do_bump_amount_stat + ('deposits-received' + ,my_instance + ,CURRENT_TIMESTAMP(0)::TIMESTAMP + ,NEW.amount_with_fee); + CALL merchant_do_bump_amount_stat + ('deposits-fees-paid' + ,my_instance + ,CURRENT_TIMESTAMP(0)::TIMESTAMP + ,NEW.deposit_fee); + RETURN NEW; +END $$; + + +-- Whenever a contract is updated, call our trigger to bump statistics +CREATE TRIGGER merchant_deposits_on_insert_statistic + AFTER INSERT + ON merchant_deposits + FOR EACH ROW EXECUTE FUNCTION merchant_deposits_insert_statistics_trigger(); + + +CREATE FUNCTION merchant_expected_transfers_insert_statistics_trigger() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + my_instance INT8; +BEGIN + SELECT mct.merchant_serial + INTO my_instance + FROM merchant_expected_transfers met + JOIN merchant_accounts ma + USING (account_serial) + WHERE met.expected_credit_serial = NEW.expected_credit_serial; + CALL merchant_do_bump_amount_stat + ('wire-fees-paid' + ,my_instance + ,CURRENT_TIMESTAMP(0)::TIMESTAMP + ,NEW.wire_fee); + RETURN NEW; +END $$; + + +-- Whenever a contract is updated, call our trigger to bump statistics +CREATE TRIGGER merchant_expected_transfers_on_insert_statistic + AFTER INSERT + ON merchant_expected_transfers + FOR EACH ROW EXECUTE FUNCTION merchant_expected_transfers_insert_statistics_trigger(); + + + +-- Enable additional bucket statistics +INSERT INTO merchant_statistic_bucket_meta + (slug + ,description + ,stype + ,ranges + ,ages) +VALUES + ('deposits-received' + ,'total amount customers deposited to us (including deposit fees)' + ,'amount' + ,ARRAY['hour'::statistic_range, 'day', 'week', 'month', 'quarter', 'year'] + ,ARRAY[72, 14, 12, 24, 12, 10] + ), + ('deposits-fees-paid' + ,'total amount in deposit fees paid by us or our customers' + ,'amount' + ,ARRAY['hour'::statistic_range, 'day', 'week', 'month', 'quarter', 'year'] + ,ARRAY[72, 14, 12, 24, 12, 10] + ), + ('wire-fees-paid' + ,'amount in wire fees paid by' + ,'amount' + ,ARRAY['hour'::statistic_range, 'day', 'week', 'month', 'quarter', 'year'] + ,ARRAY[72, 14, 12, 24, 12, 10] + ), + ('orders-paid' + ,'number of orders paid (but not necessarily settled by the exchange)' + ,'number' + ,ARRAY['hour'::statistic_range, 'day', 'week', 'month', 'quarter', 'year'] + ,ARRAY[72, 14, 12, 24, 12, 10] + ); diff --git a/src/backenddb/pg_lookup_statistics_counter_by_bucket2.c b/src/backenddb/pg_lookup_statistics_counter_by_bucket2.c @@ -107,13 +107,23 @@ lookup_statistics_counter_by_bucket_cb2 (void *cls, GNUNET_PQ_cleanup_result (rs); return; } - - bucket_start = GNUNET_TIME_timestamp_from_s (bucket_start_epoch); - tflc->cb (tflc->cb_cls, - bucket_start, - num_slugs, - (const char **) slugs, - counters); + { + const char *slugptrs[num_slugs]; + const char *pos = slugs; + + for (size_t j = 0; j<num_slugs; j++) + { + slugptrs[j] = pos; + pos += strlen (pos) + 1; + } + + bucket_start = GNUNET_TIME_timestamp_from_s (bucket_start_epoch); + tflc->cb (tflc->cb_cls, + bucket_start, + num_slugs, + slugptrs, + counters); + } GNUNET_PQ_cleanup_result (rs); } } @@ -159,7 +169,7 @@ TMH_PG_lookup_statistics_counter_by_bucket2 ( " JOIN merchant_instances mi" " USING (merchant_serial)" " WHERE mi.merchant_id=$1" - " AND msbm.slug LIKE $2 || '%'" + " AND msbm.slug LIKE $2" " AND msbc.bucket_range=$3::TEXT::statistic_range" " AND msbm.stype = 'number'" " GROUP BY msbc.bucket_start"