Add merge a BudgetInputs y aplicación de extras/ajustes al presupuesto

This commit is contained in:
Carlos Narro
2026-05-31 16:15:13 +02:00
parent a84d513c5b
commit 405fdc4e32
2 changed files with 128 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import type { BudgetInputs, BudgetResult, PartidaResult, TipoReforma } from '@/budget/types';
import type { AbstractedPreferences } from './preferences';
interface LeadInputsSource {
tipoReforma: TipoReforma | null;
m2Suelo: number | null;
alturaTecho: number | null;
provincia: string | null;
}
export function mergeIntoBudgetInputs(
prefs: AbstractedPreferences,
lead: LeadInputsSource,
): BudgetInputs {
return {
tipoReforma: lead.tipoReforma ?? 'otro',
m2Suelo: lead.m2Suelo,
alturaTecho: lead.alturaTecho,
calidadGlobal: prefs.calidadGlobal,
estructural: prefs.estructural,
provincia: lead.provincia,
materialSelections: prefs.materialSelections,
};
}
export function applyPreferences(
result: BudgetResult,
prefs: AbstractedPreferences,
): BudgetResult {
const extras: PartidaResult[] = prefs.elementos.map((e) => ({
key: e.key,
label: e.label,
importe: e.importe,
}));
prefs.ajustes.forEach((a, i) => {
const importe =
a.tipo === 'fijo' ? a.valor : Math.round(result.subtotal * (a.valor - 1));
if (importe === 0) return;
extras.push({ key: `ajuste_${i}`, label: a.label, importe });
});
if (extras.length === 0 && prefs.estiloRender.length === 0) return result;
const partidas = [...result.partidas, ...extras];
const subtotal = partidas.reduce((s, p) => s + p.importe, 0);
const total = Math.round(subtotal * result.factorZona);
const scale = result.total > 0 ? total / result.total : 1;
const rango = {
min: Math.round(result.rango.min * scale),
max: Math.round(result.rango.max * scale),
};
const materialesRender = [...result.materialesRender, ...prefs.estiloRender];
return { ...result, partidas, subtotal, total, rango, materialesRender };
}

View File

@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest';
import { applyPreferences, mergeIntoBudgetInputs } from '@/lib/voice/apply';
import type { AbstractedPreferences } from '@/lib/voice/preferences';
import type { BudgetResult } from '@/budget/types';
function prefs(partial: Partial<AbstractedPreferences>): AbstractedPreferences {
return {
calidadGlobal: 'media',
materialSelections: {},
estructural: false,
urgencia: null,
presupuestoTarget: null,
elementos: [],
estiloRender: [],
ajustes: [],
confianza: 'media',
resumen: '',
camposFaltantes: [],
...partial,
};
}
const base: BudgetResult = {
partidas: [{ key: 'mano_de_obra', label: 'Mano de obra', importe: 100000 }],
subtotal: 100000,
factorZona: 1,
total: 100000,
rango: { min: 85000, max: 115000 },
confianza: 'media',
materialesRender: ['suelo cerámico gris'],
avisos: [],
};
describe('applyPreferences', () => {
it('añade elementos y ajustes fijos como partidas y recalcula totales', () => {
const r = applyPreferences(base, prefs({
elementos: [{ key: 'isla_cocina', label: 'Isla de cocina', importe: 120000 }],
ajustes: [{ label: 'Encimera de piedra natural', tipo: 'fijo', valor: 50000, motivo: 'cuarzo' }],
estiloRender: ['estilo nórdico'],
}));
const byKey = Object.fromEntries(r.partidas.map((p) => [p.key, p.importe]));
expect(byKey.isla_cocina).toBe(120000);
expect(r.partidas.some((p) => p.label === 'Encimera de piedra natural' && p.importe === 50000)).toBe(true);
expect(r.subtotal).toBe(270000);
expect(r.total).toBe(270000);
expect(r.rango.min).toBe(229500); // round(85000 * 2.7)
expect(r.rango.max).toBe(310500); // round(115000 * 2.7)
expect(r.materialesRender).toContain('estilo nórdico');
});
it('un ajuste tipo factor se aplica sobre el subtotal original como partida explícita', () => {
const r = applyPreferences(base, prefs({
ajustes: [{ label: 'Acabado premium global', tipo: 'factor', valor: 1.1, motivo: 'premium' }],
}));
expect(r.partidas.some((p) => p.label === 'Acabado premium global' && p.importe === 10000)).toBe(true);
expect(r.subtotal).toBe(110000);
});
});
describe('mergeIntoBudgetInputs', () => {
it('vuelca preferencias y datos del lead en BudgetInputs', () => {
const inputs = mergeIntoBudgetInputs(
prefs({ calidadGlobal: 'premium', materialSelections: { suelo: 'suelo-p' }, estructural: true }),
{ tipoReforma: 'cocina', m2Suelo: 16, alturaTecho: null, provincia: 'Madrid' },
);
expect(inputs.calidadGlobal).toBe('premium');
expect(inputs.materialSelections.suelo).toBe('suelo-p');
expect(inputs.estructural).toBe(true);
expect(inputs.m2Suelo).toBe(16);
expect(inputs.provincia).toBe('Madrid');
});
});