import { deriveCantidades } from './derive'; import { resolvePrecioUnitario } from './resolve'; import { PARTIDA_LABEL, PARTIDA_ORDER } from './labels'; import type { BudgetInputs, BudgetResult, CategoriaMaterial, CatalogItem, PartidaKey, PricingConfig, TipoReforma, } from './types'; const LICENCIA_MIN = 30000; // 300 € const LICENCIA_MAX = 150000; // 1.500 € // Zonas húmedas: las únicas que llevan impermeabilización. const WET = new Set(['cocina', 'bano', 'integral']); // Intensidad de instalaciones (fontanería/electricidad) por m² según el tipo de reforma. // Baseline cocina = 1.0. Un baño concentra más instalaciones por m²; un salón o un piso // integral las diluye. Corrige el sesgo del modelo lineal €/m² sin rehacerlo. const TIPO_INTENSIDAD: Record = { cocina: 1.0, bano: 1.3, integral: 0.45, salon: 0.4, comedor: 0.4, otro: 0.7, }; // A qué partida contribuye el material de cada categoría. const MATERIAL_PARTIDA: Record = { suelo: 'alicatado', pared: 'alicatado', pintura: 'extras', mobiliario: 'carpinteria', }; const CATEGORIAS: CategoriaMaterial[] = ['suelo', 'pared', 'pintura', 'mobiliario']; export function computeBudget( inputs: BudgetInputs, config: PricingConfig, catalog: CatalogItem[], ): BudgetResult { const cant = deriveCantidades(inputs, config); const avisos: string[] = []; const materialesRender: string[] = []; const importes: Record = { demolicion: 0, impermeabilizacion: 0, alicatado: 0, fontaneria: 0, electricidad: 0, carpinteria: 0, mano_de_obra: 0, extras: 0, extras_fijos: 0, licencia: 0, }; const cantidadPorCategoria: Record = { suelo: cant.m2Suelo, pared: cant.m2Pared, pintura: cant.m2Pared, mobiliario: cant.mlMobiliario, }; for (const categoria of CATEGORIAS) { const cantidad = cantidadPorCategoria[categoria]; if (cantidad <= 0) continue; const { item } = resolvePrecioUnitario( categoria, inputs.calidadGlobal, catalog, inputs.materialSelections, ); if (!item) { avisos.push(`Sin precio para ${categoria} (calidad ${inputs.calidadGlobal})`); continue; } importes[MATERIAL_PARTIDA[categoria]] += Math.round(cantidad * item.precioUnit); if (item.descriptorRender) materialesRender.push(item.descriptorRender); } const intensidad = TIPO_INTENSIDAD[inputs.tipoReforma] ?? 1; importes.demolicion += Math.round(cant.m2Suelo * config.manoObra.demolicion); importes.fontaneria += Math.round(cant.m2Suelo * config.manoObra.fontaneria * intensidad); importes.electricidad += Math.round(cant.m2Suelo * config.manoObra.electricidad * intensidad); importes.mano_de_obra += Math.round(cant.m2Suelo * config.manoObra.mano_de_obra); // Impermeabilización: solo en zonas húmedas, proporcional al suelo a tratar. if (WET.has(inputs.tipoReforma)) { importes.impermeabilizacion += Math.round(cant.m2Suelo * config.manoObra.impermeabilizacion); } // Extras fijos (no escalan con m²). El boletín eléctrico es siempre obligatorio. const extras = config.extras; if (extras) { importes.extras_fijos += extras.boletin; if (inputs.anteriorA2000) importes.extras_fijos += extras.tuberias; if (inputs.cambioDistribucion) importes.extras_fijos += extras.distribucion; } if (inputs.estructural) importes.licencia += LICENCIA_MIN; const partidas = PARTIDA_ORDER.filter((k) => importes[k] > 0).map((k) => ({ key: k, label: PARTIDA_LABEL[k], importe: importes[k], })); const subtotal = partidas.reduce((s, p) => s + p.importe, 0); const factorZona = config.factorZona[inputs.provincia ?? ''] ?? 1; const total = Math.round(subtotal * factorZona); const hasExact = (Object.values(inputs.materialSelections) as string[]).some( (id) => catalog.some((c) => c.id === id), ); const hasM2 = inputs.m2Suelo != null && inputs.m2Suelo > 0; let confianza: BudgetResult['confianza']; let band: number; if (hasM2 && hasExact) { confianza = 'alta'; band = 0.1; } else if (hasM2 || hasExact) { confianza = 'media'; band = 0.15; } else { confianza = 'baja'; band = 0.25; } const rango = { min: Math.round(total * (1 - band)), max: Math.round(total * (1 + band)) + (inputs.estructural ? LICENCIA_MAX - LICENCIA_MIN : 0), }; return { partidas, subtotal, factorZona, total, rango, confianza, materialesRender, avisos }; }