merchant

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

commit 104613dcc3a364a1ae098f2081579abf4e9bc2f7
parent 9dd99be8e97e12021326983dc4127dc8f7398e85
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Sat, 22 Mar 2025 06:46:25 +0100

-checkpoint

Diffstat:
Msrc/backenddb/merchant-0014.sql | 23+++++++++++++++++------
Msrc/backenddb/pg_statistics_examples.sql | 47+++++++++++++++++++++++++----------------------
Msrc/backenddb/pg_statistics_helpers.sql | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
3 files changed, 204 insertions(+), 63 deletions(-)

diff --git a/src/backenddb/merchant-0014.sql b/src/backenddb/merchant-0014.sql @@ -80,7 +80,7 @@ COMMENT ON COLUMN merchant_statistic_bucket_counter.bmeta_serial_id COMMENT ON COLUMN merchant_statistic_bucket_counter.merchant_serial IS 'identifies the instance for which the statistic is kept'; COMMENT ON COLUMN merchant_statistic_bucket_counter.bucket_start - IS 'start date for the bucket'; + IS 'start date for the bucket in seconds since the epoch'; COMMENT ON COLUMN merchant_statistic_bucket_counter.bucket_range IS 'range of the bucket'; COMMENT ON COLUMN merchant_statistic_bucket_counter.cumulative_number @@ -106,7 +106,7 @@ COMMENT ON COLUMN merchant_statistic_bucket_amount.bmeta_serial_id COMMENT ON COLUMN merchant_statistic_bucket_amount.merchant_serial IS 'identifies the instance for which the statistic is kept'; COMMENT ON COLUMN merchant_statistic_bucket_amount.bucket_start - IS 'start date for the bucket'; + IS 'start date for the bucket in seconds since the epoch'; COMMENT ON COLUMN merchant_statistic_bucket_amount.bucket_range IS 'range of the bucket'; COMMENT ON COLUMN merchant_statistic_bucket_amount.curr @@ -205,7 +205,7 @@ CREATE TABLE merchant_statistic_amount_event ,delta_curr VARCHAR(12) NOT NULL ,delta_value INT8 NOT NULL ,delta_frac INT4 NOT NULL - ,CONSTRAINT event_key UNIQUE (imeta_serial_id, merchant_serial, slot, delta_curr) + ,CONSTRAINT event_key UNIQUE (imeta_serial_id, merchant_serial, delta_curr, slot) ); COMMENT ON TABLE merchant_statistic_amount_event IS 'amount to decrement an interval statistic by when a certain time value is reached'; @@ -252,9 +252,20 @@ COMMENT ON COLUMN merchant_statistic_interval_amount.cumulative_value COMMENT ON COLUMN merchant_statistic_interval_amount.cumulative_frac IS 'amount in the respective currency, fraction in units of 1/100000000 of the base value'; +CREATE TYPE merchant_statistic_interval_number_get_return_value + AS + (range INT8 + ,value INT8 + ); +COMMENT ON TYPE merchant_statistic_interval_number_get_return_value + IS 'Return type for merchant_statistic_interval_number_get stored procedure'; --- FIXME: logic for statistics garbage collection (delete ancient buckets based on 'ages') is missing! (but not urgent at all). --- Note: calling merchant_deposit_statistic_get() on each statistic to delete events should be part of the GC --- (as otherwise events are kept around if the respective statistic is never requested). +CREATE TYPE merchant_statistic_interval_amount_get_return_value + AS + (range INT8 + ,rvalue taler_amount_currency + ); +COMMENT ON TYPE merchant_statistic_interval_amount_get_return_value + IS 'Return type for merchant_statistic_interval_amount_get stored procedure'; COMMIT; diff --git a/src/backenddb/pg_statistics_examples.sql b/src/backenddb/pg_statistics_examples.sql @@ -26,11 +26,11 @@ SELECT _v.register_patch('example-statistics-0001', NULL, NULL); CREATE SCHEMA example_statistics; -SET search_path TO example_statistics; +SET search_path TO example_statistics,merchant; -- Setup statistic: what do we want to track for 'deposits'? -- (Note: this is basically the one "manual" step we might not keep hard-coded) -INSERT INTO merchant.merchant_statistic_bucket_meta +INSERT INTO merchant_statistic_bucket_meta (slug ,description ,stype @@ -40,11 +40,11 @@ VALUES ('deposits' ,'sales (before refunds)' ,'amount' - ,ARRAY['second'::statistic_range, 'minute' 'day', 'month', 'quarter', 'year'] + ,ARRAY['second'::statistic_range, 'minute', 'day', 'month', 'quarter', 'year'] ,ARRAY[120, 120, 95, 36, 40, 100] -- track last 120 s, 120 minutes, 95 days, 36 months, 40 quarters & 100 years ); -INSERT INTO merchant.merchant_statistic_interval_meta +INSERT INTO merchant_statistic_interval_meta (slug ,description ,stype @@ -81,25 +81,28 @@ END $$; COMMENT ON FUNCTION merchant_deposits_statistics_trigger IS 'adds the deposited amount to the deposit statistics'; --- Import all existing deposits from the last year into the table. -PERFORM FROM merchant_do_bump_amount_stat - (in_slug - ,in_merchant_serial - ,in_timestamp - ,in_delta) - SELECT 'deposits' - ,mct.merchant_serial - ,TO_TIMESTAMP (mdc.deposit_timestamp / 1000.0 / 1000.0) - ,mdc.amount_with_fee - FROM merchant_deposit_confirmations - JOIN merchant_contract_terms - USING (order_serial) - WHERE mdc.deposit_timestamp > (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP(0)) - 365*24*60*60) * 1000000; +DO $$ +DECLARE + rec RECORD; +BEGIN +FOR rec IN + SELECT 'deposits' AS in_slug + ,mct.merchant_serial AS in_merchant_serial + ,TO_TIMESTAMP (mdc.deposit_timestamp / 1000.0 / 1000.0)::TIMESTAMP AS in_timestamp + ,mdc.total_without_fee AS in_delta + FROM merchant_deposit_confirmations mdc + JOIN merchant_contract_terms mct + USING (order_serial) + WHERE mdc.deposit_timestamp > (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP(0)) - 365*24*60*60) * 1000000 +LOOP + CALL merchant_do_bump_amount_stat (rec.in_slug, rec.in_merchant_serial, rec.in_timestamp, rec.in_delta); +END LOOP; +END $$; -- Whenever a deposit is made, call our trigger to bump statistics CREATE TRIGGER merchant_deposits_on_insert AFTER INSERT - ON merchant.merchant_deposits + ON merchant_deposits FOR EACH ROW EXECUTE FUNCTION merchant_deposits_statistics_trigger(); @@ -107,7 +110,7 @@ CREATE TRIGGER merchant_deposits_on_insert -- Setup statistic -- (Note: this is basically the one "manual" step we might not keep hard-coded) -INSERT INTO merchant.merchant_statistic_bucket_meta +INSERT INTO merchant_statistic_bucket_meta (slug ,description ,stype @@ -117,7 +120,7 @@ VALUES ('products-sold' ,'products sold (only those tracked in inventory)' ,'number' - ,ARRAY['second'::statistic_range, 'minute' 'day', 'week', 'month', 'quarter', 'year'] + ,ARRAY['second'::statistic_range, 'minute', 'day', 'week', 'month', 'quarter', 'year'] ,ARRAY[120, 120, 60, 12, 24, 8, 10] -- track last 120s, 120 minutes, 60 days, 12 weeks, 24 months, 8 quarters and 10 years ); @@ -143,7 +146,7 @@ END $$; -- Whenever inventory changes, call our trigger to bump statistics CREATE TRIGGER merchant_products_on_sold AFTER UPDATE - ON merchant.merchant_inventory + ON merchant_inventory FOR EACH ROW EXECUTE FUNCTION merchant_products_sold_statistics_trigger(); diff --git a/src/backenddb/pg_statistics_helpers.sql b/src/backenddb/pg_statistics_helpers.sql @@ -14,6 +14,8 @@ -- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -- +BEGIN; +SET search_path TO merchant; DROP FUNCTION IF EXISTS interval_to_start; CREATE OR REPLACE FUNCTION interval_to_start ( @@ -267,9 +269,9 @@ DECLARE my_now INT8; my_record RECORD; my_meta INT8; + my_range0 INT8; my_precision0 INT8; my_start INT8; - my_range0 INT8; my_event INT8; BEGIN my_now = ROUND(EXTRACT(epoch FROM in_timestamp) * 1000000)::INT8; @@ -323,7 +325,7 @@ BEGIN RETURNING aevent_serial_id INTO my_event; - UPDATE merchant_statistic_interval_amount + UPDATE merchant_statistic_interval_amount SET cumulative_value = cumulative_value + (in_delta).val + CASE @@ -399,14 +401,12 @@ COMMENT ON PROCEDURE merchant_do_bump_amount_stat IS 'Updates an amount statistic (bucket or interval)'; --- FIXME: rewrite to return values for ALL ranges! (more sane!) DROP FUNCTION IF EXISTS merchant_statistic_interval_number_get; CREATE OR REPLACE FUNCTION merchant_statistic_interval_number_get ( IN in_slug TEXT, - IN in_instance_id TEXT, - IN in_range INT8, - OUT out_value INT8 + IN in_instance_id TEXT ) +RETURNS SETOF merchant_statistic_interval_number_get_return_value LANGUAGE plpgsql AS $$ DECLARE @@ -429,7 +429,7 @@ DECLARE my_irec RECORD; my_i INT; my_max_serial INT8 DEFAULT NULL; - my_rval INT8 DEFAULT 0; + my_rval merchant_statistic_interval_number_get_return_value; BEGIN SELECT merchant_serial INTO my_instance_id @@ -437,7 +437,6 @@ BEGIN WHERE merchant_id=in_instance_id; IF NOT FOUND THEN - out_value = 0; RETURN; END IF; @@ -449,10 +448,9 @@ BEGIN WHERE slug=in_slug; IF NOT FOUND THEN - out_value = 0; RETURN; END IF; - + my_rval.rvalue = 0; my_ranges = my_rec.ranges; my_meta = my_rec.imeta_serial_id; -- my_precisions = my_rec.precisions; @@ -469,7 +467,7 @@ BEGIN AND range = my_range AND merchant_serial = in_merchant_serial; my_max_serial = my_irec.event_delimiter; - my_rval = my_rval + my_irec.cumulative_number; + my_rval.rvalue = my_rval.rvalue + my_irec.cumulative_number; my_delta = 0; OPEN my_curs (meta_serial_id := my_meta, range := my_range, instid := my_instance_id, min_serial := my_max_serial); @@ -483,7 +481,7 @@ BEGIN IF (0 != my_delta) THEN -- remove expired events from the sum of the current slot - my_rval = my_rval - my_delta; + my_rval.rvalue = my_rval.rvalue - my_delta; UPDATE merchant_statistic_interval_counter SET cumulative_number = cumulative_number - my_delta, event_delimiter = my_max_serial + 1 @@ -498,7 +496,6 @@ BEGIN WHERE imeta_serial_id = my_meta AND merchant_serial = in_merchant_serial AND range=my_ranges[my_i+1]; - -- FIXME: here we should possibly combine multiple events by rounding down to my_precision! ELSE -- events are obsolete, delete them DELETE FROM merchant_statistic_counter_event @@ -508,19 +505,35 @@ BEGIN END IF; END IF; - IF (my_range = in_range) - THEN - out_value = my_rval; - END IF; + my_rval.range = my_range; + RETURN NEXT my_val; END LOOP; CLOSE my_curs; END $$; COMMENT ON FUNCTION merchant_statistic_interval_number_get - IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value'; + IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value for each range'; + + +DROP FUNCTION IF EXISTS merchant_statistic_interval_amount_get; +CREATE OR REPLACE FUNCTION merchant_statistic_interval_amount_get ( + IN in_slug TEXT, + IN in_instance INT8 +) +RETURNS SETOF merchant_statistic_interval_amount_get_return_value +LANGUAGE plpgsql +AS $$ +BEGIN + -- FIXME: implement! +END $$; + +COMMENT ON FUNCTION merchant_statistic_interval_amount_get + IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value; multiple values are returned, one per currency and range'; + + + --- FIXME: create dual of this function to GC the amount_events! (but needs additional loop per currency, and fancy amount handling!) DROP PROCEDURE IF EXISTS merchant_statistic_counter_gc; CREATE OR REPLACE PROCEDURE merchant_statistic_counter_gc () LANGUAGE plpgsql @@ -608,31 +621,145 @@ COMMENT ON PROCEDURE merchant_statistic_counter_gc IS 'Performs garbage collection and compaction of the merchant_statistic_counter_event table'; -DROP PROCEDURE IF EXISTS merchant_statistic_bucket_gc; -CREATE OR REPLACE PROCEDURE merchant_statistic_bucket_gc () + +DROP PROCEDURE IF EXISTS merchant_statistic_amount_gc; +CREATE OR REPLACE PROCEDURE merchant_statistic_amount_gc () LANGUAGE plpgsql AS $$ +DECLARE + my_instance INT8; + my_rec RECORD; + my_sum RECORD; + my_meta INT8; + my_ranges INT8[]; + my_precisions INT8[]; + my_precision INT4; + my_currency TEXT; + my_i INT4; + min_slot INT8; + max_slot INT8; + end_slot INT8; + my_total_val INT8; + my_total_frac INT8; BEGIN - -- FIXME: GC buckets! + -- GC for all instances + FOR my_instance IN + SELECT DISTINCT merchant_serial + FROM merchant_statistic_counter_event + LOOP + -- Do combination work for all numeric statistic events + FOR my_rec IN + SELECT imeta_serial_id + ,ranges + ,precisions + FROM merchant_statistic_interval_meta + LOOP + + my_meta = my_rec.imeta_serial_id; + my_ranges = my_rec.ranges; + my_precisions = my_rec.precisions; + FOR my_currency IN + SELECT DISTINCT delta_curr + FROM merchant_statistic_amount_event + WHERE imeta_serial_id = my_meta + LOOP + + FOR my_i IN 1..COALESCE(array_length(my_ranges,1)-1,0) + LOOP + min_slot = my_ranges[my_i]; + end_slot = my_ranges[my_i + 1]; + my_precision = my_precisions[my_i]; + LOOP + EXIT WHEN min_slot >= end_slot; + max_slot = min_slot + my_precision; + SELECT SUM(delta_value) AS total_val, + SUM(delta_frac) AS total_frac, + COUNT(*) AS matches + INTO my_sum + FROM merchant_statistic_counter_event + WHERE imeta_serial_id=my_meta + AND merchant_serial=my_instance + AND delta_curr = my_currency + AND slot >= min_slot + AND slot < max_slot; + -- we only proceed if we had more then one match (optimization) + IF FOUND AND my_sum.matches > 1 + THEN + my_total_frac = my_sum.total_frac % 100000000; + my_total_val = my_sum.total_val + my_sum.total_frac / 100000000; + -- combine entries + DELETE FROM merchant_statistic_counter_event + WHERE imeta_serial_id=my_meta + AND merchant_serial=my_instance + AND delta_curr = my_currency + AND slot >= min_slot + AND slot < max_slot; + -- The previous delete ensures that this is a conflict-free INSERT + INSERT INTO merchant_statistic_counter_event + (imeta_serial_id + ,merchant_serial + ,slot + ,delta_curr + ,delta_value + ,delta_frac + ) VALUES + (my_meta + ,my_instance + ,min_slot + ,my_currency + ,my_total_val + ,my_total_frac); + END IF; + min_slot = min_slot + my_precision; + END LOOP; -- min_slot to end_slot by precision loop + END LOOP; -- my_i loop + -- Finally, delete all events beyond the range we care about + DELETE FROM merchant_statistic_counter_event + WHERE merchant_serial=my_instance + AND imeta_serial_id=my_meta + AND slot > my_ranges[array_length(my_ranges,1)]; + END LOOP; -- my_currency loop + END LOOP; -- my_rec loop + END LOOP; -- my_instance loop END $$; -COMMENT ON PROCEDURE merchant_statistic_counter_gc - IS 'Performs garbage collection of the merchant_statistic_bucket_counter and merchant_statistic_bucket_amount tables'; +COMMENT ON PROCEDURE merchant_statistic_amount_gc + IS 'Performs garbage collection and compaction of the merchant_statistic_amount_event table'; -DROP FUNCTION IF EXISTS merchant_statistic_interval_amount_get; -CREATE OR REPLACE FUNCTION merchant_statistic_interval_amount_get ( - IN in_slug TEXT, - IN in_instance INT8, - IN in_range INT8 -) -RETURNS SETOF taler_amount_currency + +DROP PROCEDURE IF EXISTS merchant_statistic_bucket_gc; +CREATE OR REPLACE PROCEDURE merchant_statistic_bucket_gc () LANGUAGE plpgsql AS $$ +DECLARE + my_rec RECORD; + my_now INT8; + my_end INT8; BEGIN - -- FIXME: implement! + my_now = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP(0)); -- seconds since epoch + FOR my_rec IN + SELECT bmeta_serial_id + ,stype + ,ranges[array_length(ranges,1)] AS range + ,ages[array_length(ages,1)] AS age + FROM merchant_statistic_interval_meta + LOOP + my_end = my_now - age * range; -- age is given in multiples of the range (in seconds) + IF stype = 'amount' + THEN + DELETE + FROM merchant_statistic_bucket_amount + WHERE bmeta_serial_id = (my_rec).bmeta_serial_id + AND bucket_start >= my_end; + ELSE + DELETE + FROM merchant_statistic_bucket_counter + WHERE bmeta_serial_id = (my_rec).bmeta_serial_id + AND bucket_start >= my_end; + END IF; + END LOOP; END $$; - -COMMENT ON FUNCTION merchant_statistic_interval_amount_get - IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value; multiple values are returned, one per currency'; +COMMENT ON PROCEDURE merchant_statistic_counter_gc + IS 'Performs garbage collection of the merchant_statistic_bucket_counter and merchant_statistic_bucket_amount tables';