mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-08-12 06:51:00 +00:00
e18ba679be
The migrations did not describe this database, and could not be made to.
Three findings, in the order they surfaced:
* 25 functions declared across six migrations — all recorded as applied — did
not exist. Their tables did. Two were reached by the app, so merchant tier
upgrades and post-trade reputation updates had been quietly dead. This only
came to light because a user hit "Could not find the function
public.upsert_user_profile(...)" while toggling a notification setting.
* admin_roles has three conflicting definitions across the set and production
matches none of them. 001 says (id, user_id, role, granted_by, granted_at),
COMBINED says (user_id, role, created_at), production has
(id, user_id, role, permissions, created_at, updated_at).
* Applied to an empty database, five migrations fail. The legacy 0NN filenames
sort before the 14-digit timestamps they depend on — "013" < "20241117054600"
— so 013 runs before the migration creating the table it alters. The set
could never have been replayed from scratch.
So it could not be tested, could not rebuild the database, and did not match what
was running. Widening or patching it would have been dressing up a history that
was already fiction.
The baseline is a pg_dump of the live public schema, which matches production by
construction. Privileges are included deliberately: the REVOKEs on
lock_escrow_internal, release_escrow_internal, refund_escrow_internal and
request_withdraw are the 20260725030000 hardening, and dropping them would hand
fund movement back to anon.
Verified step by step against production before committing:
- applies to an empty database cleanly, after three real obstacles were fixed
(extensions live in the `extensions` schema, supabase_admin membership,
platform-level ALTER DEFAULT PRIVILEGES that cannot apply outside Supabase)
- produces 83 tables / 39 functions / 222 indexes / 360 policies — identical
counts to production
- recorded as applied in supabase_migrations without touching the 37 existing
rows, then dry-run confirmed "up to date — no pending migrations", so the
next deploy will not try to replay it over live tables
- drift check now reports "every declared function is present"; the known-gaps
list drops from 22 entries to zero
The old files move to migrations/archive/ rather than being deleted — they are
the only record of why parts of this look the way they do. Their README says
plainly not to run them, and why.
CI now applies migrations to an empty Postgres on every PR. That check was
impossible while the old set was the starting point; it is the thing that stops
this class of drift from being discovered by a user again.
567 lines
18 KiB
PL/PgSQL
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';
|