-- Migration: 004_affiliate_tables.sql -- Affiliate system: referral codes, conversions, payouts, wallets -- Created: 2026-04-12 -- ============================================================ -- Table 1: referral_codes -- ============================================================ CREATE TABLE IF NOT EXISTS referral_codes ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, code TEXT NOT NULL UNIQUE, owner_user_id UUID NOT NULL REFERENCES auth.users(id), affiliate_tier TEXT NOT NULL DEFAULT 'standard' CHECK (affiliate_tier IN ('standard', 'premium', 'founder')), commission_pct NUMERIC(5,2) NOT NULL DEFAULT 15.00, max_uses INTEGER, current_uses INTEGER NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT TRUE, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================================ -- Table 2: wallet_addresses (before payouts — FK dependency) -- ============================================================ CREATE TABLE IF NOT EXISTS wallet_addresses ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID NOT NULL REFERENCES auth.users(id), chain TEXT NOT NULL CHECK (chain IN ('ethereum', 'polygon', 'solana', 'bitcoin', 'base', 'arbitrum')), address TEXT NOT NULL, label TEXT, is_primary BOOLEAN NOT NULL DEFAULT FALSE, is_verified BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================================ -- Table 3: referral_conversions -- ============================================================ CREATE TABLE IF NOT EXISTS referral_conversions ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, referral_code_id UUID NOT NULL REFERENCES referral_codes(id), referred_user_id UUID NOT NULL REFERENCES auth.users(id), referrer_user_id UUID NOT NULL REFERENCES auth.users(id), conversion_type TEXT NOT NULL CHECK (conversion_type IN ('signup', 'upgrade', 'renewal')), subscription_tier TEXT NOT NULL CHECK (subscription_tier IN ('analyst', 'desk')), revenue_amount NUMERIC(10,2) NOT NULL DEFAULT 0, commission_amount NUMERIC(10,2) NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'paid', 'disputed', 'refunded')), converted_at TIMESTAMPTZ DEFAULT NOW(), confirmed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================================ -- Table 4: affiliate_payouts -- ============================================================ CREATE TABLE IF NOT EXISTS affiliate_payouts ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, affiliate_user_id UUID NOT NULL REFERENCES auth.users(id), payout_amount NUMERIC(10,2) NOT NULL, payout_method TEXT NOT NULL CHECK (payout_method IN ('crypto', 'paypal', 'bank_transfer', 'stripe')), wallet_address_id UUID REFERENCES wallet_addresses(id), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), transaction_hash TEXT, period_start DATE NOT NULL, period_end DATE NOT NULL, conversions_included INTEGER NOT NULL DEFAULT 0, requested_at TIMESTAMPTZ DEFAULT NOW(), processed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================================ -- Indexes -- ============================================================ CREATE INDEX IF NOT EXISTS idx_referral_codes_owner ON referral_codes(owner_user_id); CREATE INDEX IF NOT EXISTS idx_referral_codes_code ON referral_codes(code); CREATE INDEX IF NOT EXISTS idx_referral_conversions_code ON referral_conversions(referral_code_id); CREATE INDEX IF NOT EXISTS idx_referral_conversions_referrer ON referral_conversions(referrer_user_id); CREATE INDEX IF NOT EXISTS idx_referral_conversions_referred ON referral_conversions(referred_user_id); CREATE INDEX IF NOT EXISTS idx_referral_conversions_status ON referral_conversions(status); CREATE INDEX IF NOT EXISTS idx_affiliate_payouts_user ON affiliate_payouts(affiliate_user_id); CREATE INDEX IF NOT EXISTS idx_affiliate_payouts_status ON affiliate_payouts(status); CREATE INDEX IF NOT EXISTS idx_wallet_addresses_user ON wallet_addresses(user_id); -- ============================================================ -- Row Level Security -- ============================================================ ALTER TABLE referral_codes ENABLE ROW LEVEL SECURITY; ALTER TABLE referral_conversions ENABLE ROW LEVEL SECURITY; ALTER TABLE affiliate_payouts ENABLE ROW LEVEL SECURITY; ALTER TABLE wallet_addresses ENABLE ROW LEVEL SECURITY; -- referral_codes: public read, service role full access CREATE POLICY "referral_codes_public_read" ON referral_codes FOR SELECT USING (true); CREATE POLICY "referral_codes_service_write" ON referral_codes FOR ALL USING (auth.role() = 'service_role'); -- referral_conversions: service role full access only CREATE POLICY "referral_conversions_service_access" ON referral_conversions FOR ALL USING (auth.role() = 'service_role'); -- affiliate_payouts: service role full access only CREATE POLICY "affiliate_payouts_service_access" ON affiliate_payouts FOR ALL USING (auth.role() = 'service_role'); -- wallet_addresses: service role full access only CREATE POLICY "wallet_addresses_service_access" ON wallet_addresses FOR ALL USING (auth.role() = 'service_role');