Files
pwap/web/supabase/migrations/20241211092400_p2p_fraud_prevention.sql
T
pezkuwichain df58d26893 feat(p2p): add Phase 4 merchant tier system and migrations
- Add merchant tier system (Lite/Super/Diamond) with tier badges
- Add advanced order filters (token, fiat, payment method, amount range)
- Add merchant dashboard with stats, ads management, tier upgrade
- Add fraud prevention system with risk scoring and trade limits
- Rename migrations to timestamp format for Supabase CLI compatibility
- Add new migrations: phase2_phase3_tables, fraud_prevention, merchant_system
2025-12-11 10:39:08 +03:00

457 lines
15 KiB
PL/PgSQL

-- =====================================================
-- P2P FRAUD PREVENTION SYSTEM
-- Auto-detection rules and triggers
-- =====================================================
-- =====================================================
-- USER FRAUD INDICATORS TABLE
-- =====================================================
CREATE TABLE IF NOT EXISTS public.p2p_user_fraud_indicators (
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
-- Trade statistics
cancel_rate DECIMAL(5,2) DEFAULT 0, -- Percentage
dispute_rate DECIMAL(5,2) DEFAULT 0, -- Percentage
avg_trade_amount DECIMAL(18,2) DEFAULT 0,
-- Recent activity
recent_cancellations_24h INT DEFAULT 0,
recent_disputes_7d INT DEFAULT 0,
trades_today INT DEFAULT 0,
volume_today DECIMAL(18,2) DEFAULT 0,
-- Risk assessment
risk_score INT DEFAULT 0 CHECK (risk_score BETWEEN 0 AND 100),
risk_level VARCHAR(10) DEFAULT 'low' CHECK (risk_level IN ('low', 'medium', 'high', 'critical')),
active_flags TEXT[] DEFAULT '{}',
-- Restrictions
is_blocked BOOLEAN DEFAULT FALSE,
blocked_reason TEXT,
blocked_at TIMESTAMPTZ,
blocked_until TIMESTAMPTZ,
requires_review BOOLEAN DEFAULT FALSE,
-- Cooldowns
last_cancellation_at TIMESTAMPTZ,
last_dispute_at TIMESTAMPTZ,
last_trade_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_fraud_indicators_risk ON public.p2p_user_fraud_indicators(risk_score DESC);
CREATE INDEX idx_fraud_indicators_blocked ON public.p2p_user_fraud_indicators(is_blocked) WHERE is_blocked = true;
CREATE INDEX idx_fraud_indicators_review ON public.p2p_user_fraud_indicators(requires_review) WHERE requires_review = true;
-- Enable RLS
ALTER TABLE public.p2p_user_fraud_indicators ENABLE ROW LEVEL SECURITY;
-- Users can view their own indicators (limited)
CREATE POLICY "p2p_fraud_indicators_own_read" ON public.p2p_user_fraud_indicators
FOR SELECT USING (user_id = auth.uid());
-- Admins can view and update all
CREATE POLICY "p2p_fraud_indicators_admin" ON public.p2p_user_fraud_indicators
FOR ALL USING (
EXISTS (
SELECT 1 FROM public.admin_roles
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
)
);
-- =====================================================
-- SUSPICIOUS ACTIVITY LOG
-- =====================================================
CREATE TABLE IF NOT EXISTS public.p2p_suspicious_activity (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id),
trade_id UUID REFERENCES public.p2p_fiat_trades(id),
-- Activity details
activity_type VARCHAR(50) NOT NULL CHECK (activity_type IN (
'high_cancel_rate', 'frequent_disputes', 'rapid_trading',
'unusual_amount', 'new_account_large_trade', 'payment_name_mismatch',
'suspected_multi_account', 'ip_anomaly', 'device_anomaly', 'other'
)),
severity VARCHAR(10) NOT NULL CHECK (severity IN ('low', 'medium', 'high', 'critical')),
description TEXT,
metadata JSONB DEFAULT '{}',
-- Resolution
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'dismissed', 'actioned')),
reviewed_by UUID REFERENCES auth.users(id),
reviewed_at TIMESTAMPTZ,
action_taken TEXT,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_suspicious_activity_user ON public.p2p_suspicious_activity(user_id, created_at DESC);
CREATE INDEX idx_suspicious_activity_status ON public.p2p_suspicious_activity(status) WHERE status = 'pending';
CREATE INDEX idx_suspicious_activity_severity ON public.p2p_suspicious_activity(severity, created_at DESC);
-- Enable RLS
ALTER TABLE public.p2p_suspicious_activity ENABLE ROW LEVEL SECURITY;
-- Only admins can view
CREATE POLICY "p2p_suspicious_activity_admin" ON public.p2p_suspicious_activity
FOR ALL USING (
EXISTS (
SELECT 1 FROM public.admin_roles
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
)
);
-- =====================================================
-- FUNCTION: Calculate User Risk Score
-- =====================================================
CREATE OR REPLACE FUNCTION public.calculate_user_risk_score(p_user_id UUID)
RETURNS TABLE(
risk_score INT,
risk_level TEXT,
flags TEXT[]
) AS $$
DECLARE
v_indicators RECORD;
v_score INT := 0;
v_flags TEXT[] := '{}';
BEGIN
-- Get current indicators
SELECT * INTO v_indicators
FROM public.p2p_user_fraud_indicators
WHERE user_id = p_user_id;
-- If no record exists, create one
IF NOT FOUND THEN
INSERT INTO public.p2p_user_fraud_indicators (user_id)
VALUES (p_user_id)
RETURNING * INTO v_indicators;
END IF;
-- Calculate from reputation data
SELECT
COALESCE(
CASE WHEN total_trades > 0
THEN (cancelled_trades::DECIMAL / total_trades * 100)
ELSE 0
END, 0),
COALESCE(
CASE WHEN total_trades > 0
THEN (disputed_trades::DECIMAL / total_trades * 100)
ELSE 0
END, 0)
INTO v_indicators.cancel_rate, v_indicators.dispute_rate
FROM public.p2p_reputation
WHERE user_id = p_user_id;
-- Apply rules and calculate score
-- Rule 1: High cancel rate (>30%)
IF v_indicators.cancel_rate > 30 THEN
v_score := v_score + 25;
v_flags := array_append(v_flags, 'High Cancellation Rate');
END IF;
-- Rule 2: High dispute rate (>20%)
IF v_indicators.dispute_rate > 20 THEN
v_score := v_score + 30;
v_flags := array_append(v_flags, 'Frequent Disputes');
END IF;
-- Rule 3: Multiple recent cancellations (>3 in 24h)
IF v_indicators.recent_cancellations_24h > 3 THEN
v_score := v_score + 35;
v_flags := array_append(v_flags, 'Multiple Recent Cancellations');
END IF;
-- Rule 4: Recent disputes (>2 in 7d)
IF v_indicators.recent_disputes_7d > 2 THEN
v_score := v_score + 25;
v_flags := array_append(v_flags, 'Recent Disputes');
END IF;
-- Cap score at 100
v_score := LEAST(v_score, 100);
-- Determine risk level
risk_level := CASE
WHEN v_score >= 95 THEN 'critical'
WHEN v_score >= 80 THEN 'high'
WHEN v_score >= 50 THEN 'medium'
ELSE 'low'
END;
-- Update indicators table
UPDATE public.p2p_user_fraud_indicators
SET
risk_score = v_score,
risk_level = risk_level,
active_flags = v_flags,
is_blocked = (v_score >= 95),
requires_review = (v_score >= 80),
updated_at = NOW()
WHERE user_id = p_user_id;
risk_score := v_score;
flags := v_flags;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =====================================================
-- FUNCTION: Check Trade Allowed
-- =====================================================
CREATE OR REPLACE FUNCTION public.check_trade_allowed(
p_user_id UUID,
p_trade_amount DECIMAL
) RETURNS TABLE(
allowed BOOLEAN,
reason TEXT
) AS $$
DECLARE
v_indicators RECORD;
v_reputation RECORD;
v_limits RECORD;
v_cooldown_ms BIGINT;
BEGIN
-- Get fraud indicators
SELECT * INTO v_indicators
FROM public.p2p_user_fraud_indicators
WHERE user_id = p_user_id;
-- Check if blocked
IF v_indicators IS NOT NULL AND v_indicators.is_blocked THEN
allowed := FALSE;
reason := COALESCE(v_indicators.blocked_reason, 'Your account is temporarily restricted from trading.');
RETURN NEXT;
RETURN;
END IF;
-- Check cooldown after cancellation (5 minutes)
IF v_indicators IS NOT NULL AND v_indicators.last_cancellation_at IS NOT NULL THEN
v_cooldown_ms := EXTRACT(EPOCH FROM (NOW() - v_indicators.last_cancellation_at)) * 1000;
IF v_cooldown_ms < 300000 THEN -- 5 minutes
allowed := FALSE;
reason := format('Please wait %s seconds before creating a new trade.', ((300000 - v_cooldown_ms) / 1000)::INT);
RETURN NEXT;
RETURN;
END IF;
END IF;
-- Check cooldown after dispute (24 hours)
IF v_indicators IS NOT NULL AND v_indicators.last_dispute_at IS NOT NULL THEN
v_cooldown_ms := EXTRACT(EPOCH FROM (NOW() - v_indicators.last_dispute_at)) * 1000;
IF v_cooldown_ms < 86400000 THEN -- 24 hours
allowed := FALSE;
reason := 'Trading is restricted for 24 hours after a dispute.';
RETURN NEXT;
RETURN;
END IF;
END IF;
-- Get reputation for trust level
SELECT * INTO v_reputation
FROM public.p2p_reputation
WHERE user_id = p_user_id;
-- Define limits based on trust level
SELECT * INTO v_limits FROM (
SELECT
CASE COALESCE(v_reputation.trust_level, 'new')
WHEN 'verified' THEN 50000
WHEN 'advanced' THEN 10000
WHEN 'intermediate' THEN 2000
WHEN 'basic' THEN 500
ELSE 100
END as max_trade,
CASE COALESCE(v_reputation.trust_level, 'new')
WHEN 'verified' THEN 50
WHEN 'advanced' THEN 20
WHEN 'intermediate' THEN 10
WHEN 'basic' THEN 5
ELSE 3
END as max_daily_trades,
CASE COALESCE(v_reputation.trust_level, 'new')
WHEN 'verified' THEN 100000
WHEN 'advanced' THEN 25000
WHEN 'intermediate' THEN 5000
WHEN 'basic' THEN 1000
ELSE 200
END as max_daily_volume
) limits;
-- Check trade amount limit
IF p_trade_amount > v_limits.max_trade THEN
allowed := FALSE;
reason := format('Trade amount exceeds your limit of $%s. Complete more trades to increase your limit.', v_limits.max_trade);
RETURN NEXT;
RETURN;
END IF;
-- Check daily trade count
IF v_indicators IS NOT NULL AND v_indicators.trades_today >= v_limits.max_daily_trades THEN
allowed := FALSE;
reason := format('You have reached your daily limit of %s trades.', v_limits.max_daily_trades);
RETURN NEXT;
RETURN;
END IF;
-- Check daily volume
IF v_indicators IS NOT NULL AND (v_indicators.volume_today + p_trade_amount) > v_limits.max_daily_volume THEN
allowed := FALSE;
reason := format('This trade would exceed your daily volume limit of $%s.', v_limits.max_daily_volume);
RETURN NEXT;
RETURN;
END IF;
-- All checks passed
allowed := TRUE;
reason := NULL;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =====================================================
-- FUNCTION: Log Suspicious Activity
-- =====================================================
CREATE OR REPLACE FUNCTION public.log_suspicious_activity(
p_user_id UUID,
p_trade_id UUID,
p_activity_type TEXT,
p_severity TEXT,
p_description TEXT DEFAULT NULL,
p_metadata JSONB DEFAULT '{}'
) RETURNS UUID AS $$
DECLARE
v_activity_id UUID;
BEGIN
INSERT INTO public.p2p_suspicious_activity (
user_id, trade_id, activity_type, severity, description, metadata
) VALUES (
p_user_id, p_trade_id, p_activity_type, p_severity, p_description, p_metadata
)
RETURNING id INTO v_activity_id;
-- Auto-recalculate risk score
PERFORM public.calculate_user_risk_score(p_user_id);
RETURN v_activity_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =====================================================
-- TRIGGER: Update Fraud Indicators on Trade Completion
-- =====================================================
CREATE OR REPLACE FUNCTION update_fraud_indicators_on_trade()
RETURNS TRIGGER AS $$
BEGIN
-- On trade completion
IF NEW.status = 'completed' AND (OLD.status IS NULL OR OLD.status != 'completed') THEN
-- Update buyer indicators
INSERT INTO public.p2p_user_fraud_indicators (user_id, trades_today, volume_today, last_trade_at)
VALUES (NEW.buyer_id, 1, NEW.fiat_amount, NOW())
ON CONFLICT (user_id) DO UPDATE SET
trades_today = p2p_user_fraud_indicators.trades_today + 1,
volume_today = p2p_user_fraud_indicators.volume_today + NEW.fiat_amount,
last_trade_at = NOW(),
updated_at = NOW();
-- Update seller indicators
INSERT INTO public.p2p_user_fraud_indicators (user_id, trades_today, volume_today, last_trade_at)
VALUES (NEW.seller_id, 1, NEW.fiat_amount, NOW())
ON CONFLICT (user_id) DO UPDATE SET
trades_today = p2p_user_fraud_indicators.trades_today + 1,
volume_today = p2p_user_fraud_indicators.volume_today + NEW.fiat_amount,
last_trade_at = NOW(),
updated_at = NOW();
END IF;
-- On trade cancellation
IF NEW.status = 'cancelled' AND (OLD.status IS NULL OR OLD.status != 'cancelled') THEN
IF NEW.cancelled_by IS NOT NULL THEN
UPDATE public.p2p_user_fraud_indicators
SET
recent_cancellations_24h = recent_cancellations_24h + 1,
last_cancellation_at = NOW(),
updated_at = NOW()
WHERE user_id = NEW.cancelled_by;
-- Recalculate risk score
PERFORM public.calculate_user_risk_score(NEW.cancelled_by);
END IF;
END IF;
-- On dispute
IF NEW.status = 'disputed' AND (OLD.status IS NULL OR OLD.status != 'disputed') THEN
-- Update both parties
UPDATE public.p2p_user_fraud_indicators
SET
recent_disputes_7d = recent_disputes_7d + 1,
last_dispute_at = NOW(),
updated_at = NOW()
WHERE user_id IN (NEW.buyer_id, NEW.seller_id);
-- Recalculate risk scores
PERFORM public.calculate_user_risk_score(NEW.buyer_id);
PERFORM public.calculate_user_risk_score(NEW.seller_id);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER trigger_update_fraud_indicators
AFTER UPDATE ON public.p2p_fiat_trades
FOR EACH ROW EXECUTE FUNCTION update_fraud_indicators_on_trade();
-- =====================================================
-- SCHEDULED JOB: Reset Daily Counters
-- (Run at midnight UTC via pg_cron or external scheduler)
-- =====================================================
CREATE OR REPLACE FUNCTION public.reset_daily_fraud_counters()
RETURNS void AS $$
BEGIN
UPDATE public.p2p_user_fraud_indicators
SET
trades_today = 0,
volume_today = 0,
updated_at = NOW();
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =====================================================
-- SCHEDULED JOB: Reset Weekly Counters
-- =====================================================
CREATE OR REPLACE FUNCTION public.reset_weekly_fraud_counters()
RETURNS void AS $$
BEGIN
UPDATE public.p2p_user_fraud_indicators
SET
recent_disputes_7d = 0,
updated_at = NOW();
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =====================================================
-- GRANT PERMISSIONS
-- =====================================================
GRANT EXECUTE ON FUNCTION public.calculate_user_risk_score TO authenticated;
GRANT EXECUTE ON FUNCTION public.check_trade_allowed TO authenticated;
GRANT EXECUTE ON FUNCTION public.log_suspicious_activity TO authenticated;
GRANT EXECUTE ON FUNCTION public.reset_daily_fraud_counters TO authenticated;
GRANT EXECUTE ON FUNCTION public.reset_weekly_fraud_counters TO authenticated;
-- =====================================================
-- COMMENTS
-- =====================================================
COMMENT ON TABLE public.p2p_user_fraud_indicators IS 'Tracks fraud indicators and risk scores for P2P users';
COMMENT ON TABLE public.p2p_suspicious_activity IS 'Log of suspicious activities detected by the fraud system';
COMMENT ON FUNCTION public.calculate_user_risk_score IS 'Calculate and update user risk score based on trading behavior';
COMMENT ON FUNCTION public.check_trade_allowed IS 'Check if a user is allowed to make a trade based on limits and restrictions';