import { prisma } from "@/lib/db"; import { calcularTotalesCotizacion, type TotalesCotizacion } from "@/lib/calculators"; /** * Capa de datos economicos de la propuesta consultiva. * * Existe para que el dinero viva en UN solo sitio y nunca cruce hacia el prompt. * El pipeline de IA recibe de aqui unicamente `refPartida` y `nombre`; los importes * los inyecta el renderizador. Asi el principio "la IA no toca los numeros" lo * garantiza el compilador y no una regla que alguien pueda olvidar validar. */ export interface PartidaCanonica { /** P01, P02... Es lo unico que identifica una partida ante el modelo. */ refPartida: string; /** cuid real de ServicioCotizado. NUNCA sale hacia el proveedor: no es estable * entre ediciones, porque el PUT hace deleteMany + createMany. */ servicioCotizadoId: string; nombre: string; fase: number; tipoPago: string; precio: number; tiempoEntrega: string; modeloCobro: string; horas: number | null; tarifaHora: number | null; entregables: string[]; } export interface DatosEconomicos { partidas: PartidaCanonica[]; totales: TotalesCotizacion; moneda: string; } export async function cargarEconomia(cotizacionId: string): Promise { const cot = await prisma.cotizacion.findUnique({ where: { id: cotizacionId }, include: { servicios: { include: { servicioCatalogo: true }, // Mismo orden que los exportadores: los tres documentos van en el mismo // correo y deben listar las partidas igual. orderBy: [{ fase: "asc" }, { createdAt: "asc" }], }, }, }); if (!cot) throw new Error(`Cotizacion ${cotizacionId} no encontrada`); const activos = cot.servicios.filter((s) => s.seleccionado); const partidas: PartidaCanonica[] = activos.map((s, i) => ({ refPartida: `P${String(i + 1).padStart(2, "0")}`, servicioCotizadoId: s.id, nombre: s.servicioCatalogo?.nombre || s.nombre || "Servicio", fase: s.fase, tipoPago: s.tipoPago, precio: s.precio, tiempoEntrega: s.tiempoEntrega, modeloCobro: s.modeloCobro, horas: s.horas, tarifaHora: s.tarifaHora, entregables: Array.isArray(s.entregables) ? (s.entregables as string[]) : [], })); return { partidas, totales: calcularTotalesCotizacion(activos, { incluirIva: cot.incluirIva }), moneda: cot.moneda, }; } export type LecturaRatio = "subcotizado" | "en_rango" | "alto" | "objecion_probable"; /** * Ratio precio/valor para el anexo interno. * * Base: el desembolso real del primer ano CON IVA (unico + mensual x 12). Se eligio * asi para que el numerador y el denominador tengan las mismas unidades: comparar un * pago inicial contra un valor ANUAL del problema daria una lectura optimista falsa. */ export function calcularRatio( totalPrimerAnio: number, valorAnual: number | null ): { ratio: number; lectura: LecturaRatio } | null { if (!valorAnual || valorAnual <= 0) return null; const ratio = totalPrimerAnio / valorAnual; const lectura: LecturaRatio = ratio < 0.1 ? "subcotizado" : ratio <= 0.25 ? "en_rango" : ratio <= 0.4 ? "alto" : "objecion_probable"; return { ratio: Math.round(ratio * 1000) / 1000, lectura }; }