Fase 0: cerrar el MCP, unificar totales y separar el contexto interno del cliente
Primera fase del plugin de propuesta consultiva (docs/superpowers/specs/ 2026-07-28-propuesta-consultiva-ia-fase0-fase1-design.md). Recupera el contexto humano que hoy se captura y se descarta, y cierra los bloqueadores que el analisis previo destapo. Seguridad (lo mas urgente): - POST /mcp no tenia NINGUNA autenticacion: cero Depends() en main.py, mientras el servicio recibe dominio publico en produccion (SERVICE_FQDN_API_8000). Cualquiera en internet podia leer y escribir cotizaciones. Ahora exige require_auth, que ya existia en app/auth.py y no se estaba usando ahi. - _obtener_cotizacion hacia SELECT c.* y devolvia dict(cot) al agente, asi que cualquier columna nueva se publicaba sola. Ahora usa lista blanca espejo de CotizacionResponse, excluyendo observacionesInternas. Totales: - Nueva calcularTotalesCotizacion() en calculators.ts como fuente de verdad unica. El calculo estaba duplicado a mano en siete consumidores. - conIva() sustituye a los `* 1.16` hardcodeados de pdf-generator y excel-builder, que ignoraban IVA_RATE y el flag incluirIva. Contexto humano: - Campo nuevo Cotizacion.observacionesInternas. La migracion es PURAMENTE ADITIVA: no mueve ni una fila. El movimiento de datos no hace falta porque la unica fila de produccion con observaciones ya contiene texto dirigido al cliente, y separarlo en dos despliegues mantiene el rollback limpio. - Dos textareas visualmente inconfundibles en el formulario. - observaciones se imprime por primera vez en el PDF y el Excel; el dato ya viajaba hasta las rutas de borrador y se tiraba. - observacionesInternas solo se ve en la app. La garantia es estructural: el campo no existe en CotizacionPDFData ni en ExcelData, asi que el generador no puede filtrarlo aunque alguien lo intente. Bugs vecinos: - orderBy explicito en las cuatro rutas de export: el PDF asume las partidas agrupadas por fase y sin orderBy podia diferir del Excel del mismo envio. - El PDF de borrador leia solo el branding, no los datos bancarios, e imprimia los hardcodeados del generador. - detalleModelo local en PDF y Excel omitia la rama "demanda": esa partida salia en $0 y sin explicacion. Ahora delegan en la version canonica. - Los bonos salen de la tabla Bono; la lista hardcodeada queda de respaldo y su texto ya no coincidia con el seed. - La palomita de los bonos mide 0pt en las fuentes base de PDFKit (verificado), o sea que salia como dos espacios. Sustituida por una vineta. Ademas: zod pasa a ser dependencia declarada. Se importaba en schemas.ts y resolvia transitivamente, asi que un npm ci --omit=dev reventaba. Verificado con build, lint y 22 comprobaciones funcionales sobre PDF y Excel reales (texto del cliente presente, texto interno ausente incluso inyectandolo a la fuerza, texto largo multipagina, y totales con y sin IVA). Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0ff783fd65
commit
9023384de9
@@ -112,6 +112,69 @@ export function calcularTotalesOpcion(
|
||||
};
|
||||
}
|
||||
|
||||
// ----- Totales de una cotizacion: FUENTE DE VERDAD UNICA -----
|
||||
// Antes este calculo estaba duplicado a mano con .filter().reduce() en siete
|
||||
// consumidores (PDF, Excel, detalle, PreciosEditables, formulario, lista y dashboard),
|
||||
// con criterios que no coincidian, y el IVA estaba hardcodeado como * 1.16 en los dos
|
||||
// exportadores ignorando IVA_RATE y el flag incluirIva. Toda comparacion de totales
|
||||
// (y cualquier documento que deba coincidir con el PDF) debe pasar por aqui.
|
||||
//
|
||||
// Nota: los subtotales son SIN IVA, que es como los exportadores presentan hoy los
|
||||
// totales de una cotizacion simple ("los precios no incluyen IVA" en la nota al pie).
|
||||
export interface TotalesCotizacion {
|
||||
subtotalUnico: number;
|
||||
subtotalMensual: number;
|
||||
ivaUnico: number;
|
||||
ivaMensual: number;
|
||||
totalUnico: number;
|
||||
totalMensual: number;
|
||||
/** Desembolso real a 12 meses: unico + mensual x 12, con IVA si aplica.
|
||||
* Es la base del ratio precio/valor de la propuesta consultiva. */
|
||||
totalPrimerAnio: number;
|
||||
incluyeIva: boolean;
|
||||
}
|
||||
|
||||
export function calcularTotalesCotizacion(
|
||||
servicios: Array<{ tipoPago: string; precio: number; seleccionado?: boolean }>,
|
||||
opciones?: { incluirIva?: boolean }
|
||||
): TotalesCotizacion {
|
||||
const incluyeIva = opciones?.incluirIva !== false;
|
||||
const activos = servicios.filter((s) => s.seleccionado !== false);
|
||||
|
||||
const suma = (tipo: string) =>
|
||||
r2(activos.filter((s) => s.tipoPago === tipo).reduce((a, s) => a + (s.precio || 0), 0));
|
||||
|
||||
const subtotalUnico = suma("unico");
|
||||
const subtotalMensual = suma("mensual");
|
||||
|
||||
const ivaUnico = incluyeIva ? r2(subtotalUnico * IVA_RATE) : 0;
|
||||
const ivaMensual = incluyeIva ? r2(subtotalMensual * IVA_RATE) : 0;
|
||||
|
||||
const totalUnico = r2(subtotalUnico + ivaUnico);
|
||||
const totalMensual = r2(subtotalMensual + ivaMensual);
|
||||
|
||||
return {
|
||||
subtotalUnico,
|
||||
subtotalMensual,
|
||||
ivaUnico,
|
||||
ivaMensual,
|
||||
totalUnico,
|
||||
totalMensual,
|
||||
totalPrimerAnio: r2(totalUnico + totalMensual * 12),
|
||||
incluyeIva,
|
||||
};
|
||||
}
|
||||
|
||||
function r2(n: number): number {
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
|
||||
/** Aplica IVA a un monto respetando el flag de la cotizacion.
|
||||
* Sustituye a los `* 1.16` hardcodeados que habia en pdf-generator y excel-builder. */
|
||||
export function conIva(monto: number, incluirIva: boolean = true): number {
|
||||
return r2((monto || 0) * (incluirIva ? 1 + IVA_RATE : 1));
|
||||
}
|
||||
|
||||
export const FASES: Record<number, string> = {
|
||||
0: "FASE 0 - Auditoria / Acompanamiento",
|
||||
1: "FASE 1 - Setup e Infraestructura",
|
||||
|
||||
Reference in New Issue
Block a user