217 lines
7.5 KiB
PL/PgSQL
217 lines
7.5 KiB
PL/PgSQL
-- VYNDR Initial Schema
|
|
-- Feature 1.4 — All tables, indexes, RLS policies, triggers
|
|
-- All timestamps use TIMESTAMPTZ (UTC)
|
|
|
|
-- ============================================================
|
|
-- TABLE: users
|
|
-- Extends auth.users with app-specific profile data
|
|
-- ============================================================
|
|
CREATE TABLE public.users (
|
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
email TEXT NOT NULL,
|
|
tier TEXT NOT NULL DEFAULT 'free' CHECK (tier IN ('free', 'analyst', 'desk')),
|
|
scan_count INT NOT NULL DEFAULT 0,
|
|
scan_reset_date TIMESTAMPTZ NOT NULL DEFAULT (date_trunc('month', now()) + interval '1 month'),
|
|
stripe_customer_id TEXT,
|
|
founder_status BOOLEAN NOT NULL DEFAULT false,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "users_select_own" ON public.users
|
|
FOR SELECT USING (auth.uid() = id);
|
|
|
|
CREATE POLICY "users_update_own" ON public.users
|
|
FOR UPDATE USING (auth.uid() = id)
|
|
WITH CHECK (auth.uid() = id);
|
|
|
|
CREATE POLICY "users_insert_own" ON public.users
|
|
FOR INSERT WITH CHECK (auth.uid() = id);
|
|
|
|
-- ============================================================
|
|
-- TABLE: picks
|
|
-- Individual prop analysis results from scans
|
|
-- ============================================================
|
|
CREATE TABLE public.picks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
player TEXT NOT NULL,
|
|
stat_type TEXT NOT NULL,
|
|
line NUMERIC(5,1) NOT NULL,
|
|
book TEXT NOT NULL,
|
|
direction TEXT NOT NULL CHECK (direction IN ('over', 'under')),
|
|
grade TEXT NOT NULL CHECK (grade IN ('A', 'B', 'C', 'D')),
|
|
edge_pct NUMERIC(5,2),
|
|
reasoning TEXT,
|
|
kill_conditions TEXT[],
|
|
confidence NUMERIC(4,2),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_picks_user_id ON public.picks(user_id);
|
|
CREATE INDEX idx_picks_created_at ON public.picks(created_at);
|
|
|
|
ALTER TABLE public.picks ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "picks_select_own" ON public.picks
|
|
FOR SELECT USING (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "picks_insert_own" ON public.picks
|
|
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- TABLE: scan_sessions
|
|
-- Groups picks into a single scan/parlay analysis session
|
|
-- ============================================================
|
|
CREATE TABLE public.scan_sessions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
legs UUID[] NOT NULL DEFAULT '{}',
|
|
final_grade TEXT CHECK (final_grade IN ('A', 'B', 'C', 'D')),
|
|
kill_conditions TEXT[],
|
|
correlation_notes TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_scan_sessions_user_id ON public.scan_sessions(user_id);
|
|
|
|
ALTER TABLE public.scan_sessions ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "scan_sessions_select_own" ON public.scan_sessions
|
|
FOR SELECT USING (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "scan_sessions_insert_own" ON public.scan_sessions
|
|
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- TABLE: bets
|
|
-- User-submitted bets (screenshot, quick slip, sportsbook sync)
|
|
-- ============================================================
|
|
CREATE TABLE public.bets (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
amount NUMERIC(10,2) NOT NULL,
|
|
potential_payout NUMERIC(10,2),
|
|
slip_data JSONB NOT NULL,
|
|
book TEXT NOT NULL,
|
|
bet_type TEXT NOT NULL CHECK (bet_type IN ('straight', 'parlay', 'teaser', 'round_robin')),
|
|
submission_method TEXT NOT NULL CHECK (submission_method IN ('screenshot', 'quickslip', 'sync')),
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'won', 'lost', 'push', 'void')),
|
|
placed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
settled_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_bets_user_id ON public.bets(user_id);
|
|
CREATE INDEX idx_bets_status ON public.bets(status);
|
|
CREATE INDEX idx_bets_placed_at ON public.bets(placed_at);
|
|
|
|
ALTER TABLE public.bets ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "bets_select_own" ON public.bets
|
|
FOR SELECT USING (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "bets_insert_own" ON public.bets
|
|
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "bets_update_own" ON public.bets
|
|
FOR UPDATE USING (auth.uid() = user_id)
|
|
WITH CHECK (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- TABLE: outcomes
|
|
-- Actual results for each pick after game is played
|
|
-- ============================================================
|
|
CREATE TABLE public.outcomes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
pick_id UUID NOT NULL REFERENCES public.picks(id) ON DELETE CASCADE,
|
|
result TEXT NOT NULL CHECK (result IN ('hit', 'miss', 'push')),
|
|
actual_value NUMERIC(5,1) NOT NULL,
|
|
logged_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE UNIQUE INDEX idx_outcomes_pick_id ON public.outcomes(pick_id);
|
|
|
|
ALTER TABLE public.outcomes ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "outcomes_select_own" ON public.outcomes
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM public.picks
|
|
WHERE picks.id = outcomes.pick_id AND picks.user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- ============================================================
|
|
-- TABLE: performance
|
|
-- Aggregated performance metrics per user per period
|
|
-- ============================================================
|
|
CREATE TABLE public.performance (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
period TEXT NOT NULL CHECK (period IN ('weekly', 'monthly', 'all_time')),
|
|
roi NUMERIC(6,2),
|
|
win_rate NUMERIC(5,2),
|
|
sample_size INT NOT NULL DEFAULT 0,
|
|
total_wagered NUMERIC(10,2) DEFAULT 0,
|
|
total_profit NUMERIC(10,2) DEFAULT 0,
|
|
calculated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_performance_user_id ON public.performance(user_id);
|
|
CREATE UNIQUE INDEX idx_performance_user_period ON public.performance(user_id, period);
|
|
|
|
ALTER TABLE public.performance ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "performance_select_own" ON public.performance
|
|
FOR SELECT USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- TRIGGERS
|
|
-- ============================================================
|
|
|
|
-- 1. Auto-create user profile on signup
|
|
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
INSERT INTO public.users (id, email)
|
|
VALUES (NEW.id, NEW.email);
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
CREATE TRIGGER on_auth_user_created
|
|
AFTER INSERT ON auth.users
|
|
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
|
|
|
-- 2. Auto-update updated_at on users table
|
|
CREATE OR REPLACE FUNCTION public.update_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = now();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER users_updated_at
|
|
BEFORE UPDATE ON public.users
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();
|
|
|
|
-- 3. Monthly scan count reset
|
|
CREATE OR REPLACE FUNCTION public.reset_scan_count()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.scan_reset_date <= now() THEN
|
|
NEW.scan_count = 0;
|
|
NEW.scan_reset_date = date_trunc('month', now()) + interval '1 month';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER users_scan_reset
|
|
BEFORE UPDATE ON public.users
|
|
FOR EACH ROW EXECUTE FUNCTION public.reset_scan_count();
|