fix(pipeline): editar formato Post/Story y bloquear horarios pasados

Permite cambiar el formato al editar una pieza, normaliza Stories→story
al guardar y rechaza programaciones en fecha/hora ya pasada.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-16 13:05:30 +02:00
parent c434fa5834
commit 77fd628078
6 changed files with 270 additions and 37 deletions

View File

@@ -189,7 +189,7 @@ async function syncPieceToPipeline(
const existing = find.ok ? ((await find.json()) as { id: string }[]) : [];
const row: Record<string, unknown> = {
title: piece.title,
format: mapFormatToPipeline(piece.format, piece.kind),
format: mapFormatToPipeline(piece.format, piece.kind) || 'post',
month,
caption: caption || null,
notes: piece.pillar ? `Pilar: ${piece.pillar}` : null,

View File

@@ -11,9 +11,13 @@ import {
import {
buildRrssGenerationBrief,
generationRequestNoteMarker,
isRrssPipelineFormat,
type RrssPipelineFormat,
} from '@/lib/rrss-generation';
import {
isScheduledAtInPast,
normalizePipelineFormat,
parseScheduledAt,
} from '@/lib/pipeline-schedule';
import { createClient as createAdminClient } from '@supabase/supabase-js';
export const dynamic = 'force-dynamic';
@@ -58,13 +62,23 @@ export async function POST(req: NextRequest) {
}
const title = String(body.title || '').trim();
const format = body.format as RrssPipelineFormat;
const format = normalizePipelineFormat(body.format) as RrssPipelineFormat;
if (!title) {
return NextResponse.json({ error: 'title requerido' }, { status: 400 });
}
if (!isRrssPipelineFormat(format)) {
const scheduledAt = parseScheduledAt(body.scheduled_at);
if (scheduledAt === undefined) {
return NextResponse.json(
{ error: 'format debe ser post|story|reel|carrusel' },
{ error: 'scheduled_at inválido' },
{ status: 400 }
);
}
if (scheduledAt && isScheduledAtInPast(scheduledAt)) {
return NextResponse.json(
{
error:
'No se puede programar en una fecha/hora ya pasada. Elige un horario futuro.',
},
{ status: 400 }
);
}
@@ -160,8 +174,7 @@ export async function POST(req: NextRequest) {
caption,
notes: notesBase || null,
calendar_piece_id: calendarPieceId,
scheduled_at:
typeof body.scheduled_at === 'string' ? body.scheduled_at : null,
scheduled_at: scheduledAt ?? null,
channels: Array.isArray(body.channels)
? body.channels
: ['instagram', 'facebook'],

View File

@@ -9,7 +9,9 @@ import {
import { resolveActiveTenant } from '@/lib/tenant-context';
import {
channelsForCalendarPlatform,
isScheduledAtInPast,
normalizeChannels,
normalizePipelineFormat,
parseScheduledAt,
PUBLISH_STATUSES,
} from '@/lib/pipeline-schedule';
@@ -36,8 +38,6 @@ import { dispatchN8nWebhookAsync } from '@/lib/creatives-webhooks';
const SELECT =
'id,external_ref,title,format,month,status,cover_path,cover_mime,slide_paths,caption,notes,approved_at,created_at,updated_at,scheduled_at,channels,publish_status,publish_attempts,published_at,meta_ig_media_id,meta_fb_post_id,publish_error,calendar_piece_id';
const FORMATS = new Set(['post', 'story', 'reel', 'carrusel']);
function str(v: unknown, max = 2000): string | null {
if (typeof v !== 'string') return null;
const t = v.trim();
@@ -190,7 +190,7 @@ export async function POST(req: NextRequest) {
const calendarPieceId = str(body.calendar_piece_id, 120);
let title = str(body.title, 300);
let format = str(body.format, 20);
let format = str(body.format, 40);
let month = str(body.month, 7);
let caption =
sanitizePublishCaption(str(body.caption, 4000) || '', { maxLen: 4000 })
@@ -237,11 +237,21 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'title requerido' }, { status: 400 });
}
if (scheduledAt && isScheduledAtInPast(scheduledAt)) {
return NextResponse.json(
{
error:
'No se puede programar en una fecha/hora ya pasada. Elige un horario futuro.',
},
{ status: 400 }
);
}
const row: Record<string, unknown> = {
tenant_id: ctx.tenantId,
title,
external_ref: str(body.external_ref, 80),
format: format && FORMATS.has(format) ? format : 'post',
format: normalizePipelineFormat(format),
month: month || null,
caption: caption || null,
notes: notes || null,
@@ -422,8 +432,9 @@ export async function PATCH(req: NextRequest) {
if (title) clean.title = title;
const month = str(body.month, 7);
if (month) clean.month = month;
const format = str(body.format, 20);
if (format && FORMATS.has(format)) clean.format = format;
if ('format' in body && body.format != null) {
clean.format = normalizePipelineFormat(body.format);
}
if ('caption' in body) {
clean.caption =
sanitizePublishCaption(str(body.caption, 4000) || '', {
@@ -447,6 +458,15 @@ export async function PATCH(req: NextRequest) {
{ status: 400 }
);
}
if (parsed && isScheduledAtInPast(parsed)) {
return NextResponse.json(
{
error:
'No se puede programar en una fecha/hora ya pasada. Elige un horario futuro.',
},
{ status: 400 }
);
}
clean.scheduled_at = parsed;
if (parsed) {
// Cron publica queued + status aprobada. Programar implica ya validada.