create table if not exists public.products ( 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, legacy_id text, title text not null, price_label text, link text, type text, tags jsonb not null default '[]'::jsonb, cover text, preview_iframe text, detail_images jsonb not null default '[]'::jsonb, sort_order integer not null default 0, status text not null default 'active' check (status in ('active', 'inactive', 'archived')), 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.timelines ( 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, type text, title text not null, description text, event_date date, link 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.video_explanations ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, legacy_id text, title text not null, description text, video_url text, thumbnail_url text, duration_seconds integer, knowledge_tags jsonb not null default '[]'::jsonb, is_general boolean not null default false, subject_id uuid references public.subjects(id) on delete set null, legacy_subject_id text, difficulty integer, 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_videos ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, question_id uuid references public.questions(id) on delete cascade, video_id uuid references public.video_explanations(id) on delete cascade, legacy_id text, legacy_question_id text, legacy_video_id text, video_type text not null default 'specific', sort_order integer not null default 0, 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.scoreline_schools ( 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, legacy_id text, name text not null, short_name text, type text, is_hot boolean not null default false, sort_order integer not null default 0, 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.scoreline_majors ( 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.scoreline_schools(id) on delete cascade, legacy_id text, name text not null, sort_order integer not null default 0, has_restriction boolean not null default false, restriction_desc 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.scoreline_fields ( 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, legacy_id text, field_key text not null, field_name text not null, field_type text, unit text, is_filter boolean not null default false, is_required boolean not null default false, is_visible boolean not null default true, is_trend boolean not null default false, options jsonb not null default '[]'::jsonb, placeholder text, description text, sort_order integer not null default 0, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, legacy_id), unique (tenant_id, region_id, field_key) ); create table if not exists public.scoreline_records ( 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.scoreline_schools(id) on delete set null, major_id uuid references public.scoreline_majors(id) on delete set null, legacy_id text, year integer not null, school_name text, major_name text, field_values 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.referral_tracks ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, legacy_id text, event_type text not null, ref_code text, ref_user_id uuid references public.platform_users(id) on delete set null, target_user_id uuid references public.platform_users(id) on delete set null, source text, ip_address text, user_agent 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.badges ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, legacy_id text, name text not null, description text, category text, icon_url text, level integer, unlock_type text, condition_field text, condition_operator text, condition_value numeric, condition_extra 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.user_badges ( 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, badge_id uuid references public.badges(id) on delete cascade, granted_by uuid references public.platform_users(id) on delete set null, legacy_id text, note text, granted_at timestamptz, 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.crm_webhook_queue ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, legacy_id text, record_id text, status text not null default 'pending', scheduled_at timestamptz, attempts integer not null default 0, next_attempt_at timestamptz, last_error text, last_http_code integer, lead_id text, sent_at timestamptz, 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.crm_webhook_log ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, legacy_id text, record_id text, http_code integer, outcome text, error_message text, lead_id text, request_body text, response_summary text, signed_at timestamptz, attempt integer, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, legacy_id) ); create index if not exists idx_products_tenant_region on public.products(tenant_id, region_id, sort_order); create index if not exists idx_timelines_tenant_region on public.timelines(tenant_id, region_id, event_date); create index if not exists idx_video_explanations_tenant_subject on public.video_explanations(tenant_id, subject_id); create index if not exists idx_scoreline_records_tenant_year on public.scoreline_records(tenant_id, region_id, year desc); create index if not exists idx_referral_tracks_tenant_event on public.referral_tracks(tenant_id, event_type, created_at desc); create index if not exists idx_crm_queue_tenant_status on public.crm_webhook_queue(tenant_id, status, next_attempt_at); do $$ declare table_name text; begin foreach table_name in array array[ 'products', 'timelines', 'video_explanations', 'question_videos', 'scoreline_schools', 'scoreline_majors', 'scoreline_fields', 'scoreline_records', 'referral_tracks', 'badges', 'user_badges', 'crm_webhook_queue', 'crm_webhook_log' ] 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 $$;