165 lines
7.2 KiB
PL/PgSQL
165 lines
7.2 KiB
PL/PgSQL
-- 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);
|