Files
4ventures-client-portal/sql/creative_pipeline.sql

107 lines
3.9 KiB
SQL

-- Creative pipeline: producción interna de creatividades (Blue Parrot y resto
-- de clientes) separada de `creatives` (que es el repositorio de anuncios
-- Meta/Google ya aprobados). Aquí vive el flujo editorial completo:
-- Propuesta -> En diseño -> Revisión interna -> Aprobado / Rechazado -> Publicado
-- Visible también para el cliente en /dashboard/panel-cliente (RLS por tenant).
-- 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 'propuesta'
check (status in (
'propuesta', 'en_diseno', 'revision_interna',
'aprobado', 'rechazado', 'publicado'
)),
-- 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