-- Agency portfolio: integrations + alerts per tenant -- Idempotent. Run as postgres / service role. create table if not exists public.integrations ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, provider text not null check (provider in ( 'meta_ads', 'google_ads', 'ga4', 'search_console', 'tag_manager', 'clientify', 'klaviyo', 'mailchimp' )), status text not null default 'disconnected' check (status in ('disconnected', 'pending', 'connected', 'error')), external_account_id text, label text, last_sync_at timestamptz, error_message text, meta jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, provider) ); create index if not exists integrations_tenant_idx on public.integrations (tenant_id, provider); alter table public.integrations enable row level security; drop policy if exists integrations_tenant_select on public.integrations; drop policy if exists integrations_tenant_write on public.integrations; -- Clients see only their tenant; agency uses service role for cross-tenant. create policy integrations_tenant_select on public.integrations for select using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create policy integrations_tenant_write on public.integrations for all using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ) with check ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create table if not exists public.agency_alerts ( id uuid primary key default gen_random_uuid(), tenant_id uuid references public.tenants(id) on delete cascade, severity text not null default 'info' check (severity in ('info', 'warning', 'critical')), source text not null default 'system', title text not null, body text, href text, read_at timestamptz, created_at timestamptz not null default now() ); create index if not exists agency_alerts_unread_idx on public.agency_alerts (created_at desc) where read_at is null; alter table public.agency_alerts enable row level security; drop policy if exists agency_alerts_tenant_select on public.agency_alerts; create policy agency_alerts_tenant_select on public.agency_alerts for select using ( tenant_id is null or tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); -- Seed disconnected integration slots for every existing tenant insert into public.integrations (tenant_id, provider, status, label) select t.id, p.provider, 'disconnected', p.label from public.tenants t cross join ( values ('meta_ads', 'Meta Ads'), ('google_ads', 'Google Ads (MCC)'), ('ga4', 'Google Analytics 4'), ('search_console', 'Search Console'), ('tag_manager', 'Tag Manager'), ('clientify', 'Clientify'), ('klaviyo', 'Klaviyo'), ('mailchimp', 'Mailchimp') ) as p(provider, label) on conflict (tenant_id, provider) do nothing; -- Bootstrap alert so the UI isn't empty on day one insert into public.agency_alerts (tenant_id, severity, source, title, body, href) select null, 'info', 'system', 'Portafolio agencia activo', 'Selector de cliente + fichas de integración listas. Conecta Meta/MCC/GA4 por tenant.', '/dashboard/agencia' where not exists ( select 1 from public.agency_alerts where title = 'Portafolio agencia activo' );