Añade impermeabilización, extras fijos y zonas al motor de presupuesto

Acerca el cálculo a tarifas de mercado sin rehacer el modelo lineal €/m²:
- Impermeabilización como partida propia en zonas húmedas (cocina/baño/integral)
- Extras fijos que no escalan con m²: boletín (siempre), tuberías (piso anterior
  a 2000) y cambio de distribución (mover inodoro/ducha/bañera)
- Intensidad por tipo en fontanería/electricidad (baseline cocina) para que un
  integral no escale como un baño
- Factor de zona por provincia en tramos (Madrid/BCN 1.40, islas 1.30, capitales
  1.20, rural 0.85, resto 1.00)
- 2 preguntas nuevas en el formulario del cliente para disparar los extras
- Panel de precios: campo de impermeabilización + sección de extras fijos
- Seed recalibrado (mano de obra, extras, catálogo suelo/pared)
- Migración 0009 (leads.anterior_a_2000, leads.cambio_distribucion, pricing_config.extras)
- Tests del motor ampliados (impermeabilización, extras, intensidad por tipo)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Goyo Cancio
2026-06-04 14:02:57 +02:00
parent daa58c39a1
commit 2e3cd78216
18 changed files with 1941 additions and 18 deletions

View File

@@ -5,7 +5,7 @@ import type { BudgetInputs, CatalogItem, PricingConfig } from '@/budget/types';
const config: PricingConfig = {
alturaTechoDefault: 2.5,
factorZona: { Madrid: 1.1 },
manoObra: { demolicion: 1500, fontaneria: 1200, electricidad: 1000, mano_de_obra: 3000 },
manoObra: { demolicion: 1500, impermeabilizacion: 0, fontaneria: 1200, electricidad: 1000, mano_de_obra: 3000 },
};
const catalog: CatalogItem[] = [
@@ -102,3 +102,50 @@ describe('computeBudget', () => {
expect(r.total).toBeGreaterThan(0);
});
});
// Config con impermeabilización y extras fijos activos, sin factor de zona (total = subtotal).
const configFull: PricingConfig = {
alturaTechoDefault: 2.5,
factorZona: {},
manoObra: { demolicion: 1500, impermeabilizacion: 2000, fontaneria: 1200, electricidad: 1000, mano_de_obra: 3000 },
extras: { tuberias: 100000, boletin: 15000, distribucion: 80000 },
};
describe('computeBudget — impermeabilización y extras fijos', () => {
it('añade impermeabilización en zonas húmedas (baño) proporcional al suelo', () => {
const r = computeBudget(inputs({ tipoReforma: 'bano', m2Suelo: 5, provincia: null }), configFull, catalog);
const byKey = Object.fromEntries(r.partidas.map((p) => [p.key, p.importe]));
expect(byKey.impermeabilizacion).toBe(10000); // 5 * 2000
});
it('no añade impermeabilización en zonas secas (salón)', () => {
const r = computeBudget(inputs({ tipoReforma: 'salon', provincia: null }), configFull, catalog);
const byKey = Object.fromEntries(r.partidas.map((p) => [p.key, p.importe]));
expect(byKey.impermeabilizacion).toBeUndefined();
});
it('aplica siempre el boletín eléctrico como extra fijo', () => {
const r = computeBudget(inputs({ provincia: null }), configFull, catalog);
const byKey = Object.fromEntries(r.partidas.map((p) => [p.key, p.importe]));
expect(byKey.extras_fijos).toBe(15000); // solo boletín
});
it('suma tuberías y distribución según los inputs del piso', () => {
const r = computeBudget(
inputs({ provincia: null, anteriorA2000: true, cambioDistribucion: true }),
configFull,
catalog,
);
const byKey = Object.fromEntries(r.partidas.map((p) => [p.key, p.importe]));
expect(byKey.extras_fijos).toBe(195000); // 15000 + 100000 + 80000
});
it('la intensidad por tipo reduce la fontanería de un integral frente a una cocina', () => {
const cocina = computeBudget(inputs({ tipoReforma: 'cocina', m2Suelo: 16, provincia: null }), configFull, catalog);
const integral = computeBudget(inputs({ tipoReforma: 'integral', m2Suelo: 16, provincia: null }), configFull, catalog);
const fontCocina = cocina.partidas.find((p) => p.key === 'fontaneria')?.importe ?? 0;
const fontIntegral = integral.partidas.find((p) => p.key === 'fontaneria')?.importe ?? 0;
expect(fontCocina).toBe(19200); // 16 * 1200 * 1.0
expect(fontIntegral).toBe(8640); // 16 * 1200 * 0.45
});
});

View File

@@ -5,7 +5,7 @@ import type { BudgetInputs, PricingConfig } from '@/budget/types';
const config: PricingConfig = {
alturaTechoDefault: 2.5,
factorZona: {},
manoObra: { demolicion: 0, fontaneria: 0, electricidad: 0, mano_de_obra: 0 },
manoObra: { demolicion: 0, impermeabilizacion: 0, fontaneria: 0, electricidad: 0, mano_de_obra: 0 },
};
function inputs(partial: Partial<BudgetInputs>): BudgetInputs {