import { validarPropuesta, normalizar, hayBloqueantes, type Aviso } from "@/lib/propuesta/validacion"; import type { PropuestaConsultiva } from "@/lib/propuesta/schemas"; import type { DatosEconomicos } from "@/lib/propuesta/economia"; let fallas = 0; function check(n: string, ok: boolean, d = "") { console.log(`${ok ? " OK " : " FALLA"} | ${n}${d ? " -> " + d : ""}`); if (!ok) fallas++; } const tiene = (a: Aviso[], regla: string) => a.some((x) => x.regla === regla); const CITA = "no llevamos control de quien contesta el whatsapp el fin de semana"; const econ: DatosEconomicos = { partidas: [{ refPartida: "P01", servicioCotizadoId: "c1", nombre: "Sitio web", fase: 1, tipoPago: "unico", precio: 12000, tiempoEntrega: "2 semanas", modeloCobro: "fijo", horas: null, tarifaHora: null, entregables: [] }], totales: { subtotalUnico: 12000, subtotalMensual: 0, ivaUnico: 1920, ivaMensual: 0, totalUnico: 13920, totalMensual: 0, totalPrimerAnio: 13920, incluyeIva: true }, moneda: "MXN", }; const base = (): PropuestaConsultiva => ({ hechos: { citas: [{ id: "C01", textoLiteral: CITA, quienLoDijo: "el socio" }], hechos: [{ id: "H01", enunciado: "No hay control de la atencion", confianza: "confirmado", citas: ["C01"] }], materialesPendientes: [{ texto: "Accesos al dominio", bloqueaEntrega: true }], decisionesPendientes: [{ texto: "Definir responsable de atencion", quienDecide: "el socio" }], mencionesFueraDeAlcance: [], redFlags: [], }, diagnostico: { hallazgos: [{ id: "D01", urgencia: "rojo", titulo: "Prospectos sin seguimiento", descripcion: "Los mensajes del fin de semana se pierden.", confianza: "confirmado", citas: ["C01"], hechos: ["H01"] }], valorProblema: { dimensiones: [{ tipo: "tiempo", descripcion: "Horas perdidas de atencion", calculo: { factores: [{ nombre: "horas por semana", valor: 8 }, { nombre: "semanas", valor: 52 }, { nombre: "costo por hora", valor: 400 }], montoAnualMXN: 166400 }, confianza: "estimado", hechos: ["H01"] }], notaMetodologia: "Ocho horas por semana a costo cargado.", }, resultados: [{ enunciado: "Ningun mensaje sin respuesta en mas de 24 horas", metrica: "tiempo de respuesta", lineaBase: null, periodoMedicion: "mensual" }], }, redaccion: { hero: { titulo: "Propuesta de digitalizacion", subtitulo: "Ordenar la atencion para dejar de perder prospectos." }, citaDestacadaId: "C01", alcance: [{ refPartida: "P01", descripcionResultado: "Un lugar a donde mandar a los prospectos que llegan de los anuncios." }], beneficios: [{ etiqueta: "Respuesta", texto: "Cada mensaje queda registrado y con responsable.", hallazgoId: "D01" }], exclusiones: [{ texto: "No incluye migrar el historico de pedidos de los ultimos 4 anos.", razon: "Se evalua en fase 2." }], backlogEvolucion: [], notasInternas: ["El socio es quien decide."], }, }); const fuentes = { textoCliente: `El socio dijo: ${CITA}. Nada mas.`, textoInterno: "" }; check("normalizar quita acentos y puntuacion", normalizar("¿Como estas, Juan?") === "como estas juan"); check("normalizar colapsa espacios", normalizar(" a b ") === "a b"); const limpia = validarPropuesta(base(), econ, fuentes); check("una propuesta valida no produce bloqueantes", !hayBloqueantes(limpia), JSON.stringify(limpia.slice(0, 2))); let p = base(); p.redaccion.alcance[0].refPartida = "P99"; check("R2 detecta una partida inventada", tiene(validarPropuesta(p, econ, fuentes), "R2")); p = base(); p.redaccion.alcance = []; check("R2b avisa de una partida cotizada y no descrita", tiene(validarPropuesta(p, econ, fuentes), "R2b")); p = base(); p.redaccion.citaDestacadaId = "C09"; check("R3 detecta una cita inexistente", tiene(validarPropuesta(p, econ, fuentes), "R3")); p = base(); p.hechos.citas[0].textoLiteral = "esto nunca lo dijo nadie en la reunion"; check("R3 detecta una cita no verificable", tiene(validarPropuesta(p, econ, fuentes), "R3")); p = base(); p.hechos.citas[0].textoLiteral = "NO LLEVAMOS CONTROL, de quien contesta... el WhatsApp el fin de semana!"; check("R3 tolera acentos, mayusculas y puntuacion", !tiene(validarPropuesta(p, econ, fuentes), "R3")); p = base(); p.diagnostico.hallazgos[0].citas = []; p.diagnostico.hallazgos[0].hechos = []; check("R4 degrada un confirmado sin evidencia", tiene(validarPropuesta(p, econ, fuentes), "R4")); p = base(); p.diagnostico.valorProblema.dimensiones[0].calculo.montoAnualMXN = 999999; check("R5 detecta aritmetica que no cuadra", tiene(validarPropuesta(p, econ, fuentes), "R5")); p = base(); p.diagnostico.valorProblema.dimensiones[0].calculo.montoAnualMXN = null; check("R5 no se queja si no hay cifra", !tiene(validarPropuesta(p, econ, fuentes), "R5")); p = base(); p.redaccion.beneficios[0].hallazgoId = "D99"; check("R6 detecta un beneficio colgante", tiene(validarPropuesta(p, econ, fuentes), "R6")); p = base(); p.redaccion.beneficios[0].texto = "Lo integramos con HubSpot sin problema alguno."; check("MARCA detecta otro CRM", tiene(validarPropuesta(p, econ, fuentes), "MARCA")); p = base(); p.redaccion.hero.subtitulo = "Una inversion de 1,800 USD al mes para tu negocio."; check("MONEDA detecta USD", tiene(validarPropuesta(p, econ, fuentes), "MONEDA")); p = base(); p.redaccion.beneficios[0].texto = "Te garantizamos la posicion numero uno en Google."; check("GARANTIA detecta la promesa", tiene(validarPropuesta(p, econ, fuentes), "GARANTIA")); p = base(); p.redaccion.hero.subtitulo = "El sueldo de quien atiende sale muy caro hoy en dia."; check("PII advierte sobre tema salarial", tiene(validarPropuesta(p, econ, fuentes), "PII")); check("PII es advertencia, no bloqueante", validarPropuesta(p, econ, fuentes).filter((a) => a.regla === "PII").every((a) => a.severidad === "advertencia")); // ── R0: la fuga de notas internas, que es la razon de que exista ── const interno = "el socio pidio descuento antes de entender el alcance del proyecto completo"; p = base(); p.redaccion.hero.subtitulo = `Contexto del proyecto: ${interno}.`; check("R0 detecta la fuga de notas internas", tiene(validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }), "R0")); check("R0 es bloqueante", validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }).filter((a) => a.regla === "R0").every((a) => a.severidad === "bloqueante")); check("R0 no dispara si no hay fuga", !tiene(validarPropuesta(base(), econ, { ...fuentes, textoInterno: interno }), "R0")); check("R0 sin notas internas no hace nada", !tiene(validarPropuesta(base(), econ, fuentes), "R0")); // La fuga tambien se detecta en un hallazgo, no solo en el hero. p = base(); p.diagnostico.hallazgos[0].descripcion = `Observado: ${interno}.`; check("R0 mira todos los campos visibles, no solo el hero", tiene(validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }), "R0")); // Las notasInternas del propio objeto NO deben disparar R0: no se imprimen. p = base(); p.redaccion.notasInternas = [interno]; check("R0 no se queja de notasInternas (no se imprimen)", !tiene(validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }), "R0")); console.log(fallas === 0 ? "\ntodo paso\n" : `\n${fallas} fallas\n`); process.exit(fallas === 0 ? 0 : 1);