Files
4ventures-client-portal/sql/creative_pipeline.sql
4Ventures 46492c3873 Unify Ads/RRSS approval flow and add personalized Meta creative regen.
Agency validates first then client; Meta copy is 5+5+1 with on-image vs feed layers, emojis on feed, and partial v2 by image/hook/support/copy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 13:17:36 +02:00

105 lines
3.8 KiB
SQL

-- 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