feat(cotizador): enganche adicional hasta 90% de anticipo total (UI, opciones de credito, PDF, API y persistencia)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
-- Enganche adicional (anticipo extra) del esquema FRASA 2026: el cliente puede
|
||||||
|
-- dar hasta 90% de anticipo total (esquema + adicional), quedando un mínimo
|
||||||
|
-- del 10% del precio por financiar. 0 por default (no aplica a filas viejas).
|
||||||
|
-- Patrón manual: sqlite3 + _prisma_migrations. NUNCA migrate dev.
|
||||||
|
|
||||||
|
ALTER TABLE "Cotizacion" ADD COLUMN "engancheAdicional" REAL NOT NULL DEFAULT 0;
|
||||||
@@ -41,6 +41,7 @@ model Cotizacion {
|
|||||||
pctEscrituracion Float? // 0.055
|
pctEscrituracion Float? // 0.055
|
||||||
escrituracionDiferida Boolean @default(false)
|
escrituracionDiferida Boolean @default(false)
|
||||||
escrituracion Float? // gastos de escrituración (5.5% del precio)
|
escrituracion Float? // gastos de escrituración (5.5% del precio)
|
||||||
|
engancheAdicional Float? @default(0) // anticipo extra; el total no puede pasar del 90% del precio
|
||||||
// Opciones de credito del saldo (84.5%): JSON {elegida, ajustes} serializado;
|
// Opciones de credito del saldo (84.5%): JSON {elegida, ajustes} serializado;
|
||||||
// el cotizador manda el ajuste del vendedor y se persiste crudo.
|
// el cotizador manda el ajuste del vendedor y se persiste crudo.
|
||||||
opcionesCredito String?
|
opcionesCredito String?
|
||||||
|
|||||||
@@ -516,3 +516,27 @@ describe('alertasInventarioEndpoint (POST /api/cotizaciones/alertas-inventario)
|
|||||||
expect(prismaMock.actividadLocal.create).not.toHaveBeenCalled()
|
expect(prismaMock.actividadLocal.create).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('crearCotizacion — enganche adicional', () => {
|
||||||
|
it('persiste el enganche adicional y recalcula el saldo a financiar', async () => {
|
||||||
|
const { res, json } = resFake()
|
||||||
|
|
||||||
|
await crearCotizacion(reqFake({ ...bodyBase, engancheAdicional: 400000 }), res)
|
||||||
|
|
||||||
|
expect(prismaMock.cotizacion.create).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({ engancheAdicional: 400000 }),
|
||||||
|
})
|
||||||
|
const respuesta = json.mock.calls[0]?.[0] as { esquema: { engancheAdicional: number; saldoAFinanciar: number } }
|
||||||
|
expect(respuesta.esquema.engancheAdicional).toBe(400000)
|
||||||
|
expect(respuesta.esquema.saldoAFinanciar).toBeCloseTo(445000, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rechaza con 400 un adicional que pase del 90% de enganche total', async () => {
|
||||||
|
const { res } = resFake()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
crearCotizacion(reqFake({ ...bodyBase, engancheAdicional: 900000 }), res),
|
||||||
|
).rejects.toBeDefined()
|
||||||
|
expect(prismaMock.cotizacion.create).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ const crearCotizacionSchema = z.object({
|
|||||||
.max(MENSUALIDADES_MAX, `Máximo ${MENSUALIDADES_MAX} mensualidades`)
|
.max(MENSUALIDADES_MAX, `Máximo ${MENSUALIDADES_MAX} mensualidades`)
|
||||||
.optional(),
|
.optional(),
|
||||||
escrituracionDiferida: z.boolean().optional(),
|
escrituracionDiferida: z.boolean().optional(),
|
||||||
|
// Enganche adicional (anticipo extra): el total de anticipos (esquema +
|
||||||
|
// adicional) no puede superar el 90% del precio; el espejo lo valida
|
||||||
|
engancheAdicional: z.number().min(0, 'El enganche adicional no puede ser negativo').optional(),
|
||||||
// Opciones de credito del saldo (84.5%): JSON serializado {elegida, ajustes}
|
// Opciones de credito del saldo (84.5%): JSON serializado {elegida, ajustes}
|
||||||
// que manda el cotizador; se persiste crudo y se devuelve en los GET
|
// que manda el cotizador; se persiste crudo y se devuelve en los GET
|
||||||
opcionesCredito: z.string().max(4000).optional(),
|
opcionesCredito: z.string().max(4000).optional(),
|
||||||
@@ -117,12 +120,21 @@ export async function crearCotizacion(req: Request, res: Response): Promise<void
|
|||||||
const numMensualidades = Math.min(body.numMensualidades ?? MENSUALIDADES_DEFAULT, topeMensualidades)
|
const numMensualidades = Math.min(body.numMensualidades ?? MENSUALIDADES_DEFAULT, topeMensualidades)
|
||||||
const escrituracionDiferida = body.escrituracionDiferida ?? false
|
const escrituracionDiferida = body.escrituracionDiferida ?? false
|
||||||
const precioLista = obtenerPrecioLista(unidad, body.acabados)
|
const precioLista = obtenerPrecioLista(unidad, body.acabados)
|
||||||
const esquema: EsquemaFrasaResultado = calcularEsquemaFrasa({
|
// Enganche adicional: el total de anticipos no puede pasar del 90% del
|
||||||
|
// precio (espejo valida; se traduce a 400 con el mensaje del negocio)
|
||||||
|
const esquema: EsquemaFrasaResultado = (() => {
|
||||||
|
try {
|
||||||
|
return calcularEsquemaFrasa({
|
||||||
precio: precioLista,
|
precio: precioLista,
|
||||||
apartado,
|
apartado,
|
||||||
numMensualidades,
|
numMensualidades,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
|
...(body.engancheAdicional !== undefined ? { engancheAdicional: body.engancheAdicional } : {}),
|
||||||
})
|
})
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppError(400, err instanceof Error ? err.message : 'Datos del esquema inválidos')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
const cotizacion = await prisma.cotizacion.create({
|
const cotizacion = await prisma.cotizacion.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -143,6 +155,7 @@ export async function crearCotizacion(req: Request, res: Response): Promise<void
|
|||||||
pctEscrituracion: PCT_ESCRITURACION,
|
pctEscrituracion: PCT_ESCRITURACION,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
escrituracion: esquema.escrituracion,
|
escrituracion: esquema.escrituracion,
|
||||||
|
engancheAdicional: esquema.engancheAdicional,
|
||||||
// Opciones de credito elegidas por el vendedor (JSON crudo)
|
// Opciones de credito elegidas por el vendedor (JSON crudo)
|
||||||
opcionesCredito: body.opcionesCredito ?? null,
|
opcionesCredito: body.opcionesCredito ?? null,
|
||||||
// Columnas legacy, rellenadas para compatibilidad de reportes:
|
// Columnas legacy, rellenadas para compatibilidad de reportes:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
validarEsquemaInput,
|
validarEsquemaInput,
|
||||||
PCT_ENGANCHE_FIRMA,
|
PCT_ENGANCHE_FIRMA,
|
||||||
PCT_ENGANCHE,
|
PCT_ENGANCHE,
|
||||||
|
PCT_ENGANCHE_MAXIMO,
|
||||||
PCT_ESCRITURACION,
|
PCT_ESCRITURACION,
|
||||||
MENSUALIDADES_DEFAULT,
|
MENSUALIDADES_DEFAULT,
|
||||||
MENSUALIDADES_MIN,
|
MENSUALIDADES_MIN,
|
||||||
@@ -107,3 +108,28 @@ describe('validarEsquemaInput — guardas del esquema', () => {
|
|||||||
expect(MENSUALIDADES_MAX).toBe(36)
|
expect(MENSUALIDADES_MAX).toBe(36)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('calcularEsquemaFrasa (API) — enganche adicional (hasta 90% de anticipo)', () => {
|
||||||
|
const base = { precio: 1000000, apartado: 15000, numMensualidades: 14, escrituracionDiferida: false }
|
||||||
|
|
||||||
|
it('sin adicional: 0 y saldo a financiar del 84.5%', () => {
|
||||||
|
const r = calcularEsquemaFrasa(base)
|
||||||
|
expect(r.engancheAdicional).toBe(0)
|
||||||
|
expect(r.saldoAFinanciar).toBeCloseTo(845000, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('el adicional reduce el saldo a financiar peso a peso', () => {
|
||||||
|
const r = calcularEsquemaFrasa({ ...base, engancheAdicional: 400000 })
|
||||||
|
expect(r.saldoAFinanciar).toBeCloseTo(445000, 2)
|
||||||
|
expect(r.totalPagado).toBeCloseTo(155000, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rechaza pasar del 90% de enganche total', () => {
|
||||||
|
expect(PCT_ENGANCHE_MAXIMO).toBe(0.9)
|
||||||
|
expect(() => calcularEsquemaFrasa({ ...base, engancheAdicional: 745001 })).toThrow(/90%/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rechaza adicional negativo', () => {
|
||||||
|
expect(validarEsquemaInput({ ...base, engancheAdicional: -1 })).toMatch(/adicional/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
export const PCT_ENGANCHE_FIRMA = 0.025
|
export const PCT_ENGANCHE_FIRMA = 0.025
|
||||||
export const PCT_ENGANCHE = 0.075
|
export const PCT_ENGANCHE = 0.075
|
||||||
export const PCT_ESCRITURACION = 0.055
|
export const PCT_ESCRITURACION = 0.055
|
||||||
|
/** Tope total de enganche (esquema + adicional): el cliente puede dar hasta
|
||||||
|
* 90% de anticipo, quedando un mínimo del 10% del precio a financiar */
|
||||||
|
export const PCT_ENGANCHE_MAXIMO = 0.9
|
||||||
|
|
||||||
export const MENSUALIDADES_DEFAULT = 14
|
export const MENSUALIDADES_DEFAULT = 14
|
||||||
export const MENSUALIDADES_MIN = 1
|
export const MENSUALIDADES_MIN = 1
|
||||||
@@ -34,6 +37,9 @@ export interface EsquemaFrasaInput {
|
|||||||
numMensualidades: number
|
numMensualidades: number
|
||||||
/** true = la escrituración se difiere con el enganche; false = se paga a la firma */
|
/** true = la escrituración se difiere con el enganche; false = se paga a la firma */
|
||||||
escrituracionDiferida: boolean
|
escrituracionDiferida: boolean
|
||||||
|
/** Enganche adicional (anticipo extra, >= 0): opcional; el total de
|
||||||
|
* anticipos (esquema + adicional) no puede superar el 90% del precio */
|
||||||
|
engancheAdicional?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EsquemaFrasaResultado {
|
export interface EsquemaFrasaResultado {
|
||||||
@@ -57,6 +63,10 @@ export interface EsquemaFrasaResultado {
|
|||||||
mensualidad: number
|
mensualidad: number
|
||||||
/** apartado + firma + diferible — debe cuadrar con el precio saldado */
|
/** apartado + firma + diferible — debe cuadrar con el precio saldado */
|
||||||
totalPagado: number
|
totalPagado: number
|
||||||
|
/** Anticipo extra sobre el esquema (0 si no se captura) */
|
||||||
|
engancheAdicional: number
|
||||||
|
/** Lo que queda por financiar: precio − esquema − adicional (mínimo 10%) */
|
||||||
|
saldoAFinanciar: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Error de validación de entrada; null si es válida */
|
/** Error de validación de entrada; null si es válida */
|
||||||
@@ -67,6 +77,8 @@ export function validarEsquemaInput(input: EsquemaFrasaInput): string | null {
|
|||||||
if (!Number.isInteger(input.numMensualidades)) return 'Las mensualidades deben ser un número entero'
|
if (!Number.isInteger(input.numMensualidades)) return 'Las mensualidades deben ser un número entero'
|
||||||
if (input.numMensualidades < MENSUALIDADES_MIN) return `Mínimo ${MENSUALIDADES_MIN} mensualidad`
|
if (input.numMensualidades < MENSUALIDADES_MIN) return `Mínimo ${MENSUALIDADES_MIN} mensualidad`
|
||||||
if (input.numMensualidades > MENSUALIDADES_MAX) return `Máximo ${MENSUALIDADES_MAX} mensualidades`
|
if (input.numMensualidades > MENSUALIDADES_MAX) return `Máximo ${MENSUALIDADES_MAX} mensualidades`
|
||||||
|
const adicional = input.engancheAdicional ?? 0
|
||||||
|
if (!Number.isFinite(adicional) || adicional < 0) return 'El enganche adicional no puede ser negativo'
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +87,7 @@ export function calcularEsquemaFrasa(input: EsquemaFrasaInput): EsquemaFrasaResu
|
|||||||
if (error) throw new Error(error)
|
if (error) throw new Error(error)
|
||||||
|
|
||||||
const { precio, apartado, numMensualidades, escrituracionDiferida } = input
|
const { precio, apartado, numMensualidades, escrituracionDiferida } = input
|
||||||
|
const engancheAdicional = input.engancheAdicional ?? 0
|
||||||
const engancheFirmaTotal = precio * PCT_ENGANCHE_FIRMA
|
const engancheFirmaTotal = precio * PCT_ENGANCHE_FIRMA
|
||||||
const saldoFirma = Math.max(0, engancheFirmaTotal - apartado)
|
const saldoFirma = Math.max(0, engancheFirmaTotal - apartado)
|
||||||
const excedenteApartado = Math.max(0, apartado - engancheFirmaTotal)
|
const excedenteApartado = Math.max(0, apartado - engancheFirmaTotal)
|
||||||
@@ -84,6 +97,14 @@ export function calcularEsquemaFrasa(input: EsquemaFrasaInput): EsquemaFrasaResu
|
|||||||
const baseDiferible = enganche + (escrituracionDiferida ? escrituracion : 0)
|
const baseDiferible = enganche + (escrituracionDiferida ? escrituracion : 0)
|
||||||
const mensualidad = baseDiferible / numMensualidades
|
const mensualidad = baseDiferible / numMensualidades
|
||||||
const totalPagado = apartado + pagoUnicoFirma + baseDiferible
|
const totalPagado = apartado + pagoUnicoFirma + baseDiferible
|
||||||
|
// El anticipo total (esquema + adicional) no puede superar el 90% del precio
|
||||||
|
if (totalPagado + engancheAdicional > precio * PCT_ENGANCHE_MAXIMO + 1e-9) {
|
||||||
|
throw new Error(
|
||||||
|
`El enganche total no puede superar el ${PCT_ENGANCHE_MAXIMO * 100}% del precio (` +
|
||||||
|
`máx. ${(precio * PCT_ENGANCHE_MAXIMO - totalPagado).toFixed(2)} de enganche adicional)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const saldoAFinanciar = precio - totalPagado - engancheAdicional
|
||||||
|
|
||||||
return {
|
return {
|
||||||
precio,
|
precio,
|
||||||
@@ -99,5 +120,7 @@ export function calcularEsquemaFrasa(input: EsquemaFrasaInput): EsquemaFrasaResu
|
|||||||
numMensualidades,
|
numMensualidades,
|
||||||
mensualidad,
|
mensualidad,
|
||||||
totalPagado,
|
totalPagado,
|
||||||
|
engancheAdicional,
|
||||||
|
saldoAFinanciar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
MENSUALIDADES_MIN,
|
MENSUALIDADES_MIN,
|
||||||
PCT_ENGANCHE,
|
PCT_ENGANCHE,
|
||||||
PCT_ENGANCHE_FIRMA,
|
PCT_ENGANCHE_FIRMA,
|
||||||
|
PCT_ENGANCHE_MAXIMO,
|
||||||
PCT_ESCRITURACION,
|
PCT_ESCRITURACION,
|
||||||
type EsquemaFrasaResultado,
|
type EsquemaFrasaResultado,
|
||||||
} from '../lib/esquema-frasa'
|
} from '../lib/esquema-frasa'
|
||||||
@@ -15,6 +16,11 @@ interface Props {
|
|||||||
fechaEntrega?: string | null
|
fechaEntrega?: string | null
|
||||||
/** Tope de mensualidades: los meses que faltan para la entrega (default 36 sin fecha) */
|
/** Tope de mensualidades: los meses que faltan para la entrega (default 36 sin fecha) */
|
||||||
maxMensualidades?: number
|
maxMensualidades?: number
|
||||||
|
/** Enganche adicional capturado (>= 0) */
|
||||||
|
engancheAdicional: number
|
||||||
|
onEngancheAdicional: (v: number) => void
|
||||||
|
/** Máximo de enganche adicional permitido por el tope del 90% de anticipo */
|
||||||
|
engancheAdicionalMaximo: number
|
||||||
numMensualidades: number
|
numMensualidades: number
|
||||||
onNumMensualidades: (n: number) => void
|
onNumMensualidades: (n: number) => void
|
||||||
escrituracionDiferida: boolean
|
escrituracionDiferida: boolean
|
||||||
@@ -38,6 +44,9 @@ export function EsquemaPanel({
|
|||||||
esquema,
|
esquema,
|
||||||
fechaEntrega,
|
fechaEntrega,
|
||||||
maxMensualidades = MENSUALIDADES_MAX,
|
maxMensualidades = MENSUALIDADES_MAX,
|
||||||
|
engancheAdicional,
|
||||||
|
onEngancheAdicional,
|
||||||
|
engancheAdicionalMaximo,
|
||||||
numMensualidades,
|
numMensualidades,
|
||||||
onNumMensualidades,
|
onNumMensualidades,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
@@ -132,9 +141,53 @@ export function EsquemaPanel({
|
|||||||
{formatoMoneda(esquema.totalPagado)}
|
{formatoMoneda(esquema.totalPagado)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
{esquema.engancheAdicional > 0 && (
|
||||||
|
<div className="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<dt className="text-sm text-[#64748B] dark:text-[#94A3B8]">Enganche adicional</dt>
|
||||||
|
<dd className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">
|
||||||
|
{formatoMoneda(esquema.engancheAdicional)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<dt className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">
|
||||||
|
Saldo a financiar
|
||||||
|
</dt>
|
||||||
|
<dd className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">
|
||||||
|
{formatoMoneda(esquema.saldoAFinanciar)}
|
||||||
|
<span className="ml-2 text-xs font-normal text-[#64748B] dark:text-[#94A3B8]">
|
||||||
|
{formatoPorcentaje(esquema.saldoAFinanciar / esquema.precio)} del precio
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Enganche adicional: anticipo extra hasta completar 90% del precio */}
|
||||||
|
<section className="rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
|
||||||
|
<label htmlFor="enganche-adicional" className="mb-2 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">
|
||||||
|
Enganche adicional (opcional)
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-[#64748B] dark:text-[#94A3B8]">$</span>
|
||||||
|
<input
|
||||||
|
id="enganche-adicional"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={engancheAdicionalMaximo}
|
||||||
|
step={1000}
|
||||||
|
value={engancheAdicional === 0 ? '' : engancheAdicional}
|
||||||
|
placeholder="0"
|
||||||
|
onChange={(e) => onEngancheAdicional(e.target.value === '' ? 0 : Number(e.target.value))}
|
||||||
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:ring-2 focus:ring-accent/20 focus:outline-none dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#E2E8F0]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||||
|
Anticipo extra del cliente; máximo {formatoMoneda(engancheAdicionalMaximo)} para no pasar del{' '}
|
||||||
|
{formatoPorcentaje(PCT_ENGANCHE_MAXIMO)} de enganche total (queda un mínimo del 10% por financiar)
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Controles del diferimiento */}
|
{/* Controles del diferimiento */}
|
||||||
<section className="flex flex-col gap-4 rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm sm:flex-row sm:items-end">
|
<section className="flex flex-col gap-4 rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm sm:flex-row sm:items-end">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
MENSUALIDADES_DEFAULT,
|
MENSUALIDADES_DEFAULT,
|
||||||
MENSUALIDADES_MAX,
|
MENSUALIDADES_MAX,
|
||||||
MENSUALIDADES_MIN,
|
MENSUALIDADES_MIN,
|
||||||
|
PCT_ENGANCHE_MAXIMO,
|
||||||
calcularEsquemaFrasa,
|
calcularEsquemaFrasa,
|
||||||
type EsquemaFrasaResultado,
|
type EsquemaFrasaResultado,
|
||||||
} from '../lib/esquema-frasa'
|
} from '../lib/esquema-frasa'
|
||||||
@@ -53,6 +54,12 @@ interface UseCotizacionResult {
|
|||||||
/** true = la escrituración (5.5%) se difiere con el enganche; false = se paga a la firma */
|
/** true = la escrituración (5.5%) se difiere con el enganche; false = se paga a la firma */
|
||||||
escrituracionDiferida: boolean
|
escrituracionDiferida: boolean
|
||||||
setEscrituracionDiferida: (v: boolean) => void
|
setEscrituracionDiferida: (v: boolean) => void
|
||||||
|
/** Enganche adicional (anticipo extra, opcional): el total de anticipos no
|
||||||
|
* puede superar el 90% del precio (mínimo 10% queda por financiar) */
|
||||||
|
engancheAdicional: number
|
||||||
|
setEngancheAdicional: (v: number) => void
|
||||||
|
/** Máximo de enganche adicional permitido por el tope del 90% (0 sin unidad) */
|
||||||
|
engancheAdicionalMaximo: number
|
||||||
precioLista: number
|
precioLista: number
|
||||||
/** Esquema FRASA 2026 calculado; null mientras no hay unidad elegida */
|
/** Esquema FRASA 2026 calculado; null mientras no hay unidad elegida */
|
||||||
resultado: EsquemaFrasaResultado | null
|
resultado: EsquemaFrasaResultado | null
|
||||||
@@ -74,6 +81,8 @@ export interface EstadoRestaurar {
|
|||||||
numMensualidades?: number
|
numMensualidades?: number
|
||||||
/** Escrituración diferida guardada (default false) */
|
/** Escrituración diferida guardada (default false) */
|
||||||
escrituracionDiferida?: boolean
|
escrituracionDiferida?: boolean
|
||||||
|
/** Enganche adicional guardado (default 0) */
|
||||||
|
engancheAdicional?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,6 +100,7 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
|||||||
const [apartado, setApartado] = useState<number>(APARTADO_ESQUEMA_DEFAULT)
|
const [apartado, setApartado] = useState<number>(APARTADO_ESQUEMA_DEFAULT)
|
||||||
const [numMensualidades, setNumMensualidades] = useState<number>(MENSUALIDADES_DEFAULT)
|
const [numMensualidades, setNumMensualidades] = useState<number>(MENSUALIDADES_DEFAULT)
|
||||||
const [escrituracionDiferida, setEscrituracionDiferida] = useState(false)
|
const [escrituracionDiferida, setEscrituracionDiferida] = useState(false)
|
||||||
|
const [engancheAdicional, setEngancheAdicionalState] = useState(0)
|
||||||
|
|
||||||
const params = desarrolloEfectivo(reglas, desarrollo)
|
const params = desarrolloEfectivo(reglas, desarrollo)
|
||||||
|
|
||||||
@@ -115,6 +125,12 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
|||||||
setNumMensualidades(Math.max(MENSUALIDADES_MIN, Math.min(n, topeMensualidades())))
|
setNumMensualidades(Math.max(MENSUALIDADES_MIN, Math.min(n, topeMensualidades())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Enganche adicional: acotado a >= 0; el esquema valida que el anticipo
|
||||||
|
* total no supere el 90% del precio (mínimo 10% por financiar) */
|
||||||
|
const setEngancheAdicional = (v: number) => {
|
||||||
|
setEngancheAdicionalState(Number.isFinite(v) ? Math.max(0, v) : 0)
|
||||||
|
}
|
||||||
|
|
||||||
const restaurar = (estado: EstadoRestaurar) => {
|
const restaurar = (estado: EstadoRestaurar) => {
|
||||||
setDesarrolloState(estado.desarrollo)
|
setDesarrolloState(estado.desarrollo)
|
||||||
setUnidad(estado.unidad)
|
setUnidad(estado.unidad)
|
||||||
@@ -126,6 +142,7 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
|||||||
Math.min(estado.numMensualidades ?? MENSUALIDADES_DEFAULT, mensualidadesParaUnidad(estado.unidad)),
|
Math.min(estado.numMensualidades ?? MENSUALIDADES_DEFAULT, mensualidadesParaUnidad(estado.unidad)),
|
||||||
)
|
)
|
||||||
setEscrituracionDiferida(estado.escrituracionDiferida ?? false)
|
setEscrituracionDiferida(estado.escrituracionDiferida ?? false)
|
||||||
|
setEngancheAdicionalState(Math.max(0, estado.engancheAdicional ?? 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nueva = () => {
|
const nueva = () => {
|
||||||
@@ -134,6 +151,7 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
|||||||
setApartado(APARTADO_ESQUEMA_DEFAULT)
|
setApartado(APARTADO_ESQUEMA_DEFAULT)
|
||||||
setNumMensualidades(MENSUALIDADES_DEFAULT)
|
setNumMensualidades(MENSUALIDADES_DEFAULT)
|
||||||
setEscrituracionDiferida(false)
|
setEscrituracionDiferida(false)
|
||||||
|
setEngancheAdicionalState(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const numMeses = useMemo(
|
const numMeses = useMemo(
|
||||||
@@ -150,15 +168,36 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
|||||||
// incluso $0; solo se acota a >= 0 como guarda contra capturas raras
|
// incluso $0; solo se acota a >= 0 como guarda contra capturas raras
|
||||||
const apartadoEfectivo = Math.max(0, apartado)
|
const apartadoEfectivo = Math.max(0, apartado)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Esquema FRASA 2026 calculado; null mientras no hay unidad elegida. El
|
||||||
|
* enganche adicional se acota al máximo que permite el 90% de anticipo
|
||||||
|
* (el esquema lanza error si el total lo rebasa; aquí se previene).
|
||||||
|
*/
|
||||||
const resultado = useMemo<EsquemaFrasaResultado | null>(() => {
|
const resultado = useMemo<EsquemaFrasaResultado | null>(() => {
|
||||||
if (!unidad || precioLista <= 0) return null
|
if (!unidad || precioLista <= 0) return null
|
||||||
return calcularEsquemaFrasa({
|
const base = calcularEsquemaFrasa({
|
||||||
precio: precioLista,
|
precio: precioLista,
|
||||||
apartado: apartadoEfectivo,
|
apartado: apartadoEfectivo,
|
||||||
numMensualidades,
|
numMensualidades,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
})
|
})
|
||||||
}, [unidad, precioLista, apartadoEfectivo, numMensualidades, escrituracionDiferida])
|
const maxAdicional = Math.max(0, precioLista * PCT_ENGANCHE_MAXIMO - base.totalPagado)
|
||||||
|
const adicionalEfectivo = Math.min(Math.max(0, engancheAdicional), maxAdicional)
|
||||||
|
if (adicionalEfectivo === 0) return base
|
||||||
|
return calcularEsquemaFrasa({
|
||||||
|
precio: precioLista,
|
||||||
|
apartado: apartadoEfectivo,
|
||||||
|
numMensualidades,
|
||||||
|
escrituracionDiferida,
|
||||||
|
engancheAdicional: adicionalEfectivo,
|
||||||
|
})
|
||||||
|
}, [unidad, precioLista, apartadoEfectivo, numMensualidades, escrituracionDiferida, engancheAdicional])
|
||||||
|
|
||||||
|
/** Máximo de enganche adicional que permite el 90% de anticipo (0 sin unidad) */
|
||||||
|
const engancheAdicionalMaximo = useMemo(() => {
|
||||||
|
if (!resultado) return 0
|
||||||
|
return Math.max(0, precioLista * PCT_ENGANCHE_MAXIMO - (resultado.totalPagado - resultado.engancheAdicional))
|
||||||
|
}, [resultado, precioLista])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
desarrollo,
|
desarrollo,
|
||||||
@@ -175,6 +214,9 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
|||||||
setNumMensualidades: ajustarMensualidades,
|
setNumMensualidades: ajustarMensualidades,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
setEscrituracionDiferida,
|
setEscrituracionDiferida,
|
||||||
|
engancheAdicional,
|
||||||
|
setEngancheAdicional,
|
||||||
|
engancheAdicionalMaximo,
|
||||||
precioLista,
|
precioLista,
|
||||||
resultado,
|
resultado,
|
||||||
numMeses,
|
numMeses,
|
||||||
|
|||||||
@@ -79,3 +79,37 @@ describe('calcularEsquemaFrasa — mensualidades', () => {
|
|||||||
expect(validarEsquemaInput({ precio: 1000000, apartado: 15000, numMensualidades: 14, escrituracionDiferida: false })).toBeNull()
|
expect(validarEsquemaInput({ precio: 1000000, apartado: 15000, numMensualidades: 14, escrituracionDiferida: false })).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('calcularEsquemaFrasa — enganche adicional (hasta 90% de anticipo)', () => {
|
||||||
|
const base = { precio: 1000000, apartado: 15000, numMensualidades: 14, escrituracionDiferida: false }
|
||||||
|
|
||||||
|
it('sin adicional: queda en 0 y el saldo a financiar es el 84.5%', () => {
|
||||||
|
const r = calcularEsquemaFrasa(base)
|
||||||
|
expect(r.engancheAdicional).toBe(0)
|
||||||
|
expect(r.saldoAFinanciar).toBeCloseTo(845000, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('el adicional reduce el saldo a financiar peso a peso', () => {
|
||||||
|
const r = calcularEsquemaFrasa({ ...base, engancheAdicional: 400000 })
|
||||||
|
expect(r.engancheAdicional).toBe(400000)
|
||||||
|
expect(r.totalPagado).toBeCloseTo(155000, 2)
|
||||||
|
expect(r.saldoAFinanciar).toBeCloseTo(445000, 2)
|
||||||
|
// El esquema base no cambia: el adicional es aparte
|
||||||
|
expect(r.enganche).toBeCloseTo(75000, 2)
|
||||||
|
expect(r.mensualidad).toBeCloseTo(75000 / 14, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tope exacto: 90% del precio menos el esquema (745,000) es válido', () => {
|
||||||
|
const r = calcularEsquemaFrasa({ ...base, engancheAdicional: 745000 })
|
||||||
|
expect(r.saldoAFinanciar).toBeCloseTo(100000, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rechaza pasar del 90% de enganche total', () => {
|
||||||
|
expect(() => calcularEsquemaFrasa({ ...base, engancheAdicional: 745001 })).toThrow(/90%/)
|
||||||
|
expect(() => calcularEsquemaFrasa({ ...base, engancheAdicional: 900000 })).toThrow(/90%/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rechaza adicional negativo', () => {
|
||||||
|
expect(validarEsquemaInput({ ...base, engancheAdicional: -1 })).toMatch(/adicional/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
export const PCT_ENGANCHE_FIRMA = 0.025
|
export const PCT_ENGANCHE_FIRMA = 0.025
|
||||||
export const PCT_ENGANCHE = 0.075
|
export const PCT_ENGANCHE = 0.075
|
||||||
export const PCT_ESCRITURACION = 0.055
|
export const PCT_ESCRITURACION = 0.055
|
||||||
|
/** Tope total de enganche (esquema + adicional): el cliente puede dar hasta
|
||||||
|
* 90% de anticipo, quedando un mínimo del 10% del precio a financiar */
|
||||||
|
export const PCT_ENGANCHE_MAXIMO = 0.9
|
||||||
|
|
||||||
export const MENSUALIDADES_DEFAULT = 14
|
export const MENSUALIDADES_DEFAULT = 14
|
||||||
export const MENSUALIDADES_MIN = 1
|
export const MENSUALIDADES_MIN = 1
|
||||||
@@ -31,6 +34,9 @@ export interface EsquemaFrasaInput {
|
|||||||
numMensualidades: number
|
numMensualidades: number
|
||||||
/** true = la escrituración se difiere con el enganche; false = se paga a la firma */
|
/** true = la escrituración se difiere con el enganche; false = se paga a la firma */
|
||||||
escrituracionDiferida: boolean
|
escrituracionDiferida: boolean
|
||||||
|
/** Enganche adicional (anticipo extra, >= 0): opcional; el total de
|
||||||
|
* anticipos (esquema + adicional) no puede superar el 90% del precio */
|
||||||
|
engancheAdicional?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EsquemaFrasaResultado {
|
export interface EsquemaFrasaResultado {
|
||||||
@@ -54,6 +60,10 @@ export interface EsquemaFrasaResultado {
|
|||||||
mensualidad: number
|
mensualidad: number
|
||||||
/** apartado + firma + diferible — debe cuadrar con el precio saldado */
|
/** apartado + firma + diferible — debe cuadrar con el precio saldado */
|
||||||
totalPagado: number
|
totalPagado: number
|
||||||
|
/** Anticipo extra sobre el esquema (0 si no se captura) */
|
||||||
|
engancheAdicional: number
|
||||||
|
/** Lo que queda por financiar: precio − esquema − adicional (mínimo 10%) */
|
||||||
|
saldoAFinanciar: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Error de validación de entrada; null si es válida */
|
/** Error de validación de entrada; null si es válida */
|
||||||
@@ -64,6 +74,8 @@ export function validarEsquemaInput(input: EsquemaFrasaInput): string | null {
|
|||||||
if (!Number.isInteger(input.numMensualidades)) return 'Las mensualidades deben ser un número entero'
|
if (!Number.isInteger(input.numMensualidades)) return 'Las mensualidades deben ser un número entero'
|
||||||
if (input.numMensualidades < MENSUALIDADES_MIN) return `Mínimo ${MENSUALIDADES_MIN} mensualidad`
|
if (input.numMensualidades < MENSUALIDADES_MIN) return `Mínimo ${MENSUALIDADES_MIN} mensualidad`
|
||||||
if (input.numMensualidades > MENSUALIDADES_MAX) return `Máximo ${MENSUALIDADES_MAX} mensualidades`
|
if (input.numMensualidades > MENSUALIDADES_MAX) return `Máximo ${MENSUALIDADES_MAX} mensualidades`
|
||||||
|
const adicional = input.engancheAdicional ?? 0
|
||||||
|
if (!Number.isFinite(adicional) || adicional < 0) return 'El enganche adicional no puede ser negativo'
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +84,7 @@ export function calcularEsquemaFrasa(input: EsquemaFrasaInput): EsquemaFrasaResu
|
|||||||
if (error) throw new Error(error)
|
if (error) throw new Error(error)
|
||||||
|
|
||||||
const { precio, apartado, numMensualidades, escrituracionDiferida } = input
|
const { precio, apartado, numMensualidades, escrituracionDiferida } = input
|
||||||
|
const engancheAdicional = input.engancheAdicional ?? 0
|
||||||
const engancheFirmaTotal = precio * PCT_ENGANCHE_FIRMA
|
const engancheFirmaTotal = precio * PCT_ENGANCHE_FIRMA
|
||||||
const saldoFirma = Math.max(0, engancheFirmaTotal - apartado)
|
const saldoFirma = Math.max(0, engancheFirmaTotal - apartado)
|
||||||
const excedenteApartado = Math.max(0, apartado - engancheFirmaTotal)
|
const excedenteApartado = Math.max(0, apartado - engancheFirmaTotal)
|
||||||
@@ -81,6 +94,14 @@ export function calcularEsquemaFrasa(input: EsquemaFrasaInput): EsquemaFrasaResu
|
|||||||
const baseDiferible = enganche + (escrituracionDiferida ? escrituracion : 0)
|
const baseDiferible = enganche + (escrituracionDiferida ? escrituracion : 0)
|
||||||
const mensualidad = baseDiferible / numMensualidades
|
const mensualidad = baseDiferible / numMensualidades
|
||||||
const totalPagado = apartado + pagoUnicoFirma + baseDiferible
|
const totalPagado = apartado + pagoUnicoFirma + baseDiferible
|
||||||
|
// El anticipo total (esquema + adicional) no puede superar el 90% del precio
|
||||||
|
if (totalPagado + engancheAdicional > precio * PCT_ENGANCHE_MAXIMO + 1e-9) {
|
||||||
|
throw new Error(
|
||||||
|
`El enganche total no puede superar el ${PCT_ENGANCHE_MAXIMO * 100}% del precio (` +
|
||||||
|
`máx. ${(precio * PCT_ENGANCHE_MAXIMO - totalPagado).toFixed(2)} de enganche adicional)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const saldoAFinanciar = precio - totalPagado - engancheAdicional
|
||||||
|
|
||||||
return {
|
return {
|
||||||
precio,
|
precio,
|
||||||
@@ -96,5 +117,7 @@ export function calcularEsquemaFrasa(input: EsquemaFrasaInput): EsquemaFrasaResu
|
|||||||
numMensualidades,
|
numMensualidades,
|
||||||
mensualidad,
|
mensualidad,
|
||||||
totalPagado,
|
totalPagado,
|
||||||
|
engancheAdicional,
|
||||||
|
saldoAFinanciar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,3 +182,21 @@ describe('calcularOpcionCredito — tasa cero', () => {
|
|||||||
expect(r.interesesEstimados).toBe(Number((mensualidad * 240 - 845000).toFixed(2)))
|
expect(r.interesesEstimados).toBe(Number((mensualidad * 240 - 845000).toFixed(2)))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('calcularOpcionCredito — enganche adicional', () => {
|
||||||
|
it('el saldo a financiar de cada opción toma el adicional (y se acota al 90%)', () => {
|
||||||
|
const base = {
|
||||||
|
precioLista: 1000000,
|
||||||
|
apartado: 15000,
|
||||||
|
numMensualidades: 14,
|
||||||
|
escrituracionDiferida: false,
|
||||||
|
ajustes: AJUSTES_DEFAULT.CREDITO_BANCARIO,
|
||||||
|
}
|
||||||
|
const r = calcularOpcionCredito({ ...base, tipo: 'CREDITO_BANCARIO', engancheAdicional: 400000 })
|
||||||
|
expect(r.saldoAFinanciar).toBe(445000)
|
||||||
|
expect(r.montoCredito).toBe(445000)
|
||||||
|
// Contado: precio final 980,000 → saldo 828,100 − 400,000 = 428,100 de contado
|
||||||
|
const contado = calcularOpcionCredito({ ...base, tipo: 'CONTADO', ajustes: AJUSTES_DEFAULT.CONTADO, engancheAdicional: 400000 })
|
||||||
|
expect(contado.saldoAFinanciar).toBe(428100)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* Módulo puro (sin UI, sin API) para probarlo a fondo.
|
* Módulo puro (sin UI, sin API) para probarlo a fondo.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { calcularEsquemaFrasa, type EsquemaFrasaResultado } from './esquema-frasa'
|
import { PCT_ENGANCHE_MAXIMO, calcularEsquemaFrasa, type EsquemaFrasaResultado } from './esquema-frasa'
|
||||||
|
|
||||||
export type TipoCredito =
|
export type TipoCredito =
|
||||||
| 'CREDITO_BANCARIO'
|
| 'CREDITO_BANCARIO'
|
||||||
@@ -68,6 +68,8 @@ export interface EntradaOpcion {
|
|||||||
apartado: number
|
apartado: number
|
||||||
numMensualidades: number
|
numMensualidades: number
|
||||||
escrituracionDiferida: boolean
|
escrituracionDiferida: boolean
|
||||||
|
/** Enganche adicional (monto absoluto, >= 0): se acota al 90% del anticipo */
|
||||||
|
engancheAdicional?: number
|
||||||
tipo: TipoCredito
|
tipo: TipoCredito
|
||||||
ajustes: AjustesCredito
|
ajustes: AjustesCredito
|
||||||
}
|
}
|
||||||
@@ -135,14 +137,28 @@ export function calcularOpcionCredito(e: EntradaOpcion): ResultadoOpcionCredito
|
|||||||
const montoDescuento = round2(e.precioLista * descuento)
|
const montoDescuento = round2(e.precioLista * descuento)
|
||||||
const precioFinal = e.precioLista - montoDescuento
|
const precioFinal = e.precioLista - montoDescuento
|
||||||
|
|
||||||
const esquema = calcularEsquemaFrasa({
|
const esquemaBase = calcularEsquemaFrasa({
|
||||||
precio: precioFinal,
|
precio: precioFinal,
|
||||||
apartado: e.apartado,
|
apartado: e.apartado,
|
||||||
numMensualidades: e.numMensualidades,
|
numMensualidades: e.numMensualidades,
|
||||||
escrituracionDiferida: e.escrituracionDiferida,
|
escrituracionDiferida: e.escrituracionDiferida,
|
||||||
})
|
})
|
||||||
|
// El adicional se acota igual que en el hook: el anticipo total no puede
|
||||||
|
// superar el 90% del precio final de la opción
|
||||||
|
const adicionalSolicitado = Math.max(0, e.engancheAdicional ?? 0)
|
||||||
|
const adicionalEfectivo = Math.min(adicionalSolicitado, round2(precioFinal * PCT_ENGANCHE_MAXIMO - esquemaBase.totalPagado))
|
||||||
|
const esquema =
|
||||||
|
adicionalEfectivo > 0
|
||||||
|
? calcularEsquemaFrasa({
|
||||||
|
precio: precioFinal,
|
||||||
|
apartado: e.apartado,
|
||||||
|
numMensualidades: e.numMensualidades,
|
||||||
|
escrituracionDiferida: e.escrituracionDiferida,
|
||||||
|
engancheAdicional: adicionalEfectivo,
|
||||||
|
})
|
||||||
|
: esquemaBase
|
||||||
|
|
||||||
const saldoAFinanciar = round2(precioFinal - esquema.totalPagado)
|
const saldoAFinanciar = round2(esquema.saldoAFinanciar)
|
||||||
const esContado = e.tipo === 'CONTADO'
|
const esContado = e.tipo === 'CONTADO'
|
||||||
const pctContado = e.tipo === 'SEMI_CONTADO' ? (e.ajustes.pctContado ?? 0) : 0
|
const pctContado = e.tipo === 'SEMI_CONTADO' ? (e.ajustes.pctContado ?? 0) : 0
|
||||||
const pagoContado = esContado ? 0 : round2(saldoAFinanciar * pctContado)
|
const pagoContado = esContado ? 0 : round2(saldoAFinanciar * pctContado)
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ export function crearDocumentoPdf({ unidad, acabados, apartado, precioLista, esq
|
|||||||
formatoMoneda(esquema.baseDiferible),
|
formatoMoneda(esquema.baseDiferible),
|
||||||
],
|
],
|
||||||
['TOTAL DEL ESQUEMA', '', '', formatoMoneda(esquema.totalPagado)],
|
['TOTAL DEL ESQUEMA', '', '', formatoMoneda(esquema.totalPagado)],
|
||||||
|
...(esquema.engancheAdicional > 0
|
||||||
|
? [['ENGANCHE ADICIONAL (ANTICIPO EXTRA)', '', '', formatoMoneda(esquema.engancheAdicional)]]
|
||||||
|
: []),
|
||||||
|
['SALDO A FINANCIAR', '', '', formatoMoneda(esquema.saldoAFinanciar)],
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
y = finalY(doc) + 6
|
y = finalY(doc) + 6
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export default function CotizadorPage() {
|
|||||||
setNumMensualidades,
|
setNumMensualidades,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
setEscrituracionDiferida,
|
setEscrituracionDiferida,
|
||||||
|
engancheAdicional,
|
||||||
|
setEngancheAdicional,
|
||||||
|
engancheAdicionalMaximo,
|
||||||
precioLista,
|
precioLista,
|
||||||
resultado,
|
resultado,
|
||||||
numMeses,
|
numMeses,
|
||||||
@@ -186,6 +189,7 @@ export default function CotizadorPage() {
|
|||||||
apartado: apartadoEfectivo,
|
apartado: apartadoEfectivo,
|
||||||
numMensualidades,
|
numMensualidades,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
|
engancheAdicional: resultado?.engancheAdicional ?? 0,
|
||||||
tipo,
|
tipo,
|
||||||
ajustes: ajustes[tipo],
|
ajustes: ajustes[tipo],
|
||||||
}),
|
}),
|
||||||
@@ -201,7 +205,7 @@ export default function CotizadorPage() {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [precioLista, apartadoEfectivo, numMensualidades, escrituracionDiferida, ajustesCredito])
|
}, [precioLista, apartadoEfectivo, numMensualidades, escrituracionDiferida, ajustesCredito, resultado])
|
||||||
|
|
||||||
// Prefill desde la URL (botón "Cotizar" de Mi día): cliente, contacto,
|
// Prefill desde la URL (botón "Cotizar" de Mi día): cliente, contacto,
|
||||||
// desarrollo, unidad y el id de la oportunidad a vincular al guardar
|
// desarrollo, unidad y el id de la oportunidad a vincular al guardar
|
||||||
@@ -303,6 +307,7 @@ export default function CotizadorPage() {
|
|||||||
engancheDiferible: resultado.enganche,
|
engancheDiferible: resultado.enganche,
|
||||||
escrituracion: resultado.escrituracion,
|
escrituracion: resultado.escrituracion,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
|
engancheAdicional: resultado.engancheAdicional,
|
||||||
enganche: resultado.enganche,
|
enganche: resultado.enganche,
|
||||||
numMensualidades,
|
numMensualidades,
|
||||||
mensualidad: resultado.mensualidad,
|
mensualidad: resultado.mensualidad,
|
||||||
@@ -331,6 +336,7 @@ export default function CotizadorPage() {
|
|||||||
apartado: apartadoEfectivo,
|
apartado: apartadoEfectivo,
|
||||||
numMensualidades,
|
numMensualidades,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
|
engancheAdicional: resultado.engancheAdicional,
|
||||||
opcionesCredito: JSON.stringify({ elegida: opcionElegida, ajustes: ajustesCredito }),
|
opcionesCredito: JSON.stringify({ elegida: opcionElegida, ajustes: ajustesCredito }),
|
||||||
...(oportunidadId !== null ? { oportunidadId } : {}),
|
...(oportunidadId !== null ? { oportunidadId } : {}),
|
||||||
})
|
})
|
||||||
@@ -496,6 +502,7 @@ export default function CotizadorPage() {
|
|||||||
? {
|
? {
|
||||||
numMensualidades: c.numMensualidades > 0 ? c.numMensualidades : undefined,
|
numMensualidades: c.numMensualidades > 0 ? c.numMensualidades : undefined,
|
||||||
escrituracionDiferida: c.escrituracionDiferida,
|
escrituracionDiferida: c.escrituracionDiferida,
|
||||||
|
engancheAdicional: c.engancheAdicional ?? 0,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
})
|
})
|
||||||
@@ -785,6 +792,9 @@ export default function CotizadorPage() {
|
|||||||
esquema={resultado}
|
esquema={resultado}
|
||||||
fechaEntrega={unidad?.fechaEntrega ?? null}
|
fechaEntrega={unidad?.fechaEntrega ?? null}
|
||||||
maxMensualidades={unidad ? mensualidadesParaUnidad(unidad) : MENSUALIDADES_MAX}
|
maxMensualidades={unidad ? mensualidadesParaUnidad(unidad) : MENSUALIDADES_MAX}
|
||||||
|
engancheAdicional={engancheAdicional}
|
||||||
|
onEngancheAdicional={setEngancheAdicional}
|
||||||
|
engancheAdicionalMaximo={engancheAdicionalMaximo}
|
||||||
numMensualidades={numMensualidades}
|
numMensualidades={numMensualidades}
|
||||||
onNumMensualidades={setNumMensualidades}
|
onNumMensualidades={setNumMensualidades}
|
||||||
escrituracionDiferida={escrituracionDiferida}
|
escrituracionDiferida={escrituracionDiferida}
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ export interface CotizacionGuardada {
|
|||||||
escrituracion?: number | null
|
escrituracion?: number | null
|
||||||
/** true = escrituración diferida con el enganche (esquema nuevo) */
|
/** true = escrituración diferida con el enganche (esquema nuevo) */
|
||||||
escrituracionDiferida?: boolean
|
escrituracionDiferida?: boolean
|
||||||
|
/** Enganche adicional (anticipo extra; esquema nuevo, 0 por default) */
|
||||||
|
engancheAdicional?: number | null
|
||||||
enganche: number
|
enganche: number
|
||||||
numMensualidades: number
|
numMensualidades: number
|
||||||
mensualidad: number | null
|
mensualidad: number | null
|
||||||
@@ -124,6 +126,8 @@ export interface GuardarCotizacionPayload {
|
|||||||
numMensualidades?: number
|
numMensualidades?: number
|
||||||
/** true = la escrituración (5.5%) se difiere con el enganche */
|
/** true = la escrituración (5.5%) se difiere con el enganche */
|
||||||
escrituracionDiferida?: boolean
|
escrituracionDiferida?: boolean
|
||||||
|
/** Enganche adicional (anticipo extra >= 0); el total no puede pasar del 90% del precio */
|
||||||
|
engancheAdicional?: number
|
||||||
/** Item del tablero Oportunidades a vincular (botón "Cotizar" de Mi día) */
|
/** Item del tablero Oportunidades a vincular (botón "Cotizar" de Mi día) */
|
||||||
oportunidadId?: string
|
oportunidadId?: string
|
||||||
/** JSON serializado {elegida, ajustes} de las 4 opciones de crédito; el API lo guarda crudo */
|
/** JSON serializado {elegida, ajustes} de las 4 opciones de crédito; el API lo guarda crudo */
|
||||||
|
|||||||
Reference in New Issue
Block a user