feat: Feature 1.2 (NBA stats FastAPI service) + Feature 1.4 (database schema)
Feature 1.2: Python FastAPI microservice wrapping nba_api - GET /stats/season-avg, /stats/last-n, /stats/splits, /players/search - Redis caching (24hr/1hr/6hr/7day), 0.6s rate limiting, PRA derived stat - 27 Python tests passing Feature 1.4: Complete Supabase database schema - 6 tables: users, picks, scan_sessions, bets, outcomes, performance - RLS enabled on all tables with auth.uid() policies - 3 triggers: auto-create user, updated_at, scan count reset - 37 schema validation tests passing - Migration SQL ready, pending manual apply (WSL2 DNS blocker) Total: 92 tests (65 Node.js + 27 Python), all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
-- BetonBLK 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();
|
||||
Reference in New Issue
Block a user