249 lines
8.9 KiB
TypeScript
249 lines
8.9 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
||
import {
|
||
APARTADO_DEFAULT,
|
||
calcularPropuesta,
|
||
calcularPropuestas,
|
||
totalPagado,
|
||
type UnidadCotizable,
|
||
} from './cotizacion.service.js'
|
||
|
||
/**
|
||
* Fixtures de los 4 desarrollos nuevos, validados al centavo contra los Excel
|
||
* de producción (reference/cotizadores/*.md). Espejo de
|
||
* apps/cotizador/src/lib/calculations-desarrollos.test.ts.
|
||
*/
|
||
|
||
function unidad(parcial: Partial<UnidadCotizable> & Pick<UnidadCotizable, 'codigo' | 'desarrollo'>): UnidadCotizable {
|
||
return {
|
||
modelo: 'X',
|
||
fechaEntrega: null,
|
||
supEstacionamiento: 0,
|
||
supConstruccion: 0,
|
||
supExcedente: 0,
|
||
precioSinAcabados: 0,
|
||
precioConAcabados: 0,
|
||
status: 'DISPONIBLE',
|
||
...parcial,
|
||
}
|
||
}
|
||
|
||
const DESC_CERO = { creditoBancario: 0, creditoInfonavit: 0, contado: 0, semiContado: 0 }
|
||
|
||
describe('ATELIER — depto 10 (L = 3,997,500, apartado 20,000, N fijo = 1)', () => {
|
||
const depto10 = unidad({
|
||
codigo: 'ATELIER-10',
|
||
desarrollo: 'ATELIER',
|
||
fechaEntrega: '2026-01-21',
|
||
precioSinAcabados: 3997500,
|
||
precioConAcabados: 3997500,
|
||
})
|
||
|
||
it('BANCARIO: enganche 5% sobre LISTA, mensualidad remanente en 1 pago', () => {
|
||
const [p] = calcularPropuestas({
|
||
unidad: depto10,
|
||
acabados: false,
|
||
apartado: APARTADO_DEFAULT,
|
||
descuentos: DESC_CERO,
|
||
})
|
||
expect(p?.enganche).toBeCloseTo(179875, 2)
|
||
expect(p?.mensualidadesTotal).toBeCloseTo(199875, 2)
|
||
expect(p?.pagoFinal).toBeCloseTo(3597750, 2)
|
||
})
|
||
|
||
it('COFINAVIT (vía tipo COFINAVIT del API): 2.5% / 95% sobre precio final', () => {
|
||
const p = calcularPropuesta(depto10, 'COFINAVIT', false, APARTADO_DEFAULT)
|
||
expect(p.titulo).toBe('Crédito Cofinavit')
|
||
expect(p.enganche).toBeCloseTo(79937.5, 2)
|
||
expect(p.mensualidadesTotal).toBeCloseTo(99937.5, 2)
|
||
expect(p.pagoFinal).toBeCloseTo(3797625, 2)
|
||
expect(totalPagado(p, APARTADO_DEFAULT)).toBeCloseTo(3997500, 2)
|
||
})
|
||
})
|
||
|
||
describe('ESTANZA — casa 52 (L = 2,437,250): pago final sobre LISTA (quirk)', () => {
|
||
const casa52 = unidad({
|
||
codigo: 'ESTANZA-52',
|
||
desarrollo: 'ESTANZA',
|
||
precioSinAcabados: 2437250,
|
||
precioConAcabados: 2437250,
|
||
})
|
||
|
||
it('las 4 propuestas al centavo', () => {
|
||
const [bancario, infonavit, contado, semi] = calcularPropuestas({
|
||
unidad: casa52,
|
||
acabados: false,
|
||
apartado: APARTADO_DEFAULT,
|
||
descuentos: DESC_CERO,
|
||
})
|
||
expect(bancario?.enganche).toBeCloseTo(40931.25, 2)
|
||
expect(bancario?.mensualidadesTotal).toBeCloseTo(182793.75, 2)
|
||
expect(bancario?.pagoFinal).toBeCloseTo(2193525, 2)
|
||
expect(infonavit?.enganche).toBeCloseTo(40931.25, 2)
|
||
expect(infonavit?.mensualidadesTotal).toBeCloseTo(60931.25, 2)
|
||
expect(infonavit?.pagoFinal).toBeCloseTo(2315387.5, 2)
|
||
expect(contado?.enganche).toBeCloseTo(2173525, 2)
|
||
expect(contado?.pagoFinal).toBeCloseTo(243725, 2)
|
||
expect(semi?.enganche).toBeCloseTo(1198625, 2)
|
||
expect(semi?.mensualidadesTotal).toBeCloseTo(974900, 2)
|
||
expect(semi?.pagoFinal).toBeCloseTo(243725, 2)
|
||
})
|
||
})
|
||
|
||
describe('SENDA — 411-1-7 (L = 2,451,750, apartado 0, N = 4)', () => {
|
||
const casa = unidad({
|
||
codigo: 'SENDA-411-1-7',
|
||
desarrollo: 'SENDA',
|
||
fechaEntrega: '2026-12-19', // la N real se fuerza con numMeses en el test
|
||
precioSinAcabados: 2451750,
|
||
precioConAcabados: 2451750,
|
||
})
|
||
|
||
it('semi-contado 50/45/5 con desc 1% default', () => {
|
||
const p = calcularPropuesta(casa, 'SEMI_CONTADO', false, 0)
|
||
// N real depende de hoy; validamos montos que no dependen de N
|
||
expect(p.precioFinal).toBeCloseTo(2427232.5, 2)
|
||
expect(p.enganche).toBeCloseTo(1213616.25, 2)
|
||
expect(p.mensualidadesTotal).toBeCloseTo(1092254.625, 3)
|
||
expect(p.pagoFinal).toBeCloseTo(121361.625, 3)
|
||
})
|
||
|
||
it('bancario 5/5/90 con N forzada = 4', () => {
|
||
const [p] = calcularPropuestas({
|
||
unidad: casa,
|
||
acabados: false,
|
||
apartado: 0,
|
||
descuentos: DESC_CERO,
|
||
numMeses: 4,
|
||
})
|
||
expect(p?.enganche).toBeCloseTo(122587.5, 2)
|
||
expect(p?.mensualidad).toBeCloseTo(30646.875, 3)
|
||
expect(p?.pagoFinal).toBeCloseTo(2206575, 2)
|
||
})
|
||
})
|
||
|
||
describe('SENDA — unidad sin fecha de entrega', () => {
|
||
it('usa el plazo por defecto del desarrollo: mensualidad y total SÍ se calculan', () => {
|
||
const sinFecha = unidad({
|
||
codigo: 'SENDA-411-25-A101',
|
||
desarrollo: 'SENDA',
|
||
fechaEntrega: null,
|
||
precioSinAcabados: 1948000,
|
||
precioConAcabados: 1948000,
|
||
})
|
||
const p = calcularPropuesta(sinFecha, 'BANCARIO', false, APARTADO_DEFAULT)
|
||
// Antes salía numMensualidades/mensualidad null y "— (sin fecha de entrega)"
|
||
expect(p.numMensualidades).toBe(6) // nDefault de SENDA
|
||
expect(p.notaMensualidades).toBeUndefined()
|
||
expect(p.mensualidadesTotal).toBeCloseTo(97400, 2)
|
||
expect(p.mensualidad).toBeCloseTo(97400 / 6, 4)
|
||
expect(totalPagado(p, APARTADO_DEFAULT)).toBeCloseTo(1948000, 2)
|
||
expect(p.enganche).toBeCloseTo(0.05 * 1948000 - APARTADO_DEFAULT, 2)
|
||
expect(p.pagoFinal).toBeCloseTo(0.9 * 1948000, 2)
|
||
})
|
||
})
|
||
|
||
describe('SOLANA — casa 6 (L = 2,560,000, mesesParaFirma = 1)', () => {
|
||
const casa6 = unidad({
|
||
codigo: 'SOLANA-6',
|
||
desarrollo: 'SOLANA',
|
||
precioSinAcabados: 2560000,
|
||
precioConAcabados: 2560000,
|
||
mesesParaFirma: 1,
|
||
})
|
||
|
||
it('INFONAVIT con enganche 0%: el apartado se descuenta (no se cobra encima del precio)', () => {
|
||
const p = calcularPropuesta(casa6, 'INFONAVIT', false, APARTADO_DEFAULT)
|
||
expect(p.enganche).toBe(0)
|
||
expect(p.numMensualidades).toBe(1)
|
||
expect(p.mensualidadesTotal).toBeCloseTo(108000, 2)
|
||
expect(p.pagoFinal).toBeCloseTo(2432000, 2)
|
||
expect(totalPagado(p, APARTADO_DEFAULT)).toBeCloseTo(2560000, 2)
|
||
})
|
||
|
||
it('BANCARIO 5% sobre final', () => {
|
||
const p = calcularPropuesta(casa6, 'BANCARIO', false, APARTADO_DEFAULT)
|
||
expect(p.enganche).toBeCloseTo(108000, 2)
|
||
expect(p.mensualidadesTotal).toBeCloseTo(128000, 2)
|
||
expect(p.pagoFinal).toBeCloseTo(2304000, 2)
|
||
})
|
||
})
|
||
|
||
describe('cuadre del total: apartado y enganche personalizado', () => {
|
||
const TIPOS = ['BANCARIO', 'INFONAVIT', 'CONTADO', 'SEMI_CONTADO'] as const
|
||
const CASOS = [
|
||
{ desarrollo: 'DIAGONAL', precio: 4_415_000 },
|
||
{ desarrollo: 'ATELIER', precio: 3_997_500 },
|
||
{ desarrollo: 'ESTANZA', precio: 2_437_250 },
|
||
{ desarrollo: 'SENDA', precio: 2_451_750 },
|
||
{ desarrollo: 'SOLANA', precio: 2_560_000 },
|
||
]
|
||
const unidadDe = (desarrollo: string, precio: number) =>
|
||
unidad({
|
||
codigo: `${desarrollo}-1`,
|
||
desarrollo,
|
||
precioSinAcabados: precio,
|
||
precioConAcabados: precio,
|
||
})
|
||
|
||
it('las 4 propuestas de cada desarrollo cuadran con su precio final', () => {
|
||
for (const caso of CASOS) {
|
||
for (const tipo of TIPOS) {
|
||
const p = calcularPropuesta(unidadDe(caso.desarrollo, caso.precio), tipo, false, APARTADO_DEFAULT)
|
||
expect(totalPagado(p, APARTADO_DEFAULT)).toBeCloseTo(p.precioFinal, 2)
|
||
}
|
||
}
|
||
})
|
||
|
||
it('nunca hay bloques negativos y el total es el precio (apartados y enganches extremos)', () => {
|
||
for (const caso of CASOS) {
|
||
for (const tipo of TIPOS) {
|
||
for (const apartado of [0, APARTADO_DEFAULT, 200_000]) {
|
||
for (const enganchePct of [undefined, 0.05, 0.2, 0.5, 1]) {
|
||
const p = calcularPropuesta(
|
||
unidadDe(caso.desarrollo, caso.precio),
|
||
tipo,
|
||
false,
|
||
apartado,
|
||
undefined,
|
||
enganchePct,
|
||
)
|
||
expect(p.enganche).toBeGreaterThanOrEqual(0)
|
||
expect(p.mensualidadesTotal ?? 0).toBeGreaterThanOrEqual(0)
|
||
expect(p.pagoFinal).toBeGreaterThanOrEqual(0)
|
||
expect(totalPagado(p, apartado)).toBeCloseTo(p.precioFinal, 2)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
it('enganche personalizado alto (20%) no infla el total: sigue siendo el precio', () => {
|
||
const u101 = unidad({
|
||
codigo: 'U-101',
|
||
desarrollo: 'DIAGONAL',
|
||
precioSinAcabados: 4_415_000,
|
||
precioConAcabados: 4_579_000,
|
||
})
|
||
const p = calcularPropuesta(u101, 'BANCARIO', false, APARTADO_DEFAULT, undefined, 0.2)
|
||
// Antes: 20% de enganche + 90% de pago final daban $4,856,500 sobre $4,415,000
|
||
expect(totalPagado(p, APARTADO_DEFAULT)).toBeCloseTo(4_415_000, 2)
|
||
expect(p.mensualidadesTotal).toBe(0)
|
||
expect(p.pagoFinal).toBeCloseTo(4_415_000 - APARTADO_DEFAULT - p.enganche, 2)
|
||
})
|
||
|
||
it('apartado mayor al enganche: el enganche queda en 0 (nunca negativo) y el total cuadra', () => {
|
||
const u101 = unidad({
|
||
codigo: 'U-101',
|
||
desarrollo: 'DIAGONAL',
|
||
precioSinAcabados: 4_415_000,
|
||
precioConAcabados: 4_579_000,
|
||
})
|
||
// 2.5% de 4,415,000 = 110,375; con apartado 200,000 el neto sería −89,625
|
||
const p = calcularPropuesta(u101, 'BANCARIO', false, 200_000)
|
||
expect(p.enganche).toBe(0)
|
||
// El apartado de más se acredita en las mensualidades (remanente real)
|
||
expect(p.mensualidadesTotal).toBeCloseTo(4_415_000 - 200_000 - p.pagoFinal, 2)
|
||
expect(totalPagado(p, 200_000)).toBeCloseTo(4_415_000, 2)
|
||
})
|
||
})
|