- 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
292 lines
9.9 KiB
TypeScript
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,
|
|
})
|
|
})
|
|
})
|