feat(cotizador): mensualidades del diferimiento = meses faltantes para la entrega estimada de la unidad (captura de obra)
This commit is contained in:
@@ -89,12 +89,13 @@ describe('getUnidadesLocal / getUnidadLocal', () => {
|
||||
expect(senda.map((u) => u.codigo)).toEqual(['U-2', 'U-3'])
|
||||
})
|
||||
|
||||
it('completa fechaEntrega desde la captura: entrega programada > habitabilidad > inicio de obra', async () => {
|
||||
it('completa fechaEntrega desde la captura: reprogramada > programada > habitabilidad > inicio de obra', async () => {
|
||||
prismaMock.unidad.findMany.mockResolvedValue([
|
||||
fila({ codigo: 'U-1', desarrollo: 'DIAGONAL' }),
|
||||
fila({ codigo: 'U-2', desarrollo: 'DIAGONAL' }),
|
||||
fila({ codigo: 'S-1', desarrollo: 'SENDA' }),
|
||||
fila({ codigo: 'S-2', desarrollo: 'SENDA' }),
|
||||
fila({ codigo: 'S-3', desarrollo: 'SENDA' }),
|
||||
])
|
||||
prismaMock.capturaDepartamento.findMany.mockResolvedValue([
|
||||
{ unidadCodigo: 'U-1', campo: 'inicio_obra_fecha', valor: '2026-01-01' },
|
||||
@@ -104,11 +105,16 @@ describe('getUnidadesLocal / getUnidadLocal', () => {
|
||||
// Valor vacío / no-fecha: no cuenta como dato
|
||||
{ unidadCodigo: 'S-1', campo: 'entrega_programada_fecha', valor: '' },
|
||||
{ unidadCodigo: 'S-2', campo: 'entrega_programada_fecha', valor: 'pendiente' },
|
||||
// Reprogramada: manda sobre la programada cuando existe
|
||||
{ unidadCodigo: 'U-1', campo: 'entrega_reprogramada_fecha', valor: '2028-02-15' },
|
||||
// Reprogramada vacía: se ignora y gana la programada
|
||||
{ unidadCodigo: 'S-3', campo: 'entrega_reprogramada_fecha', valor: '' },
|
||||
{ unidadCodigo: 'S-3', campo: 'entrega_programada_fecha', valor: '2027-07-01' },
|
||||
])
|
||||
|
||||
const todas = await getUnidadesLocal()
|
||||
|
||||
expect(todas.map((u) => u.fechaEntrega)).toEqual(['2027-11-20', null, '2027-05-05', null])
|
||||
expect(todas.map((u) => u.fechaEntrega)).toEqual(['2028-02-15', null, '2027-05-05', null, '2027-07-01'])
|
||||
expect(prismaMock.capturaDepartamento.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { campo: { in: [...CAMPOS_FECHA_ENTREGA] } } }),
|
||||
)
|
||||
|
||||
@@ -14,12 +14,15 @@ import type { UnidadCotizable } from './cotizacion.service.js'
|
||||
|
||||
/**
|
||||
* Campos de `CapturaDepartamento` que dan la fecha de entrega de la vivienda,
|
||||
* en orden de prioridad:
|
||||
* 1. `entrega_programada_fecha` — Fecha Programada Entrega Vivienda (ESCRITURACION)
|
||||
* 2. `habitabilidad` — fecha de habitabilidad (OBRA), la de DIAGONAL
|
||||
* 3. `inicio_obra_fecha` — último recurso (ESCRITURACION)
|
||||
* en orden de prioridad (el primero con fecha válida gana):
|
||||
* 1. `entrega_reprogramada_fecha` — Fecha Reprogramada Entrega (la más reciente,
|
||||
* cuando obra movió la entrega es la fecha que manda)
|
||||
* 2. `entrega_programada_fecha` — Fecha Programada Entrega Vivienda
|
||||
* 3. `habitabilidad` — fecha de habitabilidad (OBRA), la de DIAGONAL
|
||||
* 4. `inicio_obra_fecha` — último recurso (ESCRITURACION)
|
||||
*/
|
||||
export const CAMPOS_FECHA_ENTREGA = [
|
||||
'entrega_reprogramada_fecha',
|
||||
'entrega_programada_fecha',
|
||||
'habitabilidad',
|
||||
'inicio_obra_fecha',
|
||||
|
||||
@@ -11,6 +11,8 @@ import { formatoMoneda, formatoPorcentaje } from '../lib/formatters'
|
||||
interface Props {
|
||||
/** Esquema FRASA 2026 calculado (siempre presente: solo se renderiza con unidad) */
|
||||
esquema: EsquemaFrasaResultado
|
||||
/** Fecha de entrega estimada de la unidad (captura de obra); alimenta la nota del diferimiento */
|
||||
fechaEntrega?: string | null
|
||||
numMensualidades: number
|
||||
onNumMensualidades: (n: number) => void
|
||||
escrituracionDiferida: boolean
|
||||
@@ -31,6 +33,7 @@ const pasoMonto = 'mt-1 text-lg font-semibold text-[#111827] dark:text-[#E2E8F0]
|
||||
*/
|
||||
export function EsquemaPanel({
|
||||
esquema,
|
||||
fechaEntrega,
|
||||
numMensualidades,
|
||||
onNumMensualidades,
|
||||
escrituracionDiferida,
|
||||
@@ -145,7 +148,9 @@ export function EsquemaPanel({
|
||||
)}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
De {MENSUALIDADES_MIN} a {MENSUALIDADES_MAX}; cada una de {formatoMoneda(esquema.mensualidad)}
|
||||
{fechaEntrega
|
||||
? `Sugeridas por la entrega estimada (${new Date(`${fechaEntrega}T00:00:00`).toLocaleDateString('es-MX', { day: 'numeric', month: 'short', year: 'numeric' })}); cada una de ${formatoMoneda(esquema.mensualidad)}`
|
||||
: `De ${MENSUALIDADES_MIN} a ${MENSUALIDADES_MAX}; cada una de ${formatoMoneda(esquema.mensualidad)}`}
|
||||
</p>
|
||||
</div>
|
||||
<label className="flex cursor-pointer items-center gap-2 pb-2.5 text-sm text-[#374151] dark:text-[#CBD5E1]">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { act, cleanup, renderHook } from '@testing-library/react'
|
||||
import { APARTADO_ESQUEMA_DEFAULT, useCotizacion } from './useCotizacion'
|
||||
import { APARTADO_ESQUEMA_DEFAULT, mensualidadesParaUnidad, useCotizacion } from './useCotizacion'
|
||||
import { SIN_REGLAS } from '../lib/reglas'
|
||||
import { MENSUALIDADES_DEFAULT, calcularEsquemaFrasa } from '../lib/esquema-frasa'
|
||||
import { INVENTARIO_SEED } from '../data/unidades'
|
||||
@@ -27,7 +27,7 @@ afterEach(cleanup)
|
||||
|
||||
describe('useCotizacion — esquema único FRASA 2026', () => {
|
||||
it('arranca con apartado $15,000, 14 mensualidades y escrituración a la firma', () => {
|
||||
const { result } = montar()
|
||||
const { result } = renderHook(() => useCotizacion(SIN_REGLAS))
|
||||
|
||||
expect(result.current.apartado).toBe(APARTADO_ESQUEMA_DEFAULT)
|
||||
expect(result.current.apartadoEfectivo).toBe(15000)
|
||||
@@ -35,6 +35,34 @@ describe('useCotizacion — esquema único FRASA 2026', () => {
|
||||
expect(result.current.escrituracionDiferida).toBe(false)
|
||||
})
|
||||
|
||||
it('elegir unidad con fecha de entrega: las mensualidades = meses que faltan para entregarla', () => {
|
||||
const { result } = renderHook(() => useCotizacion(SIN_REGLAS))
|
||||
|
||||
act(() => {
|
||||
result.current.setUnidad(unidadDiagonal)
|
||||
})
|
||||
expect(result.current.numMensualidades).toBe(mensualidadesParaUnidad(unidadDiagonal))
|
||||
expect(result.current.numMensualidades).not.toBe(MENSUALIDADES_DEFAULT)
|
||||
})
|
||||
|
||||
it('elegir unidad SIN fecha de entrega: cae al default del esquema (14)', () => {
|
||||
const { result } = renderHook(() => useCotizacion(SIN_REGLAS))
|
||||
|
||||
act(() => {
|
||||
result.current.setUnidad({ ...unidadDiagonal, fechaEntrega: null })
|
||||
})
|
||||
expect(result.current.numMensualidades).toBe(MENSUALIDADES_DEFAULT)
|
||||
})
|
||||
|
||||
it('mensualidadesParaUnidad: acota a 1..36 y cae al default sin fecha', () => {
|
||||
const hoy = new Date('2026-09-29T12:00:00')
|
||||
const base = { ...unidadDiagonal }
|
||||
expect(mensualidadesParaUnidad({ ...base, fechaEntrega: '2026-10-15' }, hoy)).toBe(1)
|
||||
expect(mensualidadesParaUnidad({ ...base, fechaEntrega: '2035-12-31' }, hoy)).toBe(36)
|
||||
expect(mensualidadesParaUnidad({ ...base, fechaEntrega: '2027-09-29' }, hoy)).toBe(12)
|
||||
expect(mensualidadesParaUnidad(null, hoy)).toBe(MENSUALIDADES_DEFAULT)
|
||||
})
|
||||
|
||||
it('el resultado cuadra con calcularEsquemaFrasa sobre el precio de lista', () => {
|
||||
const { result } = montar()
|
||||
const { unidad, acabados } = result.current
|
||||
@@ -42,7 +70,7 @@ describe('useCotizacion — esquema único FRASA 2026', () => {
|
||||
const esperado = calcularEsquemaFrasa({
|
||||
precio: result.current.precioLista,
|
||||
apartado: 15000,
|
||||
numMensualidades: 14,
|
||||
numMensualidades: mensualidadesParaUnidad(unidadDiagonal),
|
||||
escrituracionDiferida: false,
|
||||
})
|
||||
expect(result.current.resultado).toEqual(esperado)
|
||||
|
||||
@@ -4,6 +4,8 @@ import { obtenerPrecioLista, resolverNumMensualidades } from '../lib/calculation
|
||||
import { DESARROLLO_DEFAULT } from '../data/desarrollos'
|
||||
import {
|
||||
MENSUALIDADES_DEFAULT,
|
||||
MENSUALIDADES_MAX,
|
||||
MENSUALIDADES_MIN,
|
||||
calcularEsquemaFrasa,
|
||||
type EsquemaFrasaResultado,
|
||||
} from '../lib/esquema-frasa'
|
||||
@@ -12,6 +14,23 @@ import { SIN_REGLAS, desarrolloEfectivo, type ReglasDesarrollo } from '../lib/re
|
||||
/** Apartado con el que arranca (y se reinicia) el cotizador: cantidad libre */
|
||||
export const APARTADO_ESQUEMA_DEFAULT = 15000
|
||||
|
||||
const MS_POR_MES = 30.4 * 86400000
|
||||
|
||||
/**
|
||||
* Mensualidades del diferimiento sugeridas para una unidad: los meses que
|
||||
* faltan hasta su fecha de entrega estimada (la que vive en el módulo de
|
||||
* captura: entrega programada/reprogramada de obra). Si la unidad no trae
|
||||
* fecha, cae al default del esquema (14). El vendedor puede ajustarla después.
|
||||
*/
|
||||
export function mensualidadesParaUnidad(u: Unidad | null, hoy: Date = new Date()): number {
|
||||
const fecha = u?.fechaEntrega
|
||||
if (!fecha) return MENSUALIDADES_DEFAULT
|
||||
const entrega = new Date(`${fecha}T00:00:00`).getTime()
|
||||
if (!Number.isFinite(entrega)) return MENSUALIDADES_DEFAULT
|
||||
const sugeridas = Math.max(MENSUALIDADES_MIN, Math.round((entrega - hoy.getTime()) / MS_POR_MES))
|
||||
return Math.min(MENSUALIDADES_MAX, sugeridas)
|
||||
}
|
||||
|
||||
interface UseCotizacionResult {
|
||||
desarrollo: string
|
||||
/** Cambia de desarrollo: limpia la unidad elegida */
|
||||
@@ -25,7 +44,9 @@ interface UseCotizacionResult {
|
||||
setApartado: (v: number) => void
|
||||
/** Apartado aplicado a los cálculos (acotado a >= 0; el vendedor puede apartar con cualquier monto, incluso $0) */
|
||||
apartadoEfectivo: number
|
||||
/** Número de mensualidades del diferimiento (elección del vendedor, 1..36) */
|
||||
/** Número de mensualidades del diferimiento (1..36): al elegir unidad se
|
||||
* prellena con los meses que faltan para su entrega estimada; el vendedor
|
||||
* puede ajustarla */
|
||||
numMensualidades: number
|
||||
setNumMensualidades: (n: number) => void
|
||||
/** true = la escrituración (5.5%) se difiere con el enganche; false = se paga a la firma */
|
||||
@@ -78,6 +99,14 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
||||
setAcabados(false)
|
||||
}
|
||||
|
||||
/** Elige unidad y, con ella, las mensualidades del diferimiento: los meses
|
||||
* que faltan para la entrega estimada de esa vivienda (dato del módulo de
|
||||
* captura). El vendedor puede ajustarlas después. */
|
||||
const elegirUnidad = (u: Unidad | null) => {
|
||||
setUnidad(u)
|
||||
setNumMensualidades(mensualidadesParaUnidad(u))
|
||||
}
|
||||
|
||||
const restaurar = (estado: EstadoRestaurar) => {
|
||||
setDesarrolloState(estado.desarrollo)
|
||||
setUnidad(estado.unidad)
|
||||
@@ -124,7 +153,7 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
||||
setDesarrollo,
|
||||
params,
|
||||
unidad,
|
||||
setUnidad,
|
||||
setUnidad: elegirUnidad,
|
||||
acabados,
|
||||
setAcabados,
|
||||
apartado,
|
||||
|
||||
@@ -782,6 +782,7 @@ export default function CotizadorPage() {
|
||||
<>
|
||||
<EsquemaPanel
|
||||
esquema={resultado}
|
||||
fechaEntrega={unidad?.fechaEntrega ?? null}
|
||||
numMensualidades={numMensualidades}
|
||||
onNumMensualidades={setNumMensualidades}
|
||||
escrituracionDiferida={escrituracionDiferida}
|
||||
|
||||
Reference in New Issue
Block a user