-- Organic SEO (GA4/GSC) + GEO (AI visibility) warehouse -- Idempotent. create table if not exists public.organic_daily ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, day date not null, gsc_clicks numeric not null default 0, gsc_impressions numeric not null default 0, gsc_ctr numeric, gsc_avg_position numeric, ga4_sessions numeric not null default 0, ga4_users numeric not null default 0, ga4_organic_sessions numeric not null default 0, ga4_conversions numeric not null default 0, source text not null default 'manual' check (source in ('manual', 'sync', 'import')), meta jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), unique (tenant_id, day) ); create index if not exists organic_daily_tenant_day_idx on public.organic_daily (tenant_id, day desc); create table if not exists public.geo_snapshots ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, label text not null default 'Medición GEO', period_start date, period_end date, -- Share of Model: % de primeras respuestas / menciones de la marca share_of_model numeric, -- Engines: { chatgpt, gemini, ai_overviews } visibility % engines jsonb not null default '{}'::jsonb, -- Official brand site citation share among sources official_citation_pct numeric, -- Sentiment -1..1 or 0..100 (store 0..100) sentiment_score numeric, -- Competitor share of model rows: [{ brand, share, rank }] competitors jsonb not null default '[]'::jsonb, -- Intent / category visibility: [{ category, visibility, rank }] categories jsonb not null default '[]'::jsonb, -- Top sources feeding AI answers top_sources jsonb not null default '[]'::jsonb, prompts_analyzed integer, responses_analyzed integer, study_url text, notes text, created_at timestamptz not null default now() ); create index if not exists geo_snapshots_tenant_created_idx on public.geo_snapshots (tenant_id, created_at desc); alter table public.organic_daily enable row level security; alter table public.geo_snapshots enable row level security; drop policy if exists organic_daily_tenant_select on public.organic_daily; create policy organic_daily_tenant_select on public.organic_daily for select using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); drop policy if exists geo_snapshots_tenant_select on public.geo_snapshots; create policy geo_snapshots_tenant_select on public.geo_snapshots for select using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) );