Files
4ventures-client-portal/sql/agency_portfolio.sql
4Ventures 791976a3d8 feat: agency portfolio with per-client integrations hub
Admin home lists tenants, alerts, and connection health; header
selector enables superadmin context; Meta/GA4/GSC/GTM/CRM/email
slots live per tenant (manual status until OAuth).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 01:31:47 +02:00

117 lines
3.7 KiB
SQL

-- 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'
);