Añade el EP único de ingesta async del lead
POST /api/leads/:id/ingesta (Bearer FUNNEL_API_KEY): acepta items foto/texto etiquetados por zona y momento, más flags perfilCompleto y finalizar. - ingesta-schema.ts: zod del cuerpo (union discriminada foto|texto), exportado para test; rechaza llamadas vacías. - route.ts: auth 401, valida lead (404), inserta fotos (orden continúa el máx) y notas, traza fotos_subidas; perfilCompleto→señalarPerfilCompleto, finalizar→finalizarYEntregar. - 10 tests del schema. Verificado por HTTP: 401/200/422/404 y finalizar genera el pdf_url y avanza el lead a whatsapp_entregado. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
74
mvp/b2c/tests/api/ingesta-schema.test.ts
Normal file
74
mvp/b2c/tests/api/ingesta-schema.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ingestaBodySchema } from '@/lib/funnel/ingesta-schema';
|
||||
|
||||
describe('ingestaBodySchema', () => {
|
||||
it('acepta una foto con zona y momento por defecto "antes"', () => {
|
||||
const r = ingestaBodySchema.safeParse({
|
||||
items: [{ tipo: 'foto', zona: 'bano', imagen: 'data:image/png;base64,AAA' }],
|
||||
});
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) {
|
||||
const foto = r.data.items[0];
|
||||
expect(foto.tipo).toBe('foto');
|
||||
if (foto.tipo === 'foto') expect(foto.momento).toBe('antes');
|
||||
}
|
||||
});
|
||||
|
||||
it('acepta momento "despues" explícito', () => {
|
||||
const r = ingestaBodySchema.safeParse({
|
||||
items: [{ tipo: 'foto', zona: 'cocina', momento: 'despues', imagen: 'https://x/y.jpg' }],
|
||||
});
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
|
||||
it('acepta una nota de texto y zona opcional', () => {
|
||||
const r = ingestaBodySchema.safeParse({ items: [{ tipo: 'texto', texto: 'suelo premium' }] });
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
|
||||
it('acepta items mixtos foto + texto', () => {
|
||||
const r = ingestaBodySchema.safeParse({
|
||||
items: [
|
||||
{ tipo: 'foto', zona: 'bano', imagen: 'data:image/png;base64,AAA' },
|
||||
{ tipo: 'texto', zona: 'bano', texto: 'suelo premium' },
|
||||
],
|
||||
});
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
|
||||
it('acepta una llamada solo con flag perfilCompleto (sin items)', () => {
|
||||
const r = ingestaBodySchema.safeParse({ perfilCompleto: true });
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('acepta una llamada solo con flag finalizar', () => {
|
||||
expect(ingestaBodySchema.safeParse({ finalizar: true }).success).toBe(true);
|
||||
});
|
||||
|
||||
it('rechaza una llamada totalmente vacía', () => {
|
||||
expect(ingestaBodySchema.safeParse({}).success).toBe(false);
|
||||
expect(ingestaBodySchema.safeParse({ items: [] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('rechaza un tipo de item inválido', () => {
|
||||
const r = ingestaBodySchema.safeParse({ items: [{ tipo: 'video', imagen: 'x' }] });
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rechaza una zona fuera del enum', () => {
|
||||
const r = ingestaBodySchema.safeParse({
|
||||
items: [{ tipo: 'foto', zona: 'jardin', imagen: 'data:...' }],
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rechaza foto sin imagen y texto vacío', () => {
|
||||
expect(ingestaBodySchema.safeParse({ items: [{ tipo: 'foto', imagen: '' }] }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(ingestaBodySchema.safeParse({ items: [{ tipo: 'texto', texto: ' ' }] }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user