320 lines
12 KiB
PL/PgSQL
320 lines
12 KiB
PL/PgSQL
-- CREdispo Database Schema
|
|
-- Run this in your Supabase SQL editor
|
|
|
|
-- Enable UUID extension
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
-- ============================================
|
|
-- USERS TABLE
|
|
-- ============================================
|
|
CREATE TABLE public.users (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
auth_id UUID UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
email TEXT UNIQUE NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'agent' CHECK (role IN ('admin', 'agent', 'buyer')),
|
|
name TEXT NOT NULL,
|
|
company TEXT,
|
|
phone TEXT,
|
|
avatar_url TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================
|
|
-- SELLER LEADS TABLE
|
|
-- ============================================
|
|
CREATE TABLE public.seller_leads (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
agent_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
address TEXT NOT NULL,
|
|
asset_type TEXT NOT NULL CHECK (asset_type IN (
|
|
'Office', 'Industrial', 'Retail', 'Multifamily',
|
|
'Mixed-Use', 'Land', 'Hospitality', 'Other'
|
|
)),
|
|
asking_price NUMERIC(15, 2),
|
|
cap_rate NUMERIC(5, 2),
|
|
noi NUMERIC(15, 2),
|
|
sqft INTEGER,
|
|
units INTEGER,
|
|
year_built INTEGER,
|
|
market TEXT NOT NULL,
|
|
city TEXT,
|
|
state TEXT,
|
|
notes TEXT,
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'pending', 'matched', 'closed', 'withdrawn')),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================
|
|
-- SELLER DOCUMENTS TABLE
|
|
-- ============================================
|
|
CREATE TABLE public.seller_documents (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
lead_id UUID NOT NULL REFERENCES public.seller_leads(id) ON DELETE CASCADE,
|
|
file_url TEXT NOT NULL,
|
|
file_name TEXT NOT NULL,
|
|
file_type TEXT NOT NULL,
|
|
file_size INTEGER,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================
|
|
-- BUYERS TABLE
|
|
-- ============================================
|
|
CREATE TABLE public.buyers (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
agent_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
name TEXT NOT NULL,
|
|
company TEXT,
|
|
email TEXT NOT NULL,
|
|
phone TEXT,
|
|
asset_types TEXT[] NOT NULL DEFAULT '{}',
|
|
target_locations TEXT[] NOT NULL DEFAULT '{}',
|
|
price_min NUMERIC(15, 2),
|
|
price_max NUMERIC(15, 2),
|
|
cap_rate_min NUMERIC(5, 2),
|
|
cap_rate_max NUMERIC(5, 2),
|
|
preferences TEXT,
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'unresponsive', 'high-intent', 'inactive')),
|
|
engagement_score INTEGER DEFAULT 0,
|
|
last_contacted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================
|
|
-- MATCHES TABLE
|
|
-- ============================================
|
|
CREATE TABLE public.matches (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
lead_id UUID NOT NULL REFERENCES public.seller_leads(id) ON DELETE CASCADE,
|
|
buyer_id UUID NOT NULL REFERENCES public.buyers(id) ON DELETE CASCADE,
|
|
match_score NUMERIC(5, 2) NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'sent', 'interested', 'passed', 'closed')),
|
|
agent_notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(lead_id, buyer_id)
|
|
);
|
|
|
|
-- ============================================
|
|
-- OUTREACH TABLE
|
|
-- ============================================
|
|
CREATE TABLE public.outreach (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
match_id UUID NOT NULL REFERENCES public.matches(id) ON DELETE CASCADE,
|
|
channel TEXT NOT NULL CHECK (channel IN ('email', 'sms')),
|
|
subject TEXT,
|
|
body TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'queued', 'sent', 'delivered', 'opened', 'clicked', 'replied', 'bounced', 'failed')),
|
|
sent_at TIMESTAMPTZ,
|
|
delivered_at TIMESTAMPTZ,
|
|
opened_at TIMESTAMPTZ,
|
|
clicked_at TIMESTAMPTZ,
|
|
replied_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================
|
|
-- NDAs TABLE
|
|
-- ============================================
|
|
CREATE TABLE public.ndas (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
buyer_id UUID NOT NULL REFERENCES public.buyers(id) ON DELETE CASCADE,
|
|
lead_id UUID NOT NULL REFERENCES public.seller_leads(id) ON DELETE CASCADE,
|
|
signer_name TEXT NOT NULL,
|
|
signer_email TEXT NOT NULL,
|
|
signer_company TEXT,
|
|
signed_at TIMESTAMPTZ DEFAULT NOW(),
|
|
ip_address TEXT,
|
|
user_agent TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(buyer_id, lead_id)
|
|
);
|
|
|
|
-- ============================================
|
|
-- SUBSCRIPTIONS TABLE
|
|
-- ============================================
|
|
CREATE TABLE public.subscriptions (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
stripe_customer_id TEXT UNIQUE,
|
|
stripe_subscription_id TEXT UNIQUE,
|
|
status TEXT NOT NULL DEFAULT 'inactive' CHECK (status IN ('active', 'inactive', 'past_due', 'canceled', 'trialing')),
|
|
plan TEXT DEFAULT 'pro',
|
|
current_period_start TIMESTAMPTZ,
|
|
current_period_end TIMESTAMPTZ,
|
|
cancel_at_period_end BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================
|
|
-- PROMO CODES TABLE
|
|
-- ============================================
|
|
CREATE TABLE public.promo_codes (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
code TEXT UNIQUE NOT NULL,
|
|
discount_percent INTEGER CHECK (discount_percent BETWEEN 1 AND 100),
|
|
discount_amount NUMERIC(10, 2),
|
|
max_uses INTEGER,
|
|
current_uses INTEGER DEFAULT 0,
|
|
expires_at TIMESTAMPTZ,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================
|
|
-- INDEXES
|
|
-- ============================================
|
|
CREATE INDEX idx_seller_leads_agent ON public.seller_leads(agent_id);
|
|
CREATE INDEX idx_seller_leads_status ON public.seller_leads(status);
|
|
CREATE INDEX idx_seller_leads_asset_type ON public.seller_leads(asset_type);
|
|
CREATE INDEX idx_seller_leads_market ON public.seller_leads(market);
|
|
CREATE INDEX idx_buyers_agent ON public.buyers(agent_id);
|
|
CREATE INDEX idx_buyers_status ON public.buyers(status);
|
|
CREATE INDEX idx_matches_lead ON public.matches(lead_id);
|
|
CREATE INDEX idx_matches_buyer ON public.matches(buyer_id);
|
|
CREATE INDEX idx_matches_status ON public.matches(status);
|
|
CREATE INDEX idx_outreach_match ON public.outreach(match_id);
|
|
CREATE INDEX idx_outreach_status ON public.outreach(status);
|
|
CREATE INDEX idx_ndas_buyer ON public.ndas(buyer_id);
|
|
CREATE INDEX idx_ndas_lead ON public.ndas(lead_id);
|
|
CREATE INDEX idx_subscriptions_user ON public.subscriptions(user_id);
|
|
CREATE INDEX idx_subscriptions_status ON public.subscriptions(status);
|
|
|
|
-- ============================================
|
|
-- ROW LEVEL SECURITY (RLS)
|
|
-- ============================================
|
|
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.seller_leads ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.seller_documents ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.buyers ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.matches ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.outreach ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.ndas ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Users can read their own data
|
|
CREATE POLICY "Users can view own profile" ON public.users
|
|
FOR SELECT USING (auth.uid() = auth_id);
|
|
|
|
CREATE POLICY "Users can update own profile" ON public.users
|
|
FOR UPDATE USING (auth.uid() = auth_id);
|
|
|
|
-- Agents can manage their own seller leads
|
|
CREATE POLICY "Agents can view own leads" ON public.seller_leads
|
|
FOR SELECT USING (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid()));
|
|
|
|
CREATE POLICY "Agents can insert leads" ON public.seller_leads
|
|
FOR INSERT WITH CHECK (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid()));
|
|
|
|
CREATE POLICY "Agents can update own leads" ON public.seller_leads
|
|
FOR UPDATE USING (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid()));
|
|
|
|
-- Agents can manage their own buyers
|
|
CREATE POLICY "Agents can view own buyers" ON public.buyers
|
|
FOR SELECT USING (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid()));
|
|
|
|
CREATE POLICY "Agents can insert buyers" ON public.buyers
|
|
FOR INSERT WITH CHECK (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid()));
|
|
|
|
CREATE POLICY "Agents can update own buyers" ON public.buyers
|
|
FOR UPDATE USING (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid()));
|
|
|
|
-- Matches visible to relevant agents
|
|
CREATE POLICY "Agents can view own matches" ON public.matches
|
|
FOR SELECT USING (
|
|
lead_id IN (SELECT id FROM public.seller_leads WHERE agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid()))
|
|
);
|
|
|
|
-- Admin can see everything
|
|
CREATE POLICY "Admins full access users" ON public.users
|
|
FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Admins full access leads" ON public.seller_leads
|
|
FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Admins full access buyers" ON public.buyers
|
|
FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Admins full access matches" ON public.matches
|
|
FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Admins full access outreach" ON public.outreach
|
|
FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- ============================================
|
|
-- STORAGE BUCKETS
|
|
-- ============================================
|
|
INSERT INTO storage.buckets (id, name, public) VALUES ('documents', 'documents', false);
|
|
INSERT INTO storage.buckets (id, name, public) VALUES ('avatars', 'avatars', true);
|
|
|
|
-- Storage policies
|
|
CREATE POLICY "Authenticated users can upload documents"
|
|
ON storage.objects FOR INSERT
|
|
WITH CHECK (bucket_id = 'documents' AND auth.role() = 'authenticated');
|
|
|
|
CREATE POLICY "Users can view own documents"
|
|
ON storage.objects FOR SELECT
|
|
USING (bucket_id = 'documents' AND auth.role() = 'authenticated');
|
|
|
|
-- ============================================
|
|
-- FUNCTIONS
|
|
-- ============================================
|
|
|
|
-- Auto-update updated_at timestamp
|
|
CREATE OR REPLACE FUNCTION update_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON public.users
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
CREATE TRIGGER update_seller_leads_updated_at BEFORE UPDATE ON public.seller_leads
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
CREATE TRIGGER update_buyers_updated_at BEFORE UPDATE ON public.buyers
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
CREATE TRIGGER update_matches_updated_at BEFORE UPDATE ON public.matches
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
CREATE TRIGGER update_subscriptions_updated_at BEFORE UPDATE ON public.subscriptions
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
-- Function to create user profile on signup
|
|
CREATE OR REPLACE FUNCTION handle_new_user()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
INSERT INTO public.users (auth_id, email, name, role)
|
|
VALUES (
|
|
NEW.id,
|
|
NEW.email,
|
|
COALESCE(NEW.raw_user_meta_data->>'name', split_part(NEW.email, '@', 1)),
|
|
COALESCE(NEW.raw_user_meta_data->>'role', 'agent')
|
|
);
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
CREATE TRIGGER on_auth_user_created
|
|
AFTER INSERT ON auth.users
|
|
FOR EACH ROW EXECUTE FUNCTION handle_new_user();
|