Files
pwap/web/supabase/migrations/20241211091600_p2p_phase2_phase3_tables.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

567 lines
18 KiB
PL/PgSQL

-- =====================================================
-- P2P FIAT SYSTEM - PHASE 2 & 3 ADDITIONAL TABLES
-- Messages, Ratings, Notifications, Evidence, Fraud Reports
-- =====================================================
-- =====================================================
-- P2P MESSAGES TABLE (Phase 2 - Chat System)
-- =====================================================
CREATE TABLE IF NOT EXISTS public.p2p_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trade_id UUID NOT NULL REFERENCES public.p2p_fiat_trades(id) ON DELETE CASCADE,
sender_id UUID NOT NULL REFERENCES auth.users(id),
-- Message content
message TEXT NOT NULL CHECK (LENGTH(message) > 0 AND LENGTH(message) <= 2000),
message_type VARCHAR(20) DEFAULT 'text' CHECK (message_type IN ('text', 'image', 'system')),
attachment_url TEXT, -- Supabase Storage URL
-- Read status
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMPTZ,
-- Metadata
metadata JSONB DEFAULT '{}',
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes for p2p_messages
CREATE INDEX idx_p2p_messages_trade ON public.p2p_messages(trade_id, created_at DESC);
CREATE INDEX idx_p2p_messages_sender ON public.p2p_messages(sender_id);
CREATE INDEX idx_p2p_messages_unread ON public.p2p_messages(trade_id, is_read) WHERE is_read = false;
-- Enable RLS
ALTER TABLE public.p2p_messages ENABLE ROW LEVEL SECURITY;
-- Policy: Only trade participants can read/write messages
CREATE POLICY "p2p_messages_trade_participants" ON public.p2p_messages
FOR ALL USING (
EXISTS (
SELECT 1 FROM public.p2p_fiat_trades t
WHERE t.id = trade_id
AND (t.seller_id = auth.uid() OR t.buyer_id = auth.uid())
)
);
-- =====================================================
-- P2P RATINGS TABLE (Phase 2 - Trust System)
-- =====================================================
CREATE TABLE IF NOT EXISTS public.p2p_ratings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trade_id UUID NOT NULL REFERENCES public.p2p_fiat_trades(id) ON DELETE CASCADE,
rater_id UUID NOT NULL REFERENCES auth.users(id),
rated_id UUID NOT NULL REFERENCES auth.users(id),
-- Rating details
rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
review TEXT CHECK (LENGTH(review) <= 500),
-- Rating aspects (optional breakdown)
communication_rating INT CHECK (communication_rating BETWEEN 1 AND 5),
speed_rating INT CHECK (speed_rating BETWEEN 1 AND 5),
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
-- Constraints
CONSTRAINT unique_rating_per_trade UNIQUE(trade_id, rater_id),
CONSTRAINT cannot_rate_self CHECK (rater_id != rated_id)
);
-- Indexes for p2p_ratings
CREATE INDEX idx_p2p_ratings_trade ON public.p2p_ratings(trade_id);
CREATE INDEX idx_p2p_ratings_rated ON public.p2p_ratings(rated_id, created_at DESC);
CREATE INDEX idx_p2p_ratings_rater ON public.p2p_ratings(rater_id);
-- Enable RLS
ALTER TABLE public.p2p_ratings ENABLE ROW LEVEL SECURITY;
-- Policy: Public read (for reputation display)
CREATE POLICY "p2p_ratings_public_read" ON public.p2p_ratings
FOR SELECT USING (true);
-- Policy: Only trade participants can insert ratings
CREATE POLICY "p2p_ratings_trade_participants_insert" ON public.p2p_ratings
FOR INSERT WITH CHECK (
rater_id = auth.uid() AND
EXISTS (
SELECT 1 FROM public.p2p_fiat_trades t
WHERE t.id = trade_id
AND t.status = 'completed'
AND (t.seller_id = auth.uid() OR t.buyer_id = auth.uid())
)
);
-- =====================================================
-- P2P NOTIFICATIONS TABLE (Phase 2 - Real-time Alerts)
-- =====================================================
CREATE TABLE IF NOT EXISTS public.p2p_notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- Notification content
type VARCHAR(50) NOT NULL CHECK (type IN (
'new_order', 'payment_sent', 'payment_confirmed', 'trade_cancelled',
'dispute_opened', 'dispute_resolved', 'new_message', 'rating_received',
'offer_matched', 'trade_reminder', 'system'
)),
title TEXT NOT NULL,
message TEXT,
-- Reference
reference_type VARCHAR(20) CHECK (reference_type IN ('trade', 'offer', 'dispute', 'message')),
reference_id UUID,
-- Status
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMPTZ,
-- Action URL (frontend route)
action_url TEXT,
-- Metadata
metadata JSONB DEFAULT '{}',
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes for p2p_notifications
CREATE INDEX idx_p2p_notifications_user ON public.p2p_notifications(user_id, created_at DESC);
CREATE INDEX idx_p2p_notifications_unread ON public.p2p_notifications(user_id, is_read) WHERE is_read = false;
CREATE INDEX idx_p2p_notifications_type ON public.p2p_notifications(user_id, type);
-- Enable RLS
ALTER TABLE public.p2p_notifications ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see their own notifications
CREATE POLICY "p2p_notifications_own" ON public.p2p_notifications
FOR ALL USING (user_id = auth.uid());
-- =====================================================
-- P2P DISPUTE EVIDENCE TABLE (Phase 3 - Dispute System)
-- =====================================================
CREATE TABLE IF NOT EXISTS public.p2p_dispute_evidence (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
dispute_id UUID NOT NULL REFERENCES public.p2p_fiat_disputes(id) ON DELETE CASCADE,
uploaded_by UUID NOT NULL REFERENCES auth.users(id),
-- Evidence details
evidence_type VARCHAR(30) NOT NULL CHECK (evidence_type IN (
'screenshot', 'receipt', 'bank_statement', 'chat_log',
'transaction_proof', 'identity_doc', 'other'
)),
file_url TEXT NOT NULL,
file_name TEXT,
file_size INT, -- bytes
mime_type TEXT,
-- Description
description TEXT CHECK (LENGTH(description) <= 1000),
-- Admin review
reviewed_by UUID REFERENCES auth.users(id),
reviewed_at TIMESTAMPTZ,
review_notes TEXT,
is_valid BOOLEAN,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes for p2p_dispute_evidence
CREATE INDEX idx_p2p_evidence_dispute ON public.p2p_dispute_evidence(dispute_id, created_at);
CREATE INDEX idx_p2p_evidence_uploader ON public.p2p_dispute_evidence(uploaded_by);
-- Enable RLS
ALTER TABLE public.p2p_dispute_evidence ENABLE ROW LEVEL SECURITY;
-- Policy: Dispute parties can view evidence
CREATE POLICY "p2p_evidence_parties_read" ON public.p2p_dispute_evidence
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.p2p_fiat_disputes d
JOIN public.p2p_fiat_trades t ON t.id = d.trade_id
WHERE d.id = dispute_id
AND (t.seller_id = auth.uid() OR t.buyer_id = auth.uid() OR d.opened_by = auth.uid())
)
);
-- Policy: Dispute parties can upload evidence
CREATE POLICY "p2p_evidence_parties_insert" ON public.p2p_dispute_evidence
FOR INSERT WITH CHECK (
uploaded_by = auth.uid() AND
EXISTS (
SELECT 1 FROM public.p2p_fiat_disputes d
JOIN public.p2p_fiat_trades t ON t.id = d.trade_id
WHERE d.id = dispute_id
AND d.status IN ('open', 'under_review')
AND (t.seller_id = auth.uid() OR t.buyer_id = auth.uid())
)
);
-- Policy: Admins can view all evidence
CREATE POLICY "p2p_evidence_admin_read" ON public.p2p_dispute_evidence
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.admin_roles
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
)
);
-- Policy: Admins can update evidence (for review)
CREATE POLICY "p2p_evidence_admin_update" ON public.p2p_dispute_evidence
FOR UPDATE USING (
EXISTS (
SELECT 1 FROM public.admin_roles
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
)
);
-- =====================================================
-- P2P FRAUD REPORTS TABLE (Phase 3 - Security)
-- =====================================================
CREATE TABLE IF NOT EXISTS public.p2p_fraud_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reporter_id UUID NOT NULL REFERENCES auth.users(id),
reported_user_id UUID NOT NULL REFERENCES auth.users(id),
-- Report details
trade_id UUID REFERENCES public.p2p_fiat_trades(id),
reason VARCHAR(50) NOT NULL CHECK (reason IN (
'fake_payment', 'fake_proof', 'scam_attempt', 'harassment',
'money_laundering', 'identity_fraud', 'multiple_accounts', 'other'
)),
description TEXT NOT NULL CHECK (LENGTH(description) >= 20 AND LENGTH(description) <= 2000),
evidence_urls TEXT[] DEFAULT '{}',
-- Investigation
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN (
'pending', 'investigating', 'confirmed', 'dismissed', 'escalated'
)),
assigned_to UUID REFERENCES auth.users(id),
assigned_at TIMESTAMPTZ,
-- Resolution
resolution TEXT,
resolution_notes TEXT,
resolved_by UUID REFERENCES auth.users(id),
resolved_at TIMESTAMPTZ,
-- Action taken
action_taken VARCHAR(30) CHECK (action_taken IN (
'warning_issued', 'temporary_ban', 'permanent_ban',
'trade_restricted', 'no_action', 'referred_to_authorities'
)),
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Constraints
CONSTRAINT cannot_report_self CHECK (reporter_id != reported_user_id)
);
-- Indexes for p2p_fraud_reports
CREATE INDEX idx_p2p_fraud_reporter ON public.p2p_fraud_reports(reporter_id);
CREATE INDEX idx_p2p_fraud_reported ON public.p2p_fraud_reports(reported_user_id);
CREATE INDEX idx_p2p_fraud_status ON public.p2p_fraud_reports(status) WHERE status IN ('pending', 'investigating');
CREATE INDEX idx_p2p_fraud_trade ON public.p2p_fraud_reports(trade_id) WHERE trade_id IS NOT NULL;
-- Enable RLS
ALTER TABLE public.p2p_fraud_reports ENABLE ROW LEVEL SECURITY;
-- Policy: Users can view their own reports
CREATE POLICY "p2p_fraud_own_reports" ON public.p2p_fraud_reports
FOR SELECT USING (reporter_id = auth.uid());
-- Policy: Users can create reports
CREATE POLICY "p2p_fraud_create" ON public.p2p_fraud_reports
FOR INSERT WITH CHECK (reporter_id = auth.uid());
-- Policy: Admins can view all reports
CREATE POLICY "p2p_fraud_admin_read" ON public.p2p_fraud_reports
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.admin_roles
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
)
);
-- Policy: Admins can update reports
CREATE POLICY "p2p_fraud_admin_update" ON public.p2p_fraud_reports
FOR UPDATE USING (
EXISTS (
SELECT 1 FROM public.admin_roles
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
)
);
-- =====================================================
-- TRIGGERS FOR UPDATED_AT
-- =====================================================
CREATE TRIGGER update_p2p_messages_updated_at BEFORE UPDATE ON public.p2p_messages
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_p2p_fraud_reports_updated_at BEFORE UPDATE ON public.p2p_fraud_reports
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- =====================================================
-- ENABLE REALTIME FOR NEW TABLES
-- =====================================================
ALTER PUBLICATION supabase_realtime ADD TABLE public.p2p_messages;
ALTER PUBLICATION supabase_realtime ADD TABLE public.p2p_notifications;
-- =====================================================
-- NOTIFICATION TRIGGER FUNCTIONS
-- =====================================================
-- Function to create notification
CREATE OR REPLACE FUNCTION public.create_p2p_notification(
p_user_id UUID,
p_type TEXT,
p_title TEXT,
p_message TEXT DEFAULT NULL,
p_reference_type TEXT DEFAULT NULL,
p_reference_id UUID DEFAULT NULL,
p_action_url TEXT DEFAULT NULL,
p_metadata JSONB DEFAULT '{}'
) RETURNS UUID AS $$
DECLARE
v_notification_id UUID;
BEGIN
INSERT INTO public.p2p_notifications (
user_id, type, title, message,
reference_type, reference_id, action_url, metadata
) VALUES (
p_user_id, p_type, p_title, p_message,
p_reference_type, p_reference_id, p_action_url, p_metadata
)
RETURNING id INTO v_notification_id;
RETURN v_notification_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Trigger: Notify on new trade
CREATE OR REPLACE FUNCTION notify_on_new_trade()
RETURNS TRIGGER AS $$
DECLARE
v_offer RECORD;
BEGIN
-- Get offer details
SELECT * INTO v_offer FROM public.p2p_fiat_offers WHERE id = NEW.offer_id;
-- Notify seller about new order
PERFORM public.create_p2p_notification(
NEW.seller_id,
'new_order',
'New P2P Order',
format('Someone wants to buy %s %s', NEW.crypto_amount, v_offer.token),
'trade',
NEW.id,
format('/p2p/trade/%s', NEW.id)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER trigger_notify_new_trade
AFTER INSERT ON public.p2p_fiat_trades
FOR EACH ROW EXECUTE FUNCTION notify_on_new_trade();
-- Trigger: Notify on payment sent
CREATE OR REPLACE FUNCTION notify_on_payment_sent()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.status = 'pending' AND NEW.status = 'payment_sent' THEN
PERFORM public.create_p2p_notification(
NEW.seller_id,
'payment_sent',
'Payment Marked as Sent',
'The buyer has marked the payment as sent. Please verify and release.',
'trade',
NEW.id,
format('/p2p/trade/%s', NEW.id)
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER trigger_notify_payment_sent
AFTER UPDATE ON public.p2p_fiat_trades
FOR EACH ROW EXECUTE FUNCTION notify_on_payment_sent();
-- Trigger: Notify on trade completed
CREATE OR REPLACE FUNCTION notify_on_trade_completed()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.status != 'completed' AND NEW.status = 'completed' THEN
-- Notify buyer
PERFORM public.create_p2p_notification(
NEW.buyer_id,
'payment_confirmed',
'Trade Completed!',
'The seller has released the crypto. Check your wallet.',
'trade',
NEW.id,
format('/p2p/trade/%s', NEW.id)
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER trigger_notify_trade_completed
AFTER UPDATE ON public.p2p_fiat_trades
FOR EACH ROW EXECUTE FUNCTION notify_on_trade_completed();
-- Trigger: Notify on dispute opened
CREATE OR REPLACE FUNCTION notify_on_dispute_opened()
RETURNS TRIGGER AS $$
DECLARE
v_trade RECORD;
v_other_party UUID;
BEGIN
-- Get trade details
SELECT * INTO v_trade FROM public.p2p_fiat_trades WHERE id = NEW.trade_id;
-- Determine other party
IF NEW.opened_by = v_trade.seller_id THEN
v_other_party := v_trade.buyer_id;
ELSE
v_other_party := v_trade.seller_id;
END IF;
-- Notify the other party
PERFORM public.create_p2p_notification(
v_other_party,
'dispute_opened',
'Dispute Opened',
'A dispute has been opened for your trade. Please provide evidence.',
'dispute',
NEW.id,
format('/p2p/dispute/%s', NEW.id)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER trigger_notify_dispute_opened
AFTER INSERT ON public.p2p_fiat_disputes
FOR EACH ROW EXECUTE FUNCTION notify_on_dispute_opened();
-- Trigger: Notify on new message
CREATE OR REPLACE FUNCTION notify_on_new_message()
RETURNS TRIGGER AS $$
DECLARE
v_trade RECORD;
v_recipient_id UUID;
BEGIN
-- Skip system messages
IF NEW.message_type = 'system' THEN
RETURN NEW;
END IF;
-- Get trade details
SELECT * INTO v_trade FROM public.p2p_fiat_trades WHERE id = NEW.trade_id;
-- Determine recipient
IF NEW.sender_id = v_trade.seller_id THEN
v_recipient_id := v_trade.buyer_id;
ELSE
v_recipient_id := v_trade.seller_id;
END IF;
-- Create notification
PERFORM public.create_p2p_notification(
v_recipient_id,
'new_message',
'New Message',
LEFT(NEW.message, 100),
'trade',
NEW.trade_id,
format('/p2p/trade/%s', NEW.trade_id)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER trigger_notify_new_message
AFTER INSERT ON public.p2p_messages
FOR EACH ROW EXECUTE FUNCTION notify_on_new_message();
-- =====================================================
-- RATING HELPER FUNCTION
-- =====================================================
CREATE OR REPLACE FUNCTION public.update_user_rating_stats(p_user_id UUID)
RETURNS void AS $$
DECLARE
v_avg_rating NUMERIC;
v_total_ratings INT;
BEGIN
-- Calculate average rating
SELECT
AVG(rating)::NUMERIC(3,2),
COUNT(*)
INTO v_avg_rating, v_total_ratings
FROM public.p2p_ratings
WHERE rated_id = p_user_id;
-- Update reputation with rating info
UPDATE public.p2p_reputation
SET
updated_at = NOW()
-- Note: You could add avg_rating column to p2p_reputation if needed
WHERE user_id = p_user_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Trigger: Update stats after rating
CREATE OR REPLACE FUNCTION update_rating_stats_trigger()
RETURNS TRIGGER AS $$
BEGIN
PERFORM public.update_user_rating_stats(NEW.rated_id);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER trigger_update_rating_stats
AFTER INSERT ON public.p2p_ratings
FOR EACH ROW EXECUTE FUNCTION update_rating_stats_trigger();
-- =====================================================
-- GRANT EXECUTE PERMISSIONS
-- =====================================================
GRANT EXECUTE ON FUNCTION public.create_p2p_notification TO authenticated;
GRANT EXECUTE ON FUNCTION public.update_user_rating_stats TO authenticated;
-- =====================================================
-- ADD INDEXES FOR PERFORMANCE
-- =====================================================
-- Index for finding user's average rating quickly
CREATE INDEX idx_p2p_ratings_avg ON public.p2p_ratings(rated_id, rating);
-- Index for unread notification count
CREATE INDEX idx_p2p_notifications_unread_count ON public.p2p_notifications(user_id)
WHERE is_read = false;
-- =====================================================
-- COMMENTS FOR DOCUMENTATION
-- =====================================================
COMMENT ON TABLE public.p2p_messages IS 'Real-time chat messages between trade participants';
COMMENT ON TABLE public.p2p_ratings IS 'Post-trade ratings and reviews';
COMMENT ON TABLE public.p2p_notifications IS 'User notifications for P2P trading events';
COMMENT ON TABLE public.p2p_dispute_evidence IS 'Evidence files uploaded during disputes';
COMMENT ON TABLE public.p2p_fraud_reports IS 'Fraud reports submitted by users';