228 lines
7.4 KiB
MySQL
228 lines
7.4 KiB
MySQL
|
|
--
|
||
|
|
-- 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 tops-0001.sql
|
||
|
|
-- @brief special TOPS-specific (AML) rules to inject into an exchange
|
||
|
|
-- @author Christian Grothoff
|
||
|
|
|
||
|
|
-- Everything in one big transaction
|
||
|
|
BEGIN;
|
||
|
|
|
||
|
|
-- Check patch versioning is in place.
|
||
|
|
SELECT _v.register_patch('tops-0001', NULL, NULL);
|
||
|
|
|
||
|
|
-- Note: this NOT an accident: the schema MUST be named
|
||
|
|
-- using the filename prefix (and the name under --enable-custom of taler-exchange-dbinit).
|
||
|
|
CREATE SCHEMA IF NOT EXISTS tops;
|
||
|
|
|
||
|
|
SET search_path TO tops,exchange;
|
||
|
|
|
||
|
|
INSERT INTO exchange_statistic_interval_meta
|
||
|
|
(origin
|
||
|
|
,slug
|
||
|
|
,description
|
||
|
|
,stype
|
||
|
|
,ranges
|
||
|
|
,precisions)
|
||
|
|
VALUES
|
||
|
|
-- this first one is just for testing right now
|
||
|
|
('tops' -- must match schema!
|
||
|
|
,'deposit-transactions'
|
||
|
|
,'number of (batch) deposits performed by this merchant, used to detect sudden increase in number of transactions'
|
||
|
|
,'number'
|
||
|
|
,ARRAY(SELECT generate_series (60*60*24*7, 60*60*24*7*52, 60*60*24*7)) -- weekly volume over the last year
|
||
|
|
,array_fill (60*60*24, ARRAY[52]) -- precision is per day
|
||
|
|
),
|
||
|
|
('tops' -- must match schema!
|
||
|
|
,'deposit-volume'
|
||
|
|
,'total amount deposited by this merchant in (batch) deposits including deposit fees, used to detect sudden increase in transaction volume'
|
||
|
|
,'amount'
|
||
|
|
,ARRAY(SELECT generate_series (60*60*24*7, 60*60*24*7*52, 60*60*24*7)) -- weekly volume over the last year
|
||
|
|
,array_fill (60*60*24, ARRAY[52]) -- precision is per day
|
||
|
|
)
|
||
|
|
ON CONFLICT DO NOTHING;
|
||
|
|
|
||
|
|
INSERT INTO exchange_statistic_bucket_meta
|
||
|
|
(origin
|
||
|
|
,slug
|
||
|
|
,description
|
||
|
|
,stype
|
||
|
|
,ranges
|
||
|
|
,ages)
|
||
|
|
VALUES
|
||
|
|
-- this first one is just for testing right now
|
||
|
|
('tops' -- must match schema!
|
||
|
|
,'deposit-transactions'
|
||
|
|
,'number of (batch) deposits performed by this merchant, used to detect sudden increase in number of transactions'
|
||
|
|
,'number'
|
||
|
|
,ARRAY['day'::statistic_range,'week']
|
||
|
|
,ARRAY[5,5]
|
||
|
|
),
|
||
|
|
('tops' -- must match schema!
|
||
|
|
,'deposit-volume'
|
||
|
|
,'total amount deposited by this merchant in (batch) deposits including deposit fees, used to detect sudden increase in transaction volume'
|
||
|
|
,'amount'
|
||
|
|
,ARRAY['day'::statistic_range,'week']
|
||
|
|
,ARRAY[5,5]
|
||
|
|
)
|
||
|
|
ON CONFLICT DO NOTHING;
|
||
|
|
|
||
|
|
DROP FUNCTION IF EXISTS tops_deposit_statistics_trigger CASCADE;
|
||
|
|
CREATE FUNCTION tops_deposit_statistics_trigger()
|
||
|
|
RETURNS trigger
|
||
|
|
LANGUAGE plpgsql
|
||
|
|
AS $$
|
||
|
|
DECLARE
|
||
|
|
my_h_payto BYTEA; -- normalized h_payto of target account
|
||
|
|
my_rec RECORD;
|
||
|
|
my_last_year taler_amount; -- sum of deposits this year
|
||
|
|
my_last_month taler_amount; -- sum of deposits this month
|
||
|
|
my_old_rules RECORD;
|
||
|
|
my_properties TEXT;
|
||
|
|
my_investigate_property JSONB;
|
||
|
|
my_measure_name TEXT;
|
||
|
|
my_rules TEXT;
|
||
|
|
BEGIN
|
||
|
|
SELECT wt.h_normalized_payto
|
||
|
|
INTO my_h_payto
|
||
|
|
FROM wire_targets wt
|
||
|
|
WHERE wire_target_h_payto = NEW.wire_target_h_payto;
|
||
|
|
|
||
|
|
CALL exchange_do_bump_amount_stat
|
||
|
|
('deposit-volume'
|
||
|
|
,my_h_payto
|
||
|
|
,CURRENT_TIMESTAMP(0)::TIMESTAMP
|
||
|
|
,NEW.total_amount);
|
||
|
|
|
||
|
|
-- FIXME: this is just for testing, I want to also check
|
||
|
|
-- the 'counter'-based functions.
|
||
|
|
CALL exchange_do_bump_number_stat
|
||
|
|
('deposit-transactions'
|
||
|
|
,my_h_payto
|
||
|
|
,CURRENT_TIMESTAMP(0)::TIMESTAMP
|
||
|
|
,1);
|
||
|
|
|
||
|
|
-- Get historical deposit volumes and extract the yearly and monthly
|
||
|
|
-- interval statistic values from the result for the AML trigger check.
|
||
|
|
FOR my_rec IN
|
||
|
|
SELECT *
|
||
|
|
FROM exchange_statistic_interval_amount_get(
|
||
|
|
'deposit-volume'
|
||
|
|
,my_h_payto
|
||
|
|
)
|
||
|
|
LOOP
|
||
|
|
IF (my_rec.range = 60*60*24*7*52)
|
||
|
|
THEN
|
||
|
|
my_last_year = my_rec.rvalue;
|
||
|
|
END IF;
|
||
|
|
IF (my_rec.range = 60*60*24*7*4)
|
||
|
|
THEN
|
||
|
|
my_last_month = my_rec.rvalue;
|
||
|
|
END IF;
|
||
|
|
END LOOP;
|
||
|
|
-- Note: it is OK to ignore '.frac', as that cannot be significant.
|
||
|
|
-- Also, we effectively exclude the current month's revenue from
|
||
|
|
-- "last year" as otherwise the rule makes no sense.
|
||
|
|
-- Finally, we define the "current month" always as the last 4 weeks,
|
||
|
|
-- just like the "last year" is the last 52 weeks.
|
||
|
|
IF (my_last_year.val < my_last_month.val * 2)
|
||
|
|
THEN
|
||
|
|
-- This is suspicious. => Flag account for AML review!
|
||
|
|
--
|
||
|
|
-- FIXME: we probably want to factor the code from
|
||
|
|
-- this branch out into a generic
|
||
|
|
-- function to trigger investigations at some point!
|
||
|
|
--
|
||
|
|
-- First, get existing rules and clear an 'is_active'
|
||
|
|
-- flag, but ONLY if we are not _already_ investigating
|
||
|
|
-- the account (as in the latter case, we'll do no INSERT).
|
||
|
|
UPDATE legitimization_outcomes
|
||
|
|
SET is_active=NOT to_investigate
|
||
|
|
WHERE h_payto = my_h_payto
|
||
|
|
AND is_active
|
||
|
|
RETURNING jproperties
|
||
|
|
,new_measure_name
|
||
|
|
,jnew_rules
|
||
|
|
,to_investigate
|
||
|
|
INTO my_old_rules;
|
||
|
|
|
||
|
|
-- Note that if we have no active legitimization_outcome
|
||
|
|
-- that means we are on default rules and the account
|
||
|
|
-- did not cross KYC thresholds and thus we have no
|
||
|
|
-- established business relationship. In this case, we
|
||
|
|
-- do not care as the overall volume is insignificant.
|
||
|
|
-- This also takes care of the case where a customer
|
||
|
|
-- is new (and obviously the first few months are
|
||
|
|
-- basically always above the inherently zero or near-zero
|
||
|
|
-- transactions from the previous year).
|
||
|
|
-- Thus, we only proceed IF FOUND.
|
||
|
|
IF FOUND
|
||
|
|
THEN
|
||
|
|
my_properties = my_old_rules.jproperties;
|
||
|
|
my_measure_name = my_old_rules.new_measure_name;
|
||
|
|
my_rules = my_old_rules.jnew_rules;
|
||
|
|
my_investigate_property = json_object(ARRAY['AML_INVESTIGATION_STATE',
|
||
|
|
'AML_INVESTIGATION_TRIGGER'],
|
||
|
|
ARRAY['INVESTIATION_PENDING',
|
||
|
|
'DEPOSIT_ANOMALY']);
|
||
|
|
IF my_properties IS NULL
|
||
|
|
THEN
|
||
|
|
my_properties = my_investigate_property::TEXT;
|
||
|
|
ELSE
|
||
|
|
my_properties = (my_properties::JSONB || my_investigate_property)::TEXT;
|
||
|
|
END IF;
|
||
|
|
|
||
|
|
-- Note: here we could in theory manipulate my_properties,
|
||
|
|
-- say to set a note as to why the investigation was started.
|
||
|
|
IF NOT my_old_rules.to_investigate
|
||
|
|
THEN
|
||
|
|
-- Only insert if 'to_investigate' was not already set.
|
||
|
|
INSERT INTO legitimization_outcomes (
|
||
|
|
h_payto
|
||
|
|
,decision_time
|
||
|
|
,expiration_time
|
||
|
|
,jproperties
|
||
|
|
,new_measure_name
|
||
|
|
,to_investigate
|
||
|
|
,is_active
|
||
|
|
,jnew_rules
|
||
|
|
) VALUES (
|
||
|
|
my_h_payto
|
||
|
|
,my_now
|
||
|
|
,my_now + 366*24*60*60
|
||
|
|
,my_properties
|
||
|
|
,my_measure_name
|
||
|
|
,TRUE
|
||
|
|
,TRUE
|
||
|
|
,my_rules);
|
||
|
|
END IF;
|
||
|
|
END IF;
|
||
|
|
END IF;
|
||
|
|
RETURN NEW;
|
||
|
|
END $$;
|
||
|
|
COMMENT ON FUNCTION tops_deposit_statistics_trigger
|
||
|
|
IS 'creates deposit statistics';
|
||
|
|
|
||
|
|
-- Whenever a deposit is made, call our trigger to bump statistics
|
||
|
|
CREATE TRIGGER tops_batch_deposits_on_insert
|
||
|
|
AFTER INSERT
|
||
|
|
ON batch_deposits
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION tops_deposit_statistics_trigger();
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
COMMIT;
|