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:
2026-09-21 09:52:47 +00:00
parent 2fc80e7f9e
commit d6548999bf
68 changed files with 10016 additions and 66 deletions

View File

@@ -48,10 +48,10 @@ export const DESCUENTOS_DEFAULT: DescuentosPropuestas = {
// ---------- Parametrización por desarrollo ----------
type BasePrecio = 'lista' | 'final'
type ModoMensualidades = 'ninguna' | 'remanente' | 'remanenteLista' | 'porcentaje'
export type BasePrecio = 'lista' | 'final'
export type ModoMensualidades = 'ninguna' | 'remanente' | 'remanenteLista' | 'porcentaje'
interface PropuestaConfig {
export interface PropuestaConfig {
tipo: TipoPropuesta
claveDescuento: keyof DescuentosPropuestas
titulo: string
@@ -62,10 +62,17 @@ interface PropuestaConfig {
pagoFinalBase: BasePrecio
modoMensualidades: ModoMensualidades
engancheCeroSinApartado?: boolean
/**
* Regla del admin: false => la propuesta no se ofrece ni se puede elegir en
* el cotizador. Ausente/true => vigente (comportamiento histórico).
*/
activa?: boolean
}
interface ParametrosDesarrollo {
export interface ParametrosDesarrollo {
id: string
/** Nombre para mostrar (solo lo traen las reglas guardadas por el admin) */
nombre?: string
precioUnico: boolean
fuenteDeN: 'fechaEntrega' | 'fijo' | 'porUnidad'
nFijo?: number
@@ -75,6 +82,15 @@ interface ParametrosDesarrollo {
* siempre se calculen (espejo de apps/cotizador/src/data/desarrollos.ts).
*/
nDefault?: number
/** Cuota mensual de mantenimiento (MXN); null => pendiente */
cuotaMantenimiento?: number | null
/** Prefijo de la unidad en el PDF ('CASA', 'DEPTO') */
prefijoUnidadPdf?: string
/**
* Regla del admin: false => el desarrollo no se ofrece en el cotizador.
* Ausente/true => disponible (comportamiento histórico).
*/
activoEnCotizador?: boolean
propuestas: PropuestaConfig[]
}
@@ -153,10 +169,23 @@ export const DESARROLLOS: Record<string, ParametrosDesarrollo> = {
const DESARROLLO_DEFAULT = 'DIAGONAL'
function obtenerDesarrollo(id?: string | null): ParametrosDesarrollo {
/**
* Reglas comerciales del desarrollo: las configuradas por el admin (BD) ganan
* cuando corresponden al id pedido; si no, se usan las constantes del módulo.
*/
function obtenerDesarrollo(
id?: string | null,
reglas?: ParametrosDesarrollo | null,
): ParametrosDesarrollo {
if (reglas && (id === undefined || id === null || id === '' || reglas.id === id)) return reglas
return (id && DESARROLLOS[id]) || DESARROLLOS[DESARROLLO_DEFAULT]!
}
/** Propuestas vigentes: el admin puede desactivar una propuesta por desarrollo */
export function propuestasVigentes(params: ParametrosDesarrollo): PropuestaConfig[] {
return params.propuestas.filter((p) => p.activa !== false)
}
export interface PropuestaResultado {
tipo: TipoPropuesta
titulo: string
@@ -177,6 +206,11 @@ export interface CotizacionInput {
apartado: number
descuentos: DescuentosPropuestas
desarrollo?: string
/**
* Reglas comerciales efectivas del desarrollo (las configuradas por el admin
* o su respaldo en constantes). Si no vienen, se usan las constantes.
*/
reglas?: ParametrosDesarrollo
/** Permite forzar N en tests; por defecto se resuelve según la fuenteDeN del desarrollo */
numMeses?: number
}
@@ -356,13 +390,16 @@ export function indiceMenorTotal(totales: number[]): number {
return indice
}
/** Calcula las propuestas comerciales del desarrollo de la unidad */
/** Calcula las propuestas comerciales vigentes del desarrollo de la unidad */
export function calcularPropuestas(input: CotizacionInput): PropuestaResultado[] {
const { unidad, acabados, apartado, descuentos } = input
const params = obtenerDesarrollo(input.desarrollo ?? unidad.desarrollo ?? DESARROLLO_DEFAULT)
const params = obtenerDesarrollo(
input.desarrollo ?? unidad.desarrollo ?? DESARROLLO_DEFAULT,
input.reglas ?? null,
)
const precioLista = obtenerPrecioLista(unidad, acabados)
const n = input.numMeses ?? resolverNumMensualidades(unidad, params)
return params.propuestas.map((cfg) =>
return propuestasVigentes(params).map((cfg) =>
calcularPropuestaConfig(cfg, precioLista, apartado, descuentos[cfg.claveDescuento], n),
)
}
@@ -386,6 +423,8 @@ export type TipoPropuestaApi = keyof typeof TIPO_PROPUESTA_API
* crédito (bancario/Infonavit); Contado y Semi Contado usan su % fijo.
* `numMeses` opcional: número de mensualidades elegido por el vendedor en el
* cotizador; si no viene se resuelve con la fuente del desarrollo.
* `reglas` opcional: reglas comerciales efectivas del desarrollo de la unidad
* (BD del admin con respaldo en constantes); sin ellas se usan las constantes.
*/
export function calcularPropuesta(
unidad: UnidadCotizable,
@@ -395,12 +434,18 @@ export function calcularPropuesta(
descuento?: number,
enganchePct?: number,
numMeses?: number,
reglas?: ParametrosDesarrollo | null,
): PropuestaResultado {
const params = obtenerDesarrollo(unidad.desarrollo)
const params = obtenerDesarrollo(unidad.desarrollo, reglas ?? null)
const tipoInterno = TIPO_PROPUESTA_API[tipo]
const cfg = params.propuestas.find((p) => p.tipo === tipoInterno)
const cfg = propuestasVigentes(params).find((p) => p.tipo === tipoInterno)
if (!cfg) {
throw new Error(`La propuesta ${tipo} no aplica para el desarrollo ${params.id}`)
const desactivada = params.propuestas.some((p) => p.tipo === tipoInterno)
throw new Error(
desactivada
? `La propuesta ${tipo} está desactivada para el desarrollo ${params.id}`
: `La propuesta ${tipo} no aplica para el desarrollo ${params.id}`,
)
}
const precioLista = obtenerPrecioLista(unidad, acabados)
const n = numMeses ?? resolverNumMensualidades(unidad, params)