Sessions 5-7a: 955 tests, deployment ready

This commit is contained in:
Kev
2026-06-08 18:35:13 -04:00
parent 06b82624a2
commit 1fa04dc776
371 changed files with 49366 additions and 955 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
-- BetonBLK Initial Schema
-- VYNDR Initial Schema
-- Feature 1.4 — All tables, indexes, RLS policies, triggers
-- All timestamps use TIMESTAMPTZ (UTC)
@@ -0,0 +1,177 @@
-- Migration: 003_phase1_additions.sql
-- Phase 1 additions: Role profiles, activations, predictions, joint outcomes, discrepancy scores
-- Created: 2026-03-28
-- ============================================================
-- Table 1: player_role_profiles
-- ============================================================
CREATE TABLE IF NOT EXISTS player_role_profiles (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
player_id TEXT NOT NULL,
player_name TEXT NOT NULL,
sport TEXT NOT NULL DEFAULT 'NBA',
team TEXT,
role_profile JSONB NOT NULL DEFAULT '{}',
conditional_roles JSONB DEFAULT '{}',
dominant_role TEXT,
role_variance_score NUMERIC(4,3) DEFAULT 0,
effective_date DATE DEFAULT CURRENT_DATE,
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- Table 2: lineup_role_profiles
-- ============================================================
CREATE TABLE IF NOT EXISTS lineup_role_profiles (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
lineup_id TEXT NOT NULL,
player_id TEXT NOT NULL,
sport TEXT NOT NULL DEFAULT 'NBA',
team TEXT,
role_in_this_lineup TEXT,
historical_production JSONB DEFAULT '{}',
instances_tracked INTEGER DEFAULT 0,
last_game_date DATE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- Table 3: player_role_activations
-- ============================================================
CREATE TABLE IF NOT EXISTS player_role_activations (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
game_id TEXT,
player_id TEXT NOT NULL,
activated_role TEXT NOT NULL,
activation_trigger TEXT,
activation_timestamp TIMESTAMPTZ DEFAULT NOW(),
pre_activation_line NUMERIC(6,2),
post_activation_line NUMERIC(6,2),
line_movement_after NUMERIC(6,2),
actual_result NUMERIC(6,2),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- Table 4: model_predictions_extended
-- ============================================================
CREATE TABLE IF NOT EXISTS model_predictions_extended (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
prediction_id UUID REFERENCES picks(id),
role_stability_score NUMERIC(4,3),
role_variance_score NUMERIC(4,3),
active_role_tonight TEXT,
elevation_detected BOOLEAN DEFAULT FALSE,
evolution_detected BOOLEAN DEFAULT FALSE,
evolution_confidence NUMERIC(4,3),
similarity_instances_found INTEGER DEFAULT 0,
model_probability NUMERIC(5,4),
confidence_interval_low NUMERIC(6,2),
confidence_interval_high NUMERIC(6,2),
confidence_interval_asymmetric BOOLEAN DEFAULT FALSE,
optimal_alt_line NUMERIC(6,2),
optimal_alt_line_edge NUMERIC(5,4),
discrepancy_window_flag BOOLEAN DEFAULT FALSE,
clv_at_prediction NUMERIC(6,2),
clv_at_24hr NUMERIC(6,2),
clv_at_tip NUMERIC(6,2),
model_version TEXT DEFAULT '1.0',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- Table 5: prediction_registry
-- ============================================================
CREATE TABLE IF NOT EXISTS prediction_registry (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
prediction_type TEXT NOT NULL,
player_id TEXT NOT NULL,
sport TEXT NOT NULL,
stat_type TEXT NOT NULL,
prediction_detail JSONB NOT NULL,
registered_at TIMESTAMPTZ DEFAULT NOW(),
outcome TEXT DEFAULT 'pending' CHECK (outcome IN ('pending', 'verified', 'false')),
verified_at TIMESTAMPTZ,
public_url TEXT
);
-- ============================================================
-- Table 6: joint_outcomes
-- ============================================================
CREATE TABLE IF NOT EXISTS joint_outcomes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
game_id TEXT,
player_a_id TEXT NOT NULL,
player_a_stat TEXT NOT NULL,
player_a_result NUMERIC(6,2),
player_b_id TEXT NOT NULL,
player_b_stat TEXT NOT NULL,
player_b_result NUMERIC(6,2),
phi_coefficient NUMERIC(5,4),
sample_size INTEGER DEFAULT 0,
game_state_at_outcome TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- Table 7: discrepancy_reliability_scores
-- ============================================================
CREATE TABLE IF NOT EXISTS discrepancy_reliability_scores (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
prop_type TEXT NOT NULL,
sport TEXT NOT NULL,
reliability_score NUMERIC(4,3),
avg_resolution_minutes NUMERIC(6,1),
sample_size INTEGER DEFAULT 0,
last_updated TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- Indexes
-- ============================================================
CREATE INDEX IF NOT EXISTS idx_role_profiles_player ON player_role_profiles(player_id);
CREATE INDEX IF NOT EXISTS idx_role_profiles_sport_team ON player_role_profiles(sport, team);
CREATE INDEX IF NOT EXISTS idx_lineup_roles_player ON lineup_role_profiles(player_id);
CREATE INDEX IF NOT EXISTS idx_lineup_roles_lineup ON lineup_role_profiles(lineup_id);
CREATE INDEX IF NOT EXISTS idx_role_activations_player ON player_role_activations(player_id);
CREATE INDEX IF NOT EXISTS idx_role_activations_game ON player_role_activations(game_id);
CREATE INDEX IF NOT EXISTS idx_predictions_ext_prediction ON model_predictions_extended(prediction_id);
CREATE INDEX IF NOT EXISTS idx_prediction_registry_player ON prediction_registry(player_id, sport);
CREATE INDEX IF NOT EXISTS idx_prediction_registry_outcome ON prediction_registry(outcome);
CREATE INDEX IF NOT EXISTS idx_joint_outcomes_players ON joint_outcomes(player_a_id, player_b_id);
CREATE INDEX IF NOT EXISTS idx_joint_outcomes_game ON joint_outcomes(game_id);
CREATE INDEX IF NOT EXISTS idx_discrepancy_scores_prop ON discrepancy_reliability_scores(prop_type, sport);
-- ============================================================
-- Row Level Security
-- ============================================================
ALTER TABLE player_role_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE lineup_role_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE player_role_activations ENABLE ROW LEVEL SECURITY;
ALTER TABLE model_predictions_extended ENABLE ROW LEVEL SECURITY;
ALTER TABLE prediction_registry ENABLE ROW LEVEL SECURITY;
ALTER TABLE joint_outcomes ENABLE ROW LEVEL SECURITY;
ALTER TABLE discrepancy_reliability_scores ENABLE ROW LEVEL SECURITY;
-- Public read on prediction_registry
CREATE POLICY "prediction_registry_public_read" ON prediction_registry
FOR SELECT USING (true);
-- Service role write on prediction_registry
CREATE POLICY "prediction_registry_service_write" ON prediction_registry
FOR INSERT WITH CHECK (auth.role() = 'service_role');
-- Service role full access on all other tables
CREATE POLICY "role_profiles_service_access" ON player_role_profiles
FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "lineup_roles_service_access" ON lineup_role_profiles
FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "role_activations_service_access" ON player_role_activations
FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "predictions_ext_service_access" ON model_predictions_extended
FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "joint_outcomes_service_access" ON joint_outcomes
FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "discrepancy_scores_service_access" ON discrepancy_reliability_scores
FOR ALL USING (auth.role() = 'service_role');
@@ -0,0 +1,112 @@
-- 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');
@@ -0,0 +1,26 @@
-- Migration: 005_lineup_scheme_data.sql
-- Lineup scheme data for nightly play-by-play collection.
-- Created: 2026-04-13
CREATE TABLE IF NOT EXISTS lineup_scheme_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
game_id TEXT NOT NULL,
game_date DATE NOT NULL,
team_id TEXT NOT NULL,
lineup_hash TEXT NOT NULL,
player_ids TEXT[] NOT NULL,
play_type TEXT NOT NULL,
possessions INTEGER DEFAULT 0,
points INTEGER DEFAULT 0,
fg_made INTEGER DEFAULT 0,
fg_attempted INTEGER DEFAULT 0,
turnovers INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ls_team ON lineup_scheme_data(team_id);
CREATE INDEX IF NOT EXISTS idx_ls_hash ON lineup_scheme_data(lineup_hash);
CREATE INDEX IF NOT EXISTS idx_ls_date ON lineup_scheme_data(game_date);
ALTER TABLE lineup_scheme_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY ls_svc ON lineup_scheme_data FOR ALL USING (auth.role() = 'service_role');
@@ -0,0 +1,129 @@
-- Migration: 006_data_warehouse_calibration.sql
-- Data cache tables, grade_outcomes (all ship-version columns),
-- player_calibrated_weights.
-- Created: 2026-04-13
-- ============================================================
-- NBA Data Cache
-- ============================================================
CREATE TABLE IF NOT EXISTS nba_data_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cache_key TEXT NOT NULL UNIQUE,
data JSONB NOT NULL,
fetched_at TIMESTAMPTZ DEFAULT NOW(),
ttl_hours INTEGER DEFAULT 6
);
CREATE INDEX IF NOT EXISTS idx_nba_ck ON nba_data_cache(cache_key);
-- ============================================================
-- MLB Data Cache
-- ============================================================
CREATE TABLE IF NOT EXISTS mlb_data_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cache_key TEXT NOT NULL UNIQUE,
data JSONB NOT NULL,
fetched_at TIMESTAMPTZ DEFAULT NOW(),
ttl_hours INTEGER DEFAULT 24
);
CREATE INDEX IF NOT EXISTS idx_mlb_ck ON mlb_data_cache(cache_key);
-- ============================================================
-- Grade Outcomes — ALL ship-version columns
-- ============================================================
CREATE TABLE IF NOT EXISTS grade_outcomes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sport TEXT NOT NULL,
player_id TEXT NOT NULL,
player_name TEXT,
stat_type TEXT NOT NULL,
prop_line DECIMAL NOT NULL,
over_under TEXT NOT NULL,
grade TEXT,
abstained BOOLEAN DEFAULT FALSE,
abstention_reason TEXT,
confidence DECIMAL,
global_offset_applied DECIMAL DEFAULT 0.0,
projected_value DECIMAL,
projected_std DECIMAL,
actual_value DECIMAL,
hit BOOLEAN,
real_edge DECIMAL,
implied_probability DECIMAL,
ev_per_dollar DECIMAL,
kelly_recommended_pct DECIMAL,
sub_scores JSONB NOT NULL,
player_weights JSONB NOT NULL,
bayesian_weights_used JSONB,
similar_game_count INTEGER,
similar_game_modifier DECIMAL,
archetype_scores JSONB,
player_profile_snapshot JSONB,
pitcher_profile_snapshot JSONB,
matchup_context JSONB,
game_context JSONB,
weather_context JSONB,
teammate_context JSONB,
bullpen_context JSONB,
lineup_protection_context JSONB,
home_road_context JSONB,
day_night_context JSONB,
opponent_quality_context JSONB,
travel_fatigue_context JSONB,
regime_check JSONB,
lineup_status TEXT,
lineup_source TEXT,
line_source TEXT,
american_odds INTEGER,
bookmaker TEXT,
clv_opening_line DECIMAL,
clv_closing_line DECIMAL,
clv_movement DECIMAL,
clv_win BOOLEAN,
market_alignment TEXT,
discipline_score DECIMAL,
umpire_id TEXT,
umpire_adjustment DECIMAL,
referee_crew_id TEXT,
referee_adjustment DECIMAL,
pick_number INTEGER,
capper_post_text TEXT,
game_id TEXT,
game_date DATE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
resolved_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_go_player ON grade_outcomes(player_id);
CREATE INDEX IF NOT EXISTS idx_go_sport ON grade_outcomes(sport);
CREATE INDEX IF NOT EXISTS idx_go_date ON grade_outcomes(game_date);
CREATE INDEX IF NOT EXISTS idx_go_resolved ON grade_outcomes(resolved_at);
CREATE INDEX IF NOT EXISTS idx_go_grade ON grade_outcomes(grade);
CREATE INDEX IF NOT EXISTS idx_go_abstained ON grade_outcomes(abstained);
-- ============================================================
-- Player Calibrated Weights
-- ============================================================
CREATE TABLE IF NOT EXISTS player_calibrated_weights (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_id TEXT NOT NULL,
sport TEXT NOT NULL,
stat_type TEXT NOT NULL,
weights JSONB NOT NULL,
sample_size INTEGER NOT NULL,
calibrated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(player_id, sport, stat_type)
);
CREATE INDEX IF NOT EXISTS idx_pcw_player ON player_calibrated_weights(player_id);
-- ============================================================
-- Row Level Security
-- ============================================================
ALTER TABLE nba_data_cache ENABLE ROW LEVEL SECURITY;
ALTER TABLE mlb_data_cache ENABLE ROW LEVEL SECURITY;
ALTER TABLE grade_outcomes ENABLE ROW LEVEL SECURITY;
ALTER TABLE player_calibrated_weights ENABLE ROW LEVEL SECURITY;
CREATE POLICY nba_c_svc ON nba_data_cache FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY mlb_c_svc ON mlb_data_cache FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY go_svc ON grade_outcomes FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY pcw_svc ON player_calibrated_weights FOR ALL USING (auth.role() = 'service_role');
@@ -0,0 +1,172 @@
-- Migration: 007_lineup_odds_trust_health.sql
-- Lineup updates, reporter trust, odds warehouse, line movements,
-- reporter-line correlation, API health log, global calibration, joint outcomes.
-- Created: 2026-04-13
-- ============================================================
-- Lineup Updates
-- ============================================================
CREATE TABLE IF NOT EXISTS lineup_updates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sport TEXT NOT NULL,
team_id TEXT NOT NULL,
player_name TEXT NOT NULL,
player_id TEXT,
status TEXT NOT NULL,
source TEXT NOT NULL,
trust_level TEXT NOT NULL,
reporter_handle TEXT,
confidence DECIMAL,
raw_data JSONB,
game_date DATE NOT NULL,
detected_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_lu_team ON lineup_updates(team_id);
CREATE INDEX IF NOT EXISTS idx_lu_date ON lineup_updates(game_date);
-- ============================================================
-- Reporter Trust
-- ============================================================
CREATE TABLE IF NOT EXISTS reporter_trust (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
handle TEXT NOT NULL UNIQUE,
sport TEXT NOT NULL,
team_id TEXT NOT NULL,
outlet TEXT,
source_type TEXT NOT NULL DEFAULT 'beat_writer',
trust_level TEXT DEFAULT 'unverified',
starting_trust TEXT,
total_tracked INTEGER DEFAULT 0,
correct_count INTEGER DEFAULT 0,
accuracy DECIMAL DEFAULT 0.0,
last_tracked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_rt_handle ON reporter_trust(handle);
CREATE INDEX IF NOT EXISTS idx_rt_trust ON reporter_trust(trust_level);
-- ============================================================
-- Odds Warehouse
-- ============================================================
CREATE TABLE IF NOT EXISTS odds_warehouse (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sport TEXT NOT NULL,
game_id TEXT NOT NULL,
game_date DATE NOT NULL,
player_name TEXT NOT NULL,
player_id TEXT,
market TEXT NOT NULL,
bookmaker TEXT NOT NULL,
line DECIMAL,
price INTEGER,
over_under TEXT,
scan_type TEXT NOT NULL,
fetched_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ow_sport_date ON odds_warehouse(sport, game_date);
CREATE INDEX IF NOT EXISTS idx_ow_player ON odds_warehouse(player_name);
CREATE INDEX IF NOT EXISTS idx_ow_scan ON odds_warehouse(scan_type);
-- ============================================================
-- Line Movements (ship version — new table, not conflicting with existing)
-- ============================================================
CREATE TABLE IF NOT EXISTS ship_line_movements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sport TEXT NOT NULL,
player_name TEXT NOT NULL,
market TEXT NOT NULL,
bookmaker TEXT NOT NULL,
opening_line DECIMAL,
current_line DECIMAL,
movement DECIMAL,
direction TEXT,
game_date DATE NOT NULL,
detected_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_slm_sport_date ON ship_line_movements(sport, game_date);
-- ============================================================
-- Reporter-Line Correlation
-- ============================================================
CREATE TABLE IF NOT EXISTS reporter_line_correlation (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reporter_handle TEXT NOT NULL,
sport TEXT NOT NULL,
player_name TEXT NOT NULL,
tweet_content TEXT,
tweet_time TIMESTAMPTZ NOT NULL,
line_movement_time TIMESTAMPTZ,
gap_minutes DECIMAL,
prop_affected TEXT,
line_before DECIMAL,
line_after DECIMAL,
bookmaker TEXT,
game_date DATE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_rlc_reporter ON reporter_line_correlation(reporter_handle);
CREATE INDEX IF NOT EXISTS idx_rlc_date ON reporter_line_correlation(game_date);
-- ============================================================
-- API Health Log
-- ============================================================
CREATE TABLE IF NOT EXISTS api_health_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
api_name TEXT NOT NULL,
error_message TEXT,
games_tonight INTEGER DEFAULT 0,
failed_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ahl_api ON api_health_log(api_name);
CREATE INDEX IF NOT EXISTS idx_ahl_time ON api_health_log(failed_at);
-- ============================================================
-- Global Calibration
-- ============================================================
CREATE TABLE IF NOT EXISTS global_calibration (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sport TEXT NOT NULL UNIQUE,
offset_value DECIMAL DEFAULT 0.0,
brier_score DECIMAL,
sample_size INTEGER DEFAULT 0,
blind_spots JSONB,
calculated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- Joint Outcomes (ship version)
-- ============================================================
CREATE TABLE IF NOT EXISTS ship_joint_outcomes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_a_id TEXT NOT NULL,
player_b_id TEXT NOT NULL,
stat_a TEXT NOT NULL,
stat_b TEXT NOT NULL,
hit_a BOOLEAN NOT NULL,
hit_b BOOLEAN NOT NULL,
game_date DATE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_sjo_players ON ship_joint_outcomes(player_a_id, player_b_id);
CREATE INDEX IF NOT EXISTS idx_sjo_date ON ship_joint_outcomes(game_date);
-- ============================================================
-- Row Level Security
-- ============================================================
ALTER TABLE lineup_updates ENABLE ROW LEVEL SECURITY;
ALTER TABLE reporter_trust ENABLE ROW LEVEL SECURITY;
ALTER TABLE odds_warehouse ENABLE ROW LEVEL SECURITY;
ALTER TABLE ship_line_movements ENABLE ROW LEVEL SECURITY;
ALTER TABLE reporter_line_correlation ENABLE ROW LEVEL SECURITY;
ALTER TABLE api_health_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE global_calibration ENABLE ROW LEVEL SECURITY;
ALTER TABLE ship_joint_outcomes ENABLE ROW LEVEL SECURITY;
CREATE POLICY lu_svc ON lineup_updates FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY rt_svc ON reporter_trust FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY ow_svc ON odds_warehouse FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY slm_svc ON ship_line_movements FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY rlc_svc ON reporter_line_correlation FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY ahl_svc ON api_health_log FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY gc_svc ON global_calibration FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY sjo_svc ON ship_joint_outcomes FOR ALL USING (auth.role() = 'service_role');
@@ -0,0 +1,82 @@
-- Migration: 008_supplement_tables.sql
-- Supplement intelligence systems: coaching tendencies, player-out history,
-- evolution detections, unconventional validations.
-- Created: 2026-04-13
-- ============================================================
-- Coaching Tendencies
-- ============================================================
CREATE TABLE IF NOT EXISTS coaching_tendencies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
coach_id TEXT NOT NULL,
team_id TEXT NOT NULL,
sport TEXT NOT NULL,
season TEXT NOT NULL,
tendencies JSONB NOT NULL,
games_sampled INTEGER DEFAULT 0,
last_updated DATE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(coach_id, team_id, sport, season)
);
CREATE INDEX IF NOT EXISTS idx_ct_coach ON coaching_tendencies(coach_id);
CREATE INDEX IF NOT EXISTS idx_ct_team ON coaching_tendencies(team_id);
-- ============================================================
-- Player-Out History (redistribution training data)
-- ============================================================
CREATE TABLE IF NOT EXISTS player_out_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_out_id TEXT NOT NULL,
team_id TEXT NOT NULL,
game_id TEXT NOT NULL,
game_date DATE NOT NULL,
beneficiary_stats JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_poh_player ON player_out_history(player_out_id);
CREATE INDEX IF NOT EXISTS idx_poh_team ON player_out_history(team_id);
-- ============================================================
-- Evolution Detections (timestamped accuracy ledger)
-- ============================================================
CREATE TABLE IF NOT EXISTS evolution_detections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_id TEXT NOT NULL,
player_name TEXT,
sport TEXT NOT NULL,
detection_date DATE NOT NULL,
metrics JSONB NOT NULL,
market_adjusted_at DATE,
confirmed BOOLEAN,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ed_player ON evolution_detections(player_id);
CREATE INDEX IF NOT EXISTS idx_ed_date ON evolution_detections(detection_date);
-- ============================================================
-- Unconventional Factor Validation Log
-- ============================================================
CREATE TABLE IF NOT EXISTS unconventional_validations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
factor_name TEXT NOT NULL,
validated BOOLEAN NOT NULL,
pearson_r DECIMAL,
p_value DECIMAL,
corrected_alpha DECIMAL,
sample_size INTEGER,
validated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_uv_factor ON unconventional_validations(factor_name);
-- ============================================================
-- Row Level Security
-- ============================================================
ALTER TABLE coaching_tendencies ENABLE ROW LEVEL SECURITY;
ALTER TABLE player_out_history ENABLE ROW LEVEL SECURITY;
ALTER TABLE evolution_detections ENABLE ROW LEVEL SECURITY;
ALTER TABLE unconventional_validations ENABLE ROW LEVEL SECURITY;
CREATE POLICY ct_svc ON coaching_tendencies FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY poh_svc ON player_out_history FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY ed_svc ON evolution_detections FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY uv_svc ON unconventional_validations FOR ALL USING (auth.role() = 'service_role');
@@ -0,0 +1,28 @@
-- Migration: 009_patch_supplement.sql
-- Supplement context columns on grade_outcomes + unconventional_factor_data table.
-- Created: 2026-04-13
-- ============================================================
-- Add supplement columns to grade_outcomes
-- ============================================================
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS coaching_context JSONB;
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS redistribution_context JSONB;
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS evolution_flag BOOLEAN DEFAULT FALSE;
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS alt_line_opportunity JSONB;
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS unconventional_factors JSONB;
-- ============================================================
-- Unconventional Factor Data (daily collection for validation)
-- ============================================================
CREATE TABLE IF NOT EXISTS unconventional_factor_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
factor_name TEXT NOT NULL,
game_id TEXT NOT NULL,
game_date DATE NOT NULL,
factor_value JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ufd_factor ON unconventional_factor_data(factor_name);
CREATE INDEX IF NOT EXISTS idx_ufd_date ON unconventional_factor_data(game_date);
ALTER TABLE unconventional_factor_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY ufd_svc ON unconventional_factor_data FOR ALL USING (auth.role() = 'service_role');
@@ -0,0 +1,16 @@
-- Migration: 010_security_events.sql
-- Security events table for audit logging.
-- Created: 2026-04-13
CREATE TABLE IF NOT EXISTS security_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type TEXT NOT NULL,
ip_address TEXT,
path TEXT,
detail TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_se_type ON security_events(event_type);
CREATE INDEX IF NOT EXISTS idx_se_time ON security_events(created_at);
ALTER TABLE security_events ENABLE ROW LEVEL SECURITY;
CREATE POLICY se_svc ON security_events FOR ALL USING (auth.role() = 'service_role');
@@ -0,0 +1,164 @@
-- VYNDR Web — user profile + scan throttle + parlay leg frequency
-- Idempotent. Safe to apply multiple times.
create extension if not exists "pgcrypto";
-- ────────────────────────────────────────────────────────────
-- user_profiles
-- ────────────────────────────────────────────────────────────
create table if not exists public.user_profiles (
id uuid primary key references auth.users(id) on delete cascade,
email text,
tier text not null default 'free' check (tier in ('free', 'analyst', 'desk')),
scan_count integer not null default 0,
scan_reset_date date not null default current_date,
subscription_start timestamptz,
subscription_end timestamptz,
subscription_status text not null default 'none' check (subscription_status in ('none','active','grace_period','expired','canceled')),
cancel_at_period_end boolean not null default false,
founder_pricing boolean not null default false,
age_verified boolean not null default false,
nexapay_customer_id text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists idx_user_profiles_tier on public.user_profiles(tier);
create index if not exists idx_user_profiles_subscription_end on public.user_profiles(subscription_end);
-- Auto-create profile on auth.users insert
create or replace function public.handle_new_user()
returns trigger as $$
begin
insert into public.user_profiles (id, email)
values (new.id, new.email)
on conflict (id) do nothing;
return new;
end;
$$ language plpgsql security definer set search_path = public;
drop trigger if exists on_auth_user_created on auth.users;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();
-- updated_at maintenance
create or replace function public.touch_updated_at()
returns trigger as $$
begin
new.updated_at := now();
return new;
end;
$$ language plpgsql;
drop trigger if exists set_user_profiles_updated_at on public.user_profiles;
create trigger set_user_profiles_updated_at
before update on public.user_profiles
for each row execute function public.touch_updated_at();
-- RLS
alter table public.user_profiles enable row level security;
drop policy if exists "user can read own profile" on public.user_profiles;
create policy "user can read own profile"
on public.user_profiles for select
using (auth.uid() = id);
drop policy if exists "user can update own profile" on public.user_profiles;
create policy "user can update own profile"
on public.user_profiles for update
using (auth.uid() = id)
with check (auth.uid() = id);
-- service role bypasses RLS automatically; no explicit policy needed.
-- ────────────────────────────────────────────────────────────
-- parlay_leg_frequency — track scan + parlay popularity per day
-- ────────────────────────────────────────────────────────────
create table if not exists public.parlay_leg_frequency (
id uuid primary key default gen_random_uuid(),
player_name text not null,
stat text not null,
line numeric not null,
over_under text not null check (over_under in ('over','under')),
sport text not null check (sport in ('NBA','MLB','WNBA','NFL')),
game_date date not null default current_date,
scan_count integer not null default 0,
parlay_count integer not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (player_name, stat, line, over_under, sport, game_date)
);
create index if not exists idx_plf_game_date_sport on public.parlay_leg_frequency(game_date, sport);
create index if not exists idx_plf_parlay_rank on public.parlay_leg_frequency(game_date, parlay_count desc);
create index if not exists idx_plf_scan_rank on public.parlay_leg_frequency(game_date, scan_count desc);
drop trigger if exists set_plf_updated_at on public.parlay_leg_frequency;
create trigger set_plf_updated_at
before update on public.parlay_leg_frequency
for each row execute function public.touch_updated_at();
-- Public read; writes restricted to service role
alter table public.parlay_leg_frequency enable row level security;
drop policy if exists "anyone can read parlay frequency" on public.parlay_leg_frequency;
create policy "anyone can read parlay frequency"
on public.parlay_leg_frequency for select
using (true);
-- Atomic increment helper
create or replace function public.increment_parlay_leg_frequency(
p_player text,
p_stat text,
p_line numeric,
p_dir text,
p_sport text,
p_scan_delta integer default 0,
p_parlay_delta integer default 0
) returns void as $$
begin
insert into public.parlay_leg_frequency
(player_name, stat, line, over_under, sport, scan_count, parlay_count)
values
(p_player, p_stat, p_line, p_dir, p_sport, p_scan_delta, p_parlay_delta)
on conflict (player_name, stat, line, over_under, sport, game_date)
do update set
scan_count = public.parlay_leg_frequency.scan_count + p_scan_delta,
parlay_count = public.parlay_leg_frequency.parlay_count + p_parlay_delta,
updated_at = now();
end;
$$ language plpgsql security definer set search_path = public;
-- ────────────────────────────────────────────────────────────
-- scan_history — for ledger + per-user analytics
-- ────────────────────────────────────────────────────────────
create table if not exists public.scan_history (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id) on delete set null,
sport text not null,
player_name text not null,
stat text not null,
line numeric not null,
direction text not null,
grade text,
projection numeric,
confidence integer,
factors jsonb,
created_at timestamptz not null default now()
);
create index if not exists idx_scan_history_user on public.scan_history(user_id, created_at desc);
create index if not exists idx_scan_history_sport on public.scan_history(sport, created_at desc);
alter table public.scan_history enable row level security;
drop policy if exists "user reads own scans" on public.scan_history;
create policy "user reads own scans"
on public.scan_history for select
using (auth.uid() = user_id);
drop policy if exists "user inserts own scans" on public.scan_history;
create policy "user inserts own scans"
on public.scan_history for insert
with check (auth.uid() = user_id);
@@ -0,0 +1,74 @@
-- VYNDR Web — odds cache + waitlist signups
-- Idempotent. Safe to apply multiple times.
create extension if not exists "pgcrypto";
-- ────────────────────────────────────────────────────────────
-- odds_cache — TTL-backed mirror of upstream Odds API data
-- Reduces calls to the rate-limited Odds API (500 req/month free tier).
-- All Next.js route handlers must read from this table, never the
-- upstream API directly.
-- ────────────────────────────────────────────────────────────
create table if not exists public.odds_cache (
cache_key text primary key,
sport text not null,
data_type text not null,
payload jsonb not null,
fetched_at timestamptz not null default now(),
expires_at timestamptz not null default (now() + interval '5 minutes')
);
create index if not exists idx_odds_cache_sport on public.odds_cache(sport);
create index if not exists idx_odds_cache_expires on public.odds_cache(expires_at);
alter table public.odds_cache enable row level security;
-- Public read OK (cache contents are public market data anyway).
-- Writes restricted to service role (bypasses RLS automatically).
drop policy if exists "anyone can read odds cache" on public.odds_cache;
create policy "anyone can read odds cache"
on public.odds_cache for select
using (true);
-- Cleanup helper — call from a cron or trigger if rows accumulate.
create or replace function public.prune_expired_odds_cache()
returns integer as $$
declare
deleted_count integer;
begin
delete from public.odds_cache
where expires_at < now() - interval '1 hour';
get diagnostics deleted_count = row_count;
return deleted_count;
end;
$$ language plpgsql security definer set search_path = public;
-- ────────────────────────────────────────────────────────────
-- waitlist_signups — capture email for upcoming launches
-- Referenced by /api/waitlist (marketplace pre-launch lists).
-- ────────────────────────────────────────────────────────────
create table if not exists public.waitlist_signups (
id uuid primary key default gen_random_uuid(),
email text not null,
list text not null default 'general',
source text,
created_at timestamptz not null default now(),
unique (email, list)
);
create index if not exists idx_waitlist_list on public.waitlist_signups(list);
create index if not exists idx_waitlist_created on public.waitlist_signups(created_at desc);
alter table public.waitlist_signups enable row level security;
-- Inserts/reads restricted to service role only. No public policies.
-- ────────────────────────────────────────────────────────────
-- founder_pricing_seat_count — convenience view used by checkout
-- to decide if the next paid user still gets the $9.99 lock-in.
-- ────────────────────────────────────────────────────────────
create or replace view public.founder_pricing_seats as
select
count(*) filter (where founder_pricing) as taken,
100 as cap,
greatest(0, 100 - count(*) filter (where founder_pricing)) as remaining
from public.user_profiles;
@@ -0,0 +1,47 @@
-- ---------------------------------------------------------------
-- user_notifications: in-app alert center
-- Surfaces rare grades, cascade events, steam moves, morning results.
-- Read-tier RLS: a user can only ever see/update their own rows.
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.user_notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
type TEXT NOT NULL CHECK (type IN (
'rare_grade', 'cascade', 'steam', 'morning_results',
'line_movement', 'injury', 'system'
)),
title TEXT NOT NULL,
body TEXT,
link TEXT,
read BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_notif_user_unread_created
ON public.user_notifications (user_id, read, created_at DESC);
-- Trim noise: a soft cap of ~200 rows per user via periodic cleanup elsewhere.
CREATE INDEX IF NOT EXISTS idx_notif_user_created
ON public.user_notifications (user_id, created_at DESC);
ALTER TABLE public.user_notifications ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Users read own notifications" ON public.user_notifications;
CREATE POLICY "Users read own notifications"
ON public.user_notifications
FOR SELECT
USING (auth.uid() = user_id);
DROP POLICY IF EXISTS "Users update own notifications" ON public.user_notifications;
CREATE POLICY "Users update own notifications"
ON public.user_notifications
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Inserts/deletes are server-only (service role bypasses RLS).
-- We do NOT grant INSERT/DELETE to anon or authenticated; the server inserts
-- system notifications and the user only reads/marks-read via the policies above.
GRANT SELECT, UPDATE ON public.user_notifications TO authenticated;
+161
View File
@@ -0,0 +1,161 @@
-- ---------------------------------------------------------------
-- 014 — Data pipeline tables.
-- odds_cache : last-seen odds per book/prop
-- line_history : append-only line samples (drives steam detection + CLV)
-- cascade_alerts : injury / lineup / ump-ref / weather deltas
-- player_stats_cache : enricher payloads from nba_api, pybaseball, etc.
-- grade_history : every grade issued + result (drives Ledger + CLV)
-- prop_correlations : measured correlation between stat pairs
-- player_id_map : cross-source player ID mapping
--
-- RLS posture:
-- Every table here is WRITTEN by the service role only (no policy = no
-- row visible to anon/authenticated for INSERT/UPDATE/DELETE — service
-- role bypasses RLS). Public-readable surfaces (grade_history) get a
-- SELECT policy. Internal-only tables stay locked down.
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.odds_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
game_id TEXT NOT NULL,
sport TEXT NOT NULL,
player_id TEXT,
player_name TEXT,
stat_type TEXT,
book TEXT NOT NULL,
line NUMERIC,
odds_over INTEGER,
odds_under INTEGER,
source TEXT NOT NULL,
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (game_id, COALESCE(player_id, ''), COALESCE(stat_type, ''), book)
);
CREATE INDEX IF NOT EXISTS idx_odds_cache_game ON public.odds_cache (game_id, fetched_at DESC);
CREATE INDEX IF NOT EXISTS idx_odds_cache_player ON public.odds_cache (player_id, stat_type);
CREATE TABLE IF NOT EXISTS public.line_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
game_id TEXT NOT NULL,
player_id TEXT,
stat_type TEXT,
book TEXT NOT NULL,
line NUMERIC,
odds INTEGER,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_line_history_recent
ON public.line_history (game_id, recorded_at DESC);
CREATE INDEX IF NOT EXISTS idx_line_history_prop
ON public.line_history (player_id, stat_type, recorded_at DESC);
CREATE TABLE IF NOT EXISTS public.cascade_alerts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trigger_type TEXT NOT NULL CHECK (trigger_type IN (
'injury', 'lineup', 'weather', 'ref', 'umpire', 'manual'
)),
trigger_detail JSONB NOT NULL,
affected_props JSONB NOT NULL DEFAULT '[]'::jsonb,
affected_count INTEGER NOT NULL DEFAULT 0,
resolved BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_cascade_recent
ON public.cascade_alerts (created_at DESC) WHERE resolved = FALSE;
CREATE TABLE IF NOT EXISTS public.player_stats_cache (
player_id TEXT NOT NULL,
sport TEXT NOT NULL,
bucket TEXT NOT NULL DEFAULT 'default',
stats_data JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (player_id, sport, bucket)
);
CREATE TABLE IF NOT EXISTS public.grade_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_id TEXT NOT NULL,
player_name TEXT,
sport TEXT NOT NULL,
stat_type TEXT,
line NUMERIC,
direction TEXT CHECK (direction IN ('over', 'under')),
grade TEXT,
projection NUMERIC,
modeled_prob NUMERIC,
implied_prob NUMERIC,
game_date DATE,
result TEXT CHECK (result IN ('hit', 'miss', 'push', 'pending')),
actual_value NUMERIC,
graded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_grade_history_player
ON public.grade_history (player_id, game_date DESC);
CREATE INDEX IF NOT EXISTS idx_grade_history_recent
ON public.grade_history (graded_at DESC);
CREATE TABLE IF NOT EXISTS public.prop_correlations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_id TEXT NOT NULL,
stat_a TEXT NOT NULL,
stat_b TEXT NOT NULL,
correlation NUMERIC NOT NULL,
sample_size INTEGER,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (player_id, stat_a, stat_b)
);
CREATE TABLE IF NOT EXISTS public.player_id_map (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
display_name TEXT NOT NULL,
sport TEXT NOT NULL,
team TEXT,
nba_id TEXT,
espn_id TEXT,
mlbam_id TEXT,
nhl_id TEXT,
nfl_gsis_id TEXT,
ufc_id TEXT,
headshot_url TEXT,
active BOOLEAN NOT NULL DEFAULT TRUE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (sport, display_name, team)
);
CREATE INDEX IF NOT EXISTS idx_player_sport ON public.player_id_map (sport, active);
CREATE INDEX IF NOT EXISTS idx_player_name ON public.player_id_map (display_name);
-- ── RLS ────────────────────────────────────────────────────────────────────
ALTER TABLE public.odds_cache ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.line_history ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.cascade_alerts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.player_stats_cache ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.grade_history ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.prop_correlations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.player_id_map ENABLE ROW LEVEL SECURITY;
-- Public-readable surfaces. The Ledger is published; correlation data and
-- the player ID map are referenced by the UI for headshots + cross-source
-- IDs. odds_cache / line_history / cascade_alerts / player_stats_cache stay
-- locked to the service role.
DROP POLICY IF EXISTS "Anyone can read grade history" ON public.grade_history;
CREATE POLICY "Anyone can read grade history"
ON public.grade_history
FOR SELECT
USING (TRUE);
DROP POLICY IF EXISTS "Anyone can read prop correlations" ON public.prop_correlations;
CREATE POLICY "Anyone can read prop correlations"
ON public.prop_correlations
FOR SELECT
USING (TRUE);
DROP POLICY IF EXISTS "Anyone can read player id map" ON public.player_id_map;
CREATE POLICY "Anyone can read player id map"
ON public.player_id_map
FOR SELECT
USING (TRUE);
GRANT SELECT ON public.grade_history TO anon, authenticated;
GRANT SELECT ON public.prop_correlations TO anon, authenticated;
GRANT SELECT ON public.player_id_map TO anon, authenticated;
@@ -0,0 +1,73 @@
-- ---------------------------------------------------------------
-- 015 — Web push subscriptions + MFA tracking + grace period.
--
-- Three concerns bundled in one migration because they all surfaced
-- in the Phase 5 PWA / payments / security pass:
-- 1. push_subscriptions — per-user push endpoints from the browser
-- 2. user_profiles.mfa_setup_prompted — track if we've already asked
-- a paid user to enable MFA (so we only nag once)
-- 3. user_profiles.grace_period_until — Stripe payment_failed grants
-- a 48h grace before tier downgrades take effect
-- ---------------------------------------------------------------
create table if not exists public.push_subscriptions (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
endpoint text not null,
keys_p256dh text not null,
keys_auth text not null,
-- One row per (user, endpoint). A user installing the PWA on two
-- devices gets two rows. Re-subscribing on the same device upserts.
sport_preferences text[] not null default '{nba,mlb,wnba,nfl,nhl,ncaab,ncaafb}',
notify_on_resolution boolean not null default true,
notify_on_cascade boolean not null default true,
notify_on_cheatsheet boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (user_id, endpoint)
);
create index if not exists idx_push_subscriptions_user on public.push_subscriptions(user_id);
create index if not exists idx_push_subscriptions_sport on public.push_subscriptions using gin (sport_preferences);
create index if not exists idx_push_subscriptions_resolution
on public.push_subscriptions(notify_on_resolution) where notify_on_resolution = true;
drop trigger if exists set_push_subscriptions_updated_at on public.push_subscriptions;
create trigger set_push_subscriptions_updated_at
before update on public.push_subscriptions
for each row execute function public.touch_updated_at();
alter table public.push_subscriptions enable row level security;
drop policy if exists "users manage own push subscriptions" on public.push_subscriptions;
create policy "users manage own push subscriptions"
on public.push_subscriptions for all
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
-- ---------------------------------------------------------------
-- user_profiles + users additions.
--
-- Two columns get added to BOTH tables because the codebase has two
-- user-state tables in flight:
-- public.users — Express backend / Stripe webhook writes here
-- public.user_profiles — Next.js routes + AuthContext read here
-- Unifying them is out of scope; keeping the columns in sync is the
-- least-risk move.
-- ---------------------------------------------------------------
alter table public.user_profiles
add column if not exists mfa_setup_prompted boolean not null default false,
add column if not exists grace_period_until timestamptz;
alter table public.users
add column if not exists mfa_setup_prompted boolean not null default false,
add column if not exists grace_period_until timestamptz;
create index if not exists idx_user_profiles_grace_period
on public.user_profiles(grace_period_until)
where grace_period_until is not null;
create index if not exists idx_users_grace_period
on public.users(grace_period_until)
where grace_period_until is not null;
@@ -0,0 +1,145 @@
-- ---------------------------------------------------------------
-- 016 — Resolution infrastructure.
--
-- closing_lines : Pinnacle closing reference, captured at tip-off
-- (CLV vs our graded line)
-- player_id_map : ESPN ↔ MLB Stats API ↔ NBA API cross-references
-- resolution_results : append-only per-prop outcome with full audit trail
-- (DNP, corrections, closing line link, CLV)
--
-- Also retrofits grade_history for the resolution pipeline:
-- + game_id (resolution route queries by this)
-- + resolved_at (idempotency: resolved_at IS NULL = unresolved)
-- + margin (actual_value - line for grading-loop analytics)
-- + correction_* (audit trail for morning correction sweeps)
-- + player_minutes / was_starter (DNP detection inputs)
-- + result CHECK (relaxed to include 'void' for postponed games)
--
-- RLS posture: all three new tables are service-role-only (no public read).
-- The Ledger queries grade_history directly; resolution_results is an
-- internal audit table.
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.closing_lines (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
game_id text NOT NULL,
sport text NOT NULL,
player_name text NOT NULL,
player_espn_id text,
stat_type text NOT NULL,
pinnacle_line numeric,
pinnacle_over_odds integer,
pinnacle_under_odds integer,
fair_over_probability numeric,
fair_under_probability numeric,
captured_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (game_id, player_espn_id, stat_type)
);
CREATE INDEX IF NOT EXISTS idx_closing_lines_game ON public.closing_lines(game_id);
CREATE INDEX IF NOT EXISTS idx_closing_lines_sport_date ON public.closing_lines(sport, captured_at);
ALTER TABLE public.closing_lines ENABLE ROW LEVEL SECURITY;
-- ---------------------------------------------------------------
-- player_id_map: first introduced in migration 014 with a wider but
-- differently-shaped column set. CREATE TABLE IF NOT EXISTS would no-op
-- if 014 already created the table, leaving 016's columns missing —
-- which would break populate-player-ids.js and oddsPapiAdapter.js.
--
-- Strategy: create the table if absent, then ALTER ADD COLUMN IF NOT
-- EXISTS to bring 014's schema forward. The columns 014 has that 016
-- doesn't (nba_id, nhl_id, nfl_gsis_id, ufc_id, headshot_url) stay
-- intact — harmless extras.
CREATE TABLE IF NOT EXISTS public.player_id_map (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
display_name text NOT NULL,
normalized_name text NOT NULL,
espn_id text NOT NULL UNIQUE,
mlbam_id text,
nba_api_id text,
sport text NOT NULL,
team_abbr text,
active boolean NOT NULL DEFAULT true,
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Bring forward 016's columns onto whatever shape 014 left behind.
-- These are no-ops when 016 created the table from scratch.
ALTER TABLE public.player_id_map
ADD COLUMN IF NOT EXISTS normalized_name text,
ADD COLUMN IF NOT EXISTS nba_api_id text,
ADD COLUMN IF NOT EXISTS team_abbr text,
ADD COLUMN IF NOT EXISTS espn_id text,
ADD COLUMN IF NOT EXISTS mlbam_id text;
-- 014's UNIQUE(sport, display_name, team) does NOT cover espn_id alone,
-- which the new code's upsert relies on. Add it idempotently via a
-- unique index — does nothing if espn_id is already unique-enforced.
CREATE UNIQUE INDEX IF NOT EXISTS idx_player_id_espn_unique
ON public.player_id_map(espn_id) WHERE espn_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_player_id_sport ON public.player_id_map(sport);
CREATE INDEX IF NOT EXISTS idx_player_id_name ON public.player_id_map(normalized_name);
CREATE INDEX IF NOT EXISTS idx_player_id_mlbam ON public.player_id_map(mlbam_id) WHERE mlbam_id IS NOT NULL;
ALTER TABLE public.player_id_map ENABLE ROW LEVEL SECURITY;
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.resolution_results (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
grade_id uuid NOT NULL,
game_id text NOT NULL,
sport text NOT NULL,
player_espn_id text NOT NULL,
player_name text NOT NULL,
stat_type text NOT NULL,
line numeric NOT NULL,
direction text NOT NULL CHECK (direction IN ('over', 'under')),
actual_value numeric NOT NULL,
result text NOT NULL CHECK (result IN ('hit', 'miss', 'push', 'void')),
margin numeric,
player_minutes numeric,
was_starter boolean,
closing_line_id uuid REFERENCES public.closing_lines(id),
clv numeric,
resolved_at timestamptz NOT NULL DEFAULT now(),
correction_note text,
correction_original_value numeric,
correction_original_result text
);
CREATE INDEX IF NOT EXISTS idx_resolution_game ON public.resolution_results(game_id);
CREATE INDEX IF NOT EXISTS idx_resolution_sport_date ON public.resolution_results(sport, resolved_at);
CREATE INDEX IF NOT EXISTS idx_resolution_player ON public.resolution_results(player_espn_id);
CREATE INDEX IF NOT EXISTS idx_resolution_result ON public.resolution_results(result);
ALTER TABLE public.resolution_results ENABLE ROW LEVEL SECURITY;
-- ---------------------------------------------------------------
-- grade_history retrofit. Columns confirmed missing by Session 6a audit.
-- ---------------------------------------------------------------
ALTER TABLE public.grade_history
ADD COLUMN IF NOT EXISTS game_id text,
ADD COLUMN IF NOT EXISTS resolved_at timestamptz,
ADD COLUMN IF NOT EXISTS margin numeric,
ADD COLUMN IF NOT EXISTS player_minutes numeric,
ADD COLUMN IF NOT EXISTS was_starter boolean,
ADD COLUMN IF NOT EXISTS closing_line_id uuid REFERENCES public.closing_lines(id),
ADD COLUMN IF NOT EXISTS clv numeric,
ADD COLUMN IF NOT EXISTS correction_note text,
ADD COLUMN IF NOT EXISTS correction_original_value numeric,
ADD COLUMN IF NOT EXISTS correction_original_result text;
CREATE INDEX IF NOT EXISTS idx_grade_history_game ON public.grade_history(game_id);
CREATE INDEX IF NOT EXISTS idx_grade_history_unresolved
ON public.grade_history(game_id) WHERE resolved_at IS NULL;
-- Relax the result CHECK to include 'void' for postponed/canceled games.
-- Drop-and-recreate is atomic inside the migration; production data with
-- any of the legacy values (hit/miss/push/pending) revalidates cleanly.
ALTER TABLE public.grade_history DROP CONSTRAINT IF EXISTS grade_history_result_check;
ALTER TABLE public.grade_history
ADD CONSTRAINT grade_history_result_check
CHECK (result IS NULL OR result IN ('hit', 'miss', 'push', 'pending', 'void'));
@@ -0,0 +1,117 @@
-- ---------------------------------------------------------------
-- 017 — Intelligence tables (Session 6b).
--
-- historical_props : ParlayAPI bulk archive (3.7M+ rows over time)
-- line_snapshots : every SharpAPI pull stored for movement tracking
-- ref_profiles : per-referee tendencies (foul rate, pace, bias)
-- coach_profiles : per-coach system + pace preferences
-- game_ref_assignments : game-day ref crew, with derived crew impact
--
-- All five tables are service-role-only — no RLS public read policies.
-- Internal services read/write via getSupabaseServiceClient(); public
-- consumers (Ledger, Pricing) read from grade_history / resolution_results.
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.historical_props (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
sport text NOT NULL,
game_date date NOT NULL,
player_name text NOT NULL,
stat_type text NOT NULL,
line numeric NOT NULL,
closing_line numeric,
result text CHECK (result IS NULL OR result IN ('over','under','push')),
source text NOT NULL DEFAULT 'parlayapi',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_hist_props_player
ON public.historical_props (player_name, stat_type);
CREATE INDEX IF NOT EXISTS idx_hist_props_sport_date
ON public.historical_props (sport, game_date DESC);
ALTER TABLE public.historical_props ENABLE ROW LEVEL SECURITY;
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.line_snapshots (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
game_id text NOT NULL,
sport text NOT NULL,
player_name text,
player_id text,
stat_type text NOT NULL,
line numeric NOT NULL,
over_odds integer,
under_odds integer,
book text,
consensus_median numeric,
snapshot_at timestamptz NOT NULL DEFAULT now()
);
-- Partial index on (game_id, stat_type) is the access pattern for movement
-- queries; full snapshot_at index covers retention sweeps (delete > 90d).
CREATE INDEX IF NOT EXISTS idx_snapshots_game
ON public.line_snapshots (game_id, stat_type, snapshot_at);
CREATE INDEX IF NOT EXISTS idx_snapshots_time
ON public.line_snapshots (snapshot_at);
ALTER TABLE public.line_snapshots ENABLE ROW LEVEL SECURITY;
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.ref_profiles (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
ref_name text NOT NULL UNIQUE,
avg_fouls_per_game numeric,
avg_free_throws_per_game numeric,
pace_impact numeric, -- + = faster games
home_whistle_bias numeric, -- + = favors home team
games_reffed integer,
last_updated timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_ref_profiles_name ON public.ref_profiles (ref_name);
ALTER TABLE public.ref_profiles ENABLE ROW LEVEL SECURITY;
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.coach_profiles (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
coach_name text NOT NULL,
team text NOT NULL,
sport text NOT NULL,
career_avg_pace numeric,
current_team_pace numeric,
pace_delta numeric, -- career_avg - current_team
tenure_games integer NOT NULL DEFAULT 0,
primary_player text,
system_style text, -- 'half_court_iso','motion','transition','post_heavy'
without_primary_style text,
without_primary_pace_delta numeric,
last_updated timestamptz NOT NULL DEFAULT now(),
UNIQUE (coach_name, team, sport)
);
CREATE INDEX IF NOT EXISTS idx_coach_profiles_team
ON public.coach_profiles (team, sport);
ALTER TABLE public.coach_profiles ENABLE ROW LEVEL SECURITY;
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.game_ref_assignments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
game_id text NOT NULL UNIQUE,
sport text NOT NULL,
game_date date NOT NULL,
ref1_name text,
ref2_name text,
ref3_name text,
ref_crew_avg_fouls numeric, -- precomputed from ref_profiles
ref_crew_pace_impact numeric,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_game_refs_game
ON public.game_ref_assignments (game_id);
CREATE INDEX IF NOT EXISTS idx_game_refs_date
ON public.game_ref_assignments (game_date);
ALTER TABLE public.game_ref_assignments ENABLE ROW LEVEL SECURITY;
@@ -0,0 +1,75 @@
-- ---------------------------------------------------------------
-- 018 — Engine 2 + learning loop tables (Session 6c).
--
-- grade_history retrofit : Engine 2 analysis columns
-- engine1_weights : versioned factor weights for Engine 1
-- accuracy_tracking : per-grade-tier hit rates with baseline lock
--
-- All three are service-role-only. The Ledger surface reads grade_history
-- directly (it already had RLS configured in earlier migrations); the new
-- engine2_* columns inherit that policy.
-- ---------------------------------------------------------------
ALTER TABLE public.grade_history
ADD COLUMN IF NOT EXISTS engine2_grade text,
ADD COLUMN IF NOT EXISTS engine2_confidence numeric,
ADD COLUMN IF NOT EXISTS engine2_narrative text,
ADD COLUMN IF NOT EXISTS engine2_agrees boolean,
ADD COLUMN IF NOT EXISTS engine2_key_factor text,
ADD COLUMN IF NOT EXISTS engine2_trap_concern text,
ADD COLUMN IF NOT EXISTS engine2_model text,
ADD COLUMN IF NOT EXISTS engine2_latency_ms integer,
ADD COLUMN IF NOT EXISTS engine2_analyzed_at timestamptz;
CREATE INDEX IF NOT EXISTS idx_grade_history_engine2_pending
ON public.grade_history (id) WHERE engine2_analyzed_at IS NULL;
-- Session 7a additions: store Engine 1 factors (for the learning loop's
-- weight adjuster) and the quantile-based P(Over) (for the UI + Engine 2).
ALTER TABLE public.grade_history
ADD COLUMN IF NOT EXISTS factors jsonb,
ADD COLUMN IF NOT EXISTS p_over numeric;
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.engine1_weights (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
sport text NOT NULL,
stat_type text NOT NULL,
factor_name text NOT NULL,
weight numeric NOT NULL,
previous_weight numeric,
adjustment_reason text,
resolved_grade_id uuid,
version integer NOT NULL DEFAULT 1,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_weights_sport_stat
ON public.engine1_weights (sport, stat_type);
CREATE INDEX IF NOT EXISTS idx_weights_version
ON public.engine1_weights (sport, stat_type, factor_name, version DESC);
ALTER TABLE public.engine1_weights ENABLE ROW LEVEL SECURITY;
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.accuracy_tracking (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
sport text NOT NULL,
grade text NOT NULL,
period text NOT NULL DEFAULT 'all_time',
total_graded integer NOT NULL DEFAULT 0,
total_hit integer NOT NULL DEFAULT 0,
total_miss integer NOT NULL DEFAULT 0,
total_push integer NOT NULL DEFAULT 0,
total_void integer NOT NULL DEFAULT 0,
hit_rate numeric,
baseline_hit_rate numeric,
baseline_locked boolean NOT NULL DEFAULT false,
clv_avg numeric,
engine2_agree_rate numeric,
last_updated timestamptz NOT NULL DEFAULT now(),
UNIQUE (sport, grade, period)
);
ALTER TABLE public.accuracy_tracking ENABLE ROW LEVEL SECURITY;