create table if not exists public.exam_dates ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, region_id uuid references public.regions(id) on delete set null, school_id uuid references public.schools(id) on delete set null, legacy_id text, exam_name text not null, exam_date date, exam_type text, sort_order integer not null default 0, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, legacy_id) ); create table if not exists public.question_type_groups ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, subject_id uuid references public.subjects(id) on delete cascade, legacy_id text, legacy_subject_id text, display_name text not null, types jsonb not null default '[]'::jsonb, sort_order integer not null default 0, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, legacy_id) ); create table if not exists public.subject_shares ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, source_subject_id uuid references public.subjects(id) on delete cascade, target_subject_id uuid references public.subjects(id) on delete cascade, legacy_id text, legacy_source_subject_id text, legacy_target_subject_id text, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, legacy_id) ); create table if not exists public.recent_practices ( 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 cascade, legacy_id text, practice_type text, target_legacy_id text, target_name text, progress integer not null default 0, color text, last_access_at timestamptz, last_practice_at timestamptz, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, legacy_id) ); create table if not exists public.user_word_progress ( 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 cascade, word_id uuid references public.vocabulary_words(id) on delete cascade, legacy_id text, legacy_user_id text, legacy_word_id text, status text not null default 'new' check (status in ('new', 'learning', 'mastered', 'reviewing')), correct_count integer not null default 0, wrong_count integer not null default 0, last_review_date timestamptz, next_review_date timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, legacy_id), unique (tenant_id, user_id, word_id) ); create table if not exists public.user_word_favorites ( 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 cascade, word_id uuid references public.vocabulary_words(id) on delete cascade, legacy_id text, legacy_user_id text, legacy_word_id text, note text, favorited_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, legacy_id), unique (tenant_id, user_id, word_id) ); create table if not exists public.content_assets ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, legacy_id text, asset_key text, title text, category text, description text, file_name text, cdn_url text, is_public boolean not null default false, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, legacy_id) ); create table if not exists public.dashboard_daily_stats ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, legacy_id text, stat_date date not null, region_id uuid references public.regions(id) on delete set null, legacy_region_id text, new_users integer not null default 0, new_questions integer not null default 0, new_orders integer not null default 0, new_revenue_cents integer not null default 0, active_users integer not null default 0, rebuilt_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, stat_date, legacy_region_id) ); create table if not exists public.revenue_daily_stats ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, legacy_id text, stat_date date not null, region_id uuid references public.regions(id) on delete set null, legacy_region_id text, sale_type text, real_revenue_cents integer not null default 0, order_count integer not null default 0, code_count integer not null default 0, code_used integer not null default 0, code_estimated_cents integer not null default 0, estimated_revenue_cents integer not null default 0, rebuilt_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, stat_date, legacy_region_id, sale_type) ); create index if not exists idx_exam_dates_tenant_region on public.exam_dates(tenant_id, region_id, exam_date); create index if not exists idx_question_type_groups_subject on public.question_type_groups(tenant_id, subject_id, sort_order); create index if not exists idx_recent_practices_user on public.recent_practices(tenant_id, user_id, last_practice_at desc); create index if not exists idx_user_word_progress_user on public.user_word_progress(tenant_id, user_id, status); create index if not exists idx_content_assets_tenant_key on public.content_assets(tenant_id, asset_key); create index if not exists idx_dashboard_daily_stats_date on public.dashboard_daily_stats(tenant_id, stat_date desc); create index if not exists idx_revenue_daily_stats_date on public.revenue_daily_stats(tenant_id, stat_date desc); do $$ declare table_name text; begin foreach table_name in array array[ 'exam_dates', 'question_type_groups', 'subject_shares', 'recent_practices', 'user_word_progress', 'user_word_favorites', 'content_assets', 'dashboard_daily_stats', 'revenue_daily_stats' ] 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 $$;