-- --------------------------------------------------------------- -- 019 — ledger_entries (Session 58, work-order Phase 1 + CLV amendments). -- -- The truth infrastructure: every grade — user scan AND pipeline pre-grade — -- persists here, settles against the real result, and carries closing-line -- value. Rows with user_id NULL are the PUBLIC model record. -- -- DATA SEMANTICS: `line`, `locked_odds`, `book`, `closing_line`, -- `closing_odds` are REAL book values captured at a timestamp — never -- generated. `model_value` / grade / edge / confidence are VYNDR model -- output. A row must never exist with a fabricated market value. -- -- All writes go through the service role (pipeline + API). Clients only -- read: their own rows, plus the public (user_id IS NULL) record. -- --------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.ledger_entries ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NULL REFERENCES auth.users (id) ON DELETE CASCADE, player_key text NOT NULL, player_name text NOT NULL, sport text NOT NULL, stat text NOT NULL, line numeric NOT NULL, side text NOT NULL CHECK (side IN ('over', 'under')), locked_odds text NULL, book text NULL, grade text NOT NULL, edge numeric NULL, confidence numeric NULL, model_value numeric NULL, graded_at timestamptz NOT NULL DEFAULT now(), game_id text NOT NULL, game_date date NOT NULL, closing_line numeric NULL, closing_odds text NULL, clv numeric NULL, clv_result text NULL CHECK (clv_result IN ('beat', 'faded', 'flat')), outcome text NULL CHECK (outcome IN ('hit', 'miss', 'push')), actual_value numeric NULL, settled_at timestamptz NULL, revised_from_grade text NULL ); -- Idempotency: pipeline re-runs and double-taps upsert into this. PG15+ -- NULLS NOT DISTINCT makes the null-user (public record) rows dedupe too, -- and — unlike a coalesce-expression index — PostgREST `on_conflict` can -- target a real constraint's column list. ALTER TABLE public.ledger_entries ADD CONSTRAINT ledger_entries_dedupe UNIQUE NULLS NOT DISTINCT (user_id, player_key, stat, line, side, game_id); CREATE INDEX IF NOT EXISTS idx_ledger_user_graded ON public.ledger_entries (user_id, graded_at DESC); CREATE INDEX IF NOT EXISTS idx_ledger_gamedate_outcome ON public.ledger_entries (game_date, outcome); CREATE INDEX IF NOT EXISTS idx_ledger_player_stat ON public.ledger_entries (player_key, stat); CREATE INDEX IF NOT EXISTS idx_ledger_public_clv ON public.ledger_entries (clv_result) WHERE user_id IS NULL; -- The settlement worker's scan: unsettled rows by date. CREATE INDEX IF NOT EXISTS idx_ledger_unsettled ON public.ledger_entries (game_date) WHERE outcome IS NULL; ALTER TABLE public.ledger_entries ENABLE ROW LEVEL SECURITY; -- Users read their own rows. CREATE POLICY ledger_select_own ON public.ledger_entries FOR SELECT TO authenticated USING (user_id = auth.uid()); -- Everyone (anon + authenticated) reads the public model record. CREATE POLICY ledger_select_public ON public.ledger_entries FOR SELECT TO anon, authenticated USING (user_id IS NULL); -- No INSERT/UPDATE/DELETE policies: client-side writes are impossible. -- The pipeline + API write via the service role, which bypasses RLS.