mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 09:07:55 +00:00
df58d26893
- 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
457 lines
15 KiB
PL/PgSQL
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';
|