feat(desarrollos): modulo de alta de desarrollos con inventario, reglas del cotizador por desarrollo y micrositio publico
Modulo nuevo (solo admin) para dar de alta desarrollos sin tocar codigo: - CRUD de la ficha, modelos e inventario (alta manual y generador por rango) - Reglas del cotizador por desarrollo, editables y persistidas en la BD - El cotizador consume las reglas desde /desarrollos/reglas-cotizador y cae a las constantes con aviso cuando un desarrollo aun no tiene reglas guardadas - Micrositio publico en /d/:clave (sin sesion) con ficha, modelos y disponibilidad; jamas expone datos de clientes ni cotizaciones Integracion: - La landing monta el modulo en el grid (clave 'desarrollos', solo admin) y el micrositio fuera del login; el rol manager NO accede (matriz y API con 403) - La tabla de roles de Configuracion ahora lista todos los modulos - Se alinea la FK de ReglaCotizadorDesarrollo con la BD viva (Desarrollo.id) Verificado: 708 pruebas del API, 115 del cotizador y 89 de la landing en verde; 12/12 cotizaciones identicas antes y despues del cambio de reglas; prueba en vivo del API con alta, modelos, inventario, reglas, micrositio y baja (30/30).
This commit is contained in:
108
apps/cotizador/src/hooks/useCotizacion.test.tsx
Normal file
108
apps/cotizador/src/hooks/useCotizacion.test.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { act, cleanup, renderHook } from '@testing-library/react'
|
||||
import { useCotizacion } from './useCotizacion'
|
||||
import { reglasDesdeApi, SIN_REGLAS, type ReglasDesarrollo } from '../lib/reglas'
|
||||
import { DESARROLLOS } from '../data/desarrollos'
|
||||
import { INVENTARIO_SEED } from '../data/unidades'
|
||||
|
||||
/**
|
||||
* Reglas comerciales configurables en el cotizador: las propuestas desactivadas
|
||||
* por el admin dejan de ofrecerse y, si la elegida se desactiva, se avisa y se
|
||||
* cae a la primera vigente.
|
||||
*/
|
||||
|
||||
const unidadDiagonal = INVENTARIO_SEED.find((u) => u.desarrollo === 'DIAGONAL')!
|
||||
|
||||
function reglasDiagonales(activa: (tipo: string) => boolean): ReglasDesarrollo {
|
||||
return reglasDesdeApi({
|
||||
DIAGONAL: {
|
||||
propuestas: DESARROLLOS['DIAGONAL']!.propuestas.map((p) => ({ ...p, activa: activa(p.tipo) })),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function montar(reglas: ReglasDesarrollo = SIN_REGLAS) {
|
||||
const hook = renderHook(({ reglas }) => useCotizacion(reglas), {
|
||||
initialProps: { reglas: reglas as ReglasDesarrollo },
|
||||
})
|
||||
act(() => {
|
||||
hook.result.current.setUnidad(unidadDiagonal)
|
||||
})
|
||||
return hook
|
||||
}
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('useCotizacion y las reglas del admin', () => {
|
||||
it('sin reglas ofrece las 4 propuestas de siempre', () => {
|
||||
const { result } = montar()
|
||||
|
||||
expect(result.current.propuestas.map((p) => p.tipo)).toEqual([
|
||||
'CREDITO_BANCARIO',
|
||||
'CREDITO_INFONAVIT_COFINAVIT',
|
||||
'CONTADO',
|
||||
'SEMI_CONTADO',
|
||||
])
|
||||
expect(result.current.avisoPropuestaDesactivada).toBeNull()
|
||||
})
|
||||
|
||||
it('una propuesta desactivada no se ofrece ni se puede elegir', () => {
|
||||
const { result } = montar(reglasDiagonales((tipo) => tipo !== 'CONTADO'))
|
||||
|
||||
expect(result.current.propuestas.map((p) => p.tipo)).toEqual([
|
||||
'CREDITO_BANCARIO',
|
||||
'CREDITO_INFONAVIT_COFINAVIT',
|
||||
'SEMI_CONTADO',
|
||||
])
|
||||
})
|
||||
|
||||
it('si el admin desactiva la propuesta elegida, avisa y cae a la primera vigente', () => {
|
||||
const { result, rerender } = montar()
|
||||
|
||||
// El vendedor elige Contado (índice 2)
|
||||
act(() => {
|
||||
result.current.setPropuestaIndex(2)
|
||||
})
|
||||
expect(result.current.propuestas[2]?.tipo).toBe('CONTADO')
|
||||
|
||||
// El admin la desactiva (llegan reglas nuevas)
|
||||
rerender({ reglas: reglasDiagonales((tipo) => tipo !== 'CONTADO') })
|
||||
|
||||
expect(result.current.propuestas.map((p) => p.tipo)).not.toContain('CONTADO')
|
||||
expect(result.current.propuestaIndex).toBe(0)
|
||||
expect(result.current.avisoPropuestaDesactivada).toContain('Contado')
|
||||
expect(result.current.avisoPropuestaDesactivada).toContain('Crédito Bancario')
|
||||
})
|
||||
|
||||
it('los descuentos por defecto salen de las reglas configuradas', () => {
|
||||
const { result } = montar(
|
||||
reglasDesdeApi({
|
||||
DIAGONAL: {
|
||||
propuestas: DESARROLLOS['DIAGONAL']!.propuestas.map((p) =>
|
||||
p.tipo === 'CONTADO' ? { ...p, descuentoDefault: 0.06 } : p,
|
||||
),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.current.descuentos.contado).toBe(0.06)
|
||||
expect(result.current.descuentos.semiContado).toBe(0.01)
|
||||
})
|
||||
|
||||
it('las reglas cambian los números de las propuestas', () => {
|
||||
const { result } = montar(
|
||||
reglasDesdeApi({
|
||||
DIAGONAL: {
|
||||
propuestas: DESARROLLOS['DIAGONAL']!.propuestas.map((p) =>
|
||||
p.tipo === 'CREDITO_BANCARIO' ? { ...p, pctEnganche: 0.05 } : p,
|
||||
),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const bancario = result.current.propuestas.find((p) => p.tipo === 'CREDITO_BANCARIO')
|
||||
const precioLista = result.current.precioLista
|
||||
expect(bancario?.enganche).toBeCloseTo(0.05 * precioLista - result.current.apartadoEfectivo, 2)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user