Files
FRASA-CAS/apps/cotizador/src/lib/calculations-desarrollos.test.ts
Consult AS Technology afb454b54c feat(cotizador): multi-desarrollo — ATELIER, ESTANZA, SENDA, SOLANA + DIAGONAL
- Motor de cálculo parametrizado por desarrollo (enganches, descuentos, splits,
  fuente de N, quirk Infonavit-lista); DIAGONAL byte-idéntico
- 285 unidades (5 desarrollos) en seeds, API (?desarrollo=) y board Monday
- Selector de desarrollo en UI, propuestas por config, PDF multi-desarrollo
- Knowledge IA: catálogo por desarrollo + matriz de diferencias (10 docs)
- Tests: cotizador 53, api 99 — fixtures de los Excel validados al centavo
2026-08-22 04:31:21 +00:00

292 lines
9.9 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
calcularPropuestas,
obtenerPrecioLista,
resolverNumMensualidades,
totalPagado,
APARTADO_DEFAULT,
} from './calculations'
import { DESARROLLOS, descuentosDefault, obtenerDesarrollo } from '../data/desarrollos'
import { UNIDADES_ATELIER } from '../data/unidades-atelier'
import { UNIDADES_ESTANZA } from '../data/unidades-estanza'
import { UNIDADES_SENDA } from '../data/unidades-senda'
import { UNIDADES_SOLANA } from '../data/unidades-solana'
import type { Unidad } from '../types'
/**
* Fixtures validados al centavo contra los Excel de producción
* (ver reference/cotizadores/{ATELIER,ESTANZA,SENDA,SOLANA}.md).
* Orden de propuestas en la config: [bancario, infonavit/cofinavit, contado, semi].
*/
function porCodigo(unidades: Unidad[], codigo: string): Unidad {
const u = unidades.find((x) => x.codigo === codigo)
if (!u) throw new Error(`Fixture ${codigo} no existe en el seed`)
return u
}
describe('ATELIER — depto 10 (L = 3,997,500, apartado 20,000, N fijo = 1)', () => {
const unidad = porCodigo(UNIDADES_ATELIER, 'ATELIER-10')
const calcular = () =>
calcularPropuestas({
unidad,
acabados: false,
apartado: APARTADO_DEFAULT,
descuentos: descuentosDefault('ATELIER'),
})
it('precio único: no depende de acabados', () => {
expect(obtenerPrecioLista(unidad, true)).toBe(3997500)
expect(obtenerPrecioLista(unidad, false)).toBe(3997500)
})
it('resuelve N = 1 fijo (entrega inmediata)', () => {
expect(resolverNumMensualidades(unidad, DESARROLLOS['ATELIER'])).toBe(1)
})
it('BANCARIO: enganche 5% sobre LISTA; mensualidad = remanente en 1 pago', () => {
const [p] = calcular()
expect(p.enganche).toBeCloseTo(179875, 2)
expect(p.mensualidadesTotal).toBeCloseTo(199875, 2)
expect(p.mensualidad).toBeCloseTo(199875, 2)
expect(p.numMensualidades).toBe(1)
expect(p.pagoFinal).toBeCloseTo(3597750, 2)
})
it('COFINAVIT: estructura Infonavit sin quirk (todo sobre precio final)', () => {
const [, p] = calcular()
expect(p.titulo).toBe('Crédito Cofinavit')
expect(p.enganche).toBeCloseTo(79937.5, 2)
expect(p.mensualidadesTotal).toBeCloseTo(99937.5, 2)
expect(p.mensualidad).toBeCloseTo(99937.5, 2)
expect(p.pagoFinal).toBeCloseTo(3797625, 2)
expect(totalPagado(p, APARTADO_DEFAULT)).toBeCloseTo(3997500, 2)
})
it('CONTADO', () => {
const [, , p] = calcular()
expect(p.enganche).toBeCloseTo(3577750, 2)
expect(p.pagoFinal).toBeCloseTo(399750, 2)
expect(p.mensualidad).toBeNull()
})
it('SEMI CONTADO', () => {
const [, , , p] = calcular()
expect(p.enganche).toBeCloseTo(1978750, 2)
expect(p.mensualidadesTotal).toBeCloseTo(1599000, 2)
expect(p.pagoFinal).toBeCloseTo(399750, 2)
})
it('bancario cuadra: apartado + enganche + mensualidades + pago final = precio final', () => {
const [p] = calcular()
expect(totalPagado(p, APARTADO_DEFAULT)).toBeCloseTo(3997500, 2)
})
})
describe('ESTANZA — casa 52 (L = 2,437,250, apartado 20,000, N fijo = 1, desc 0)', () => {
const unidad = porCodigo(UNIDADES_ESTANZA, 'ESTANZA-52')
const calcular = () =>
calcularPropuestas({
unidad,
acabados: false,
apartado: APARTADO_DEFAULT,
descuentos: descuentosDefault('ESTANZA'),
})
it('BANCARIO: enganche 2.5% sobre final; pago final 90% sobre LISTA (quirk)', () => {
const [p] = calcular()
expect(p.enganche).toBeCloseTo(40931.25, 2)
expect(p.mensualidadesTotal).toBeCloseTo(182793.75, 2)
expect(p.mensualidad).toBeCloseTo(182793.75, 2)
expect(p.pagoFinal).toBeCloseTo(2193525, 2)
})
it('INFONAVIT: pago final 95% sobre LISTA (quirk)', () => {
const [, p] = calcular()
expect(p.enganche).toBeCloseTo(40931.25, 2)
expect(p.mensualidadesTotal).toBeCloseTo(60931.25, 2)
expect(p.pagoFinal).toBeCloseTo(2315387.5, 2)
})
it('CONTADO', () => {
const [, , p] = calcular()
expect(p.enganche).toBeCloseTo(2173525, 2)
expect(p.pagoFinal).toBeCloseTo(243725, 2)
})
it('SEMI CONTADO', () => {
const [, , , p] = calcular()
expect(p.enganche).toBeCloseTo(1198625, 2)
expect(p.mensualidadesTotal).toBeCloseTo(974900, 2)
expect(p.pagoFinal).toBeCloseTo(243725, 2)
})
})
describe('SENDA — 411-1-7 (L = 2,451,750, apartado 0 en el Excel, N = 4)', () => {
const unidad = porCodigo(UNIDADES_SENDA, 'SENDA-411-1-7')
const calcular = () =>
calcularPropuestas({
unidad,
acabados: false,
apartado: 0, // el Excel de SENDA traía MONTO APARTADO (C15) en 0
descuentos: descuentosDefault('SENDA'),
numMeses: 4,
})
it('BANCARIO: enganche 5%, mensualidades 5% ÷ N, pago final 90%', () => {
const [p] = calcular()
expect(p.enganche).toBeCloseTo(122587.5, 2)
expect(p.mensualidadesTotal).toBeCloseTo(122587.5, 2)
expect(p.mensualidad).toBeCloseTo(30646.875, 3)
expect(p.pagoFinal).toBeCloseTo(2206575, 2)
})
it('INFONAVIT: sin quirk de lista — mensualidades sobre precio final', () => {
const [, p] = calcular()
expect(p.enganche).toBeCloseTo(61293.75, 2)
expect(p.mensualidadesTotal).toBeCloseTo(61293.75, 2)
expect(p.mensualidad).toBeCloseTo(15323.4375, 4)
expect(p.pagoFinal).toBeCloseTo(2329162.5, 2)
})
it('CONTADO (desc default 2%)', () => {
const [, , p] = calcular()
expect(p.precioFinal).toBeCloseTo(2402715, 2)
expect(p.enganche).toBeCloseTo(2162443.5, 2)
expect(p.pagoFinal).toBeCloseTo(240271.5, 2)
})
it('SEMI CONTADO (desc default 1%, split 50/45/5)', () => {
const [, , , p] = calcular()
expect(p.precioFinal).toBeCloseTo(2427232.5, 2)
expect(p.enganche).toBeCloseTo(1213616.25, 2)
expect(p.mensualidadesTotal).toBeCloseTo(1092254.625, 3)
expect(p.mensualidad).toBeCloseTo(273063.65625, 5)
expect(p.pagoFinal).toBeCloseTo(121361.625, 3)
})
})
describe('SENDA — unidad sin fecha de entrega (411-25-A101)', () => {
const unidad = porCodigo(UNIDADES_SENDA, 'SENDA-411-25-A101')
it('N = null y mensualidades "—" con nota; enganche y pago final sí se calculan', () => {
expect(unidad.fechaEntrega).toBeNull()
const [bancario] = calcularPropuestas({
unidad,
acabados: false,
apartado: APARTADO_DEFAULT,
descuentos: descuentosDefault('SENDA'),
})
expect(bancario.numMensualidades).toBeNull()
expect(bancario.mensualidad).toBeNull()
expect(bancario.mensualidadesTotal).toBeNull()
expect(bancario.notaMensualidades).toBe('sin fecha de entrega')
expect(bancario.enganche).toBeCloseTo(0.05 * 1948000 - APARTADO_DEFAULT, 2)
expect(bancario.pagoFinal).toBeCloseTo(0.9 * 1948000, 2)
expect(totalPagado(bancario, APARTADO_DEFAULT)).toBeNull()
})
})
describe('SOLANA — casa 6 (L = 2,560,000, apartado 20,000, mesesParaFirma = 1)', () => {
const unidad = porCodigo(UNIDADES_SOLANA, 'SOLANA-6')
const calcular = () =>
calcularPropuestas({
unidad,
acabados: false,
apartado: APARTADO_DEFAULT,
descuentos: descuentosDefault('SOLANA'),
})
it('N viene de mesesParaFirma de la unidad', () => {
expect(unidad.mesesParaFirma).toBe(1)
expect(resolverNumMensualidades(unidad, DESARROLLOS['SOLANA'])).toBe(1)
})
it('BANCARIO: enganche 5% sobre final', () => {
const [p] = calcular()
expect(p.enganche).toBeCloseTo(108000, 2)
expect(p.mensualidadesTotal).toBeCloseTo(128000, 2)
expect(p.pagoFinal).toBeCloseTo(2304000, 2)
})
it('INFONAVIT: enganche 0% con guarda (no resta el apartado)', () => {
const [, p] = calcular()
expect(p.enganche).toBe(0)
expect(p.mensualidadesTotal).toBeCloseTo(128000, 2)
expect(p.pagoFinal).toBeCloseTo(2432000, 2)
})
it('CONTADO', () => {
const [, , p] = calcular()
expect(p.enganche).toBeCloseTo(2284000, 2)
expect(p.pagoFinal).toBeCloseTo(256000, 2)
})
it('SEMI CONTADO', () => {
const [, , , p] = calcular()
expect(p.enganche).toBeCloseTo(1260000, 2)
expect(p.mensualidadesTotal).toBeCloseTo(1024000, 2)
expect(p.pagoFinal).toBeCloseTo(256000, 2)
})
})
describe('SOLANA — casa 9 (L = 2,605,000, mesesParaFirma = 1)', () => {
const unidad = porCodigo(UNIDADES_SOLANA, 'SOLANA-9')
const calcular = () =>
calcularPropuestas({
unidad,
acabados: false,
apartado: APARTADO_DEFAULT,
descuentos: descuentosDefault('SOLANA'),
})
it('las 4 propuestas al centavo', () => {
const [bancario, infonavit, contado, semi] = calcular()
expect(bancario.enganche).toBeCloseTo(110250, 2)
expect(bancario.mensualidadesTotal).toBeCloseTo(130250, 2)
expect(bancario.pagoFinal).toBeCloseTo(2344500, 2)
expect(infonavit.enganche).toBe(0)
expect(infonavit.mensualidadesTotal).toBeCloseTo(130250, 2)
expect(infonavit.pagoFinal).toBeCloseTo(2474750, 2)
expect(contado.enganche).toBeCloseTo(2324500, 2)
expect(contado.pagoFinal).toBeCloseTo(260500, 2)
expect(semi.enganche).toBeCloseTo(1282500, 2)
expect(semi.mensualidadesTotal).toBeCloseTo(1042000, 2)
expect(semi.pagoFinal).toBeCloseTo(260500, 2)
})
})
describe('configuración de desarrollos', () => {
it('los 5 desarrollos tienen 4 propuestas', () => {
for (const id of ['DIAGONAL', 'ATELIER', 'ESTANZA', 'SENDA', 'SOLANA']) {
expect(DESARROLLOS[id].propuestas).toHaveLength(4)
}
})
it('id desconocido cae a DIAGONAL', () => {
expect(obtenerDesarrollo('NO_EXISTE').id).toBe('DIAGONAL')
expect(obtenerDesarrollo(undefined).id).toBe('DIAGONAL')
})
it('descuentos default por desarrollo', () => {
expect(descuentosDefault('DIAGONAL')).toEqual({
creditoBancario: 0,
creditoInfonavit: 0,
contado: 0.02,
semiContado: 0.01,
})
expect(descuentosDefault('SENDA')).toEqual({
creditoBancario: 0,
creditoInfonavit: 0,
contado: 0.02,
semiContado: 0.01,
})
expect(descuentosDefault('ATELIER')).toEqual({
creditoBancario: 0,
creditoInfonavit: 0,
contado: 0,
semiContado: 0,
})
})
})