Files
FRASA-CAS/api-gateway/src/services/cotizacion-desarrollos.service.test.ts

249 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})
})