alter table public.content_nodes add column if not exists access_rules jsonb not null default '{}'::jsonb; alter table public.question_collections add column if not exists access_rules jsonb not null default '{}'::jsonb; alter table public.practice_blueprints add column if not exists access_rules jsonb not null default '{}'::jsonb; alter table public.practice_sessions add column if not exists access_mode text not null default 'free', add column if not exists access_entitlement_id uuid references public.entitlements(id) on delete set null, add column if not exists consumed_free_quota integer not null default 0, add column if not exists access_snapshot jsonb not null default '{}'::jsonb; do $$ begin if not exists (select 1 from pg_constraint where conname = 'practice_sessions_access_mode_check') then alter table public.practice_sessions add constraint practice_sessions_access_mode_check check (access_mode in ('free', 'svip', 'staff')); end if; if not exists (select 1 from pg_constraint where conname = 'practice_sessions_consumed_free_quota_check') then alter table public.practice_sessions add constraint practice_sessions_consumed_free_quota_check check (consumed_free_quota >= 0); end if; end $$; create table if not exists public.practice_daily_usage ( 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, usage_date date not null default current_date, scope_type text not null default 'tenant' check (scope_type in ('tenant', 'region', 'subject', 'question_bank', 'content_entry', 'content_node', 'collection', 'blueprint')), scope_id uuid, free_limit integer not null default 25 check (free_limit >= 0), used_count integer not null default 0 check (used_count >= 0), metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists public.practice_access_events ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, user_id uuid references public.platform_users(id) on delete set null, practice_session_id uuid references public.practice_sessions(id) on delete set null, event_type text not null check (event_type in ('session_created', 'session_denied', 'quota_consumed')), access_mode text not null default 'free' check (access_mode in ('free', 'svip', 'staff', 'denied')), requested_count integer not null default 0 check (requested_count >= 0), granted_count integer not null default 0 check (granted_count >= 0), consumed_free_quota integer not null default 0 check (consumed_free_quota >= 0), reason text, scope_type text, scope_id uuid, entitlement_id uuid references public.entitlements(id) on delete set null, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); create index if not exists idx_practice_daily_usage_user on public.practice_daily_usage(tenant_id, user_id, usage_date desc); create unique index if not exists idx_practice_daily_usage_unique_scope on public.practice_daily_usage(tenant_id, user_id, usage_date, scope_type, scope_id) nulls not distinct; create index if not exists idx_practice_access_events_user on public.practice_access_events(tenant_id, user_id, created_at desc); create index if not exists idx_practice_access_events_session on public.practice_access_events(tenant_id, practice_session_id); do $$ declare table_name text; begin foreach table_name in array array['practice_daily_usage', 'practice_access_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); end loop; execute 'create trigger set_updated_at before update on public.practice_daily_usage for each row execute function app.touch_updated_at()'; end $$;