alter table public.video_explanations add column if not exists asset_id uuid references public.content_assets(id) on delete set null, add column if not exists access_mode text not null default 'svip', add column if not exists free_preview_seconds integer not null default 0, add column if not exists play_count integer not null default 0; do $$ begin if not exists (select 1 from pg_constraint where conname = 'video_explanations_access_mode_check') then alter table public.video_explanations add constraint video_explanations_access_mode_check check (access_mode in ('free', 'svip', 'video_quota')); end if; if not exists (select 1 from pg_constraint where conname = 'video_explanations_free_preview_check') then alter table public.video_explanations add constraint video_explanations_free_preview_check check (free_preview_seconds >= 0); end if; if not exists (select 1 from pg_constraint where conname = 'video_explanations_play_count_check') then alter table public.video_explanations add constraint video_explanations_play_count_check check (play_count >= 0); end if; end $$; create table if not exists public.video_play_quota_accounts ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, user_id uuid not null references public.platform_users(id) on delete cascade, quota_type text not null default 'video_play', total_quota integer not null default 0, used_quota integer not null default 0, expires_at timestamptz, source_type text, source_id uuid, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, user_id, quota_type, source_type, source_id) ); create table if not exists public.video_play_events ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, user_id uuid not null references public.platform_users(id) on delete cascade, video_id uuid not null references public.video_explanations(id) on delete cascade, question_id uuid references public.questions(id) on delete set null, entitlement_id uuid references public.entitlements(id) on delete set null, quota_account_id uuid references public.video_play_quota_accounts(id) on delete set null, play_token_hash text not null unique, status text not null default 'issued' check (status in ('issued', 'started', 'completed', 'expired', 'revoked')), access_mode text not null default 'svip' check (access_mode in ('free', 'svip', 'video_quota')), consumed_quota integer not null default 0 check (consumed_quota >= 0), signed_url_expires_at timestamptz, ip_address text, user_agent text, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists idx_video_explanations_asset on public.video_explanations(tenant_id, asset_id) where asset_id is not null; create index if not exists idx_video_quota_user on public.video_play_quota_accounts(tenant_id, user_id, quota_type, expires_at); create index if not exists idx_video_play_events_user on public.video_play_events(tenant_id, user_id, created_at desc); create index if not exists idx_video_play_events_video on public.video_play_events(tenant_id, video_id, created_at desc); do $$ declare table_name text; begin foreach table_name in array array['video_play_quota_accounts', 'video_play_events'] loop execute format('alter table public.%I enable row level security', table_name); execute format('drop policy if exists tenant_isolation on public.%I', table_name); execute format( 'create policy tenant_isolation on public.%I for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin())', table_name ); execute format('drop trigger if exists set_updated_at on public.%I', table_name); execute format('create trigger set_updated_at before update on public.%I for each row execute function app.touch_updated_at()', table_name); end loop; end $$;