Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user