-- Creative pipeline: producción RRSS separada de `creatives` (Ads Meta). -- Flujo unificado (ver sql/approval_status_unify.sql): -- borrador → revision_agencia → pendiente_aprobacion → aprobada|rechazada → publicada -- Cliente solo ve desde pendiente_aprobacion. Idempotente. create table if not exists public.creative_pipeline ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, -- referencia externa estable usada por los scripts de generación (ej. "oct-04") external_ref text, title text not null, format text not null default 'post' check (format in ('post', 'story', 'reel', 'carrusel')), -- mes/calendario al que pertenece la pieza (agrupación en el board) month text, -- 'YYYY-MM' status text not null default 'borrador' check (status in ( 'borrador', 'revision_agencia', 'pendiente_aprobacion', 'aprobada', 'rechazada', 'publicada' )), -- portada (todas las piezas tienen una) cover_path text, cover_mime text, -- piezas carrusel: array ordenado de storage paths de los slides interiores + CTA -- ej. ["tenant/.../slide-2.jpg", "tenant/.../slide-3.jpg", ...] slide_paths jsonb not null default '[]'::jsonb, caption text, notes text, created_by uuid references auth.users(id), approved_by uuid references auth.users(id), approved_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create unique index if not exists creative_pipeline_tenant_ref_idx on public.creative_pipeline (tenant_id, external_ref) where external_ref is not null; do $$ begin if not exists ( select 1 from pg_constraint where conname = 'creative_pipeline_tenant_ref_uniq' ) then alter table public.creative_pipeline add constraint creative_pipeline_tenant_ref_uniq unique (tenant_id, external_ref); end if; end $$; create index if not exists creative_pipeline_tenant_month_idx on public.creative_pipeline (tenant_id, month, status); alter table public.creative_pipeline enable row level security; -- Lectura: cliente ve las piezas de su propio tenant (incluye revisión interna, -- ya que este panel es también para el cliente, no solo para el equipo). create policy creative_pipeline_tenant_select on public.creative_pipeline for select using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); -- Alta/edición de contenido vía API del equipo (service role) — bloqueado para -- clientes directos por RLS; el endpoint PATCH permite al cliente mover el -- estado a 'aprobado'/'rechazado' únicamente mediante el mismo filtro de tenant. create policy creative_pipeline_tenant_insert on public.creative_pipeline for insert with check ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create policy creative_pipeline_tenant_update on public.creative_pipeline for update using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create policy creative_pipeline_tenant_delete on public.creative_pipeline for delete using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); comment on table public.creative_pipeline is 'Pipeline interno de producción de creatividades (RRSS), separado de creatives (ads). Mismo bucket tenant-assets, prefijo pipeline/.'; -- Storage: reutiliza el bucket privado `tenant-assets` (ver src/lib/assets.ts), -- path = {tenant_id}/pipeline/{id}/cover.jpg y {tenant_id}/pipeline/{id}/slide-N.jpg