import { describe, it, expect } from 'vitest'; import { computeBudget } from '@/budget/compute'; 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 }, }; const catalog: CatalogItem[] = [ { id: 'suelo-m', categoria: 'suelo', nombre: 'Cerámico', calidad: 'media', precioUnit: 2800, unidad: 'm2', descriptorRender: 'suelo cerámico gris', esDefault: true, sku: 'SUE-M' }, { id: 'pared-m', categoria: 'pared', nombre: 'Azulejo', calidad: 'media', precioUnit: 2400, unidad: 'm2', descriptorRender: 'azulejo blanco', esDefault: true, sku: 'PAR-M' }, { id: 'pintura-m', categoria: 'pintura', nombre: 'Plástica', calidad: 'media', precioUnit: 800, unidad: 'm2', descriptorRender: 'pintura blanca mate', esDefault: true, sku: 'PIN-M' }, { id: 'mob-m', categoria: 'mobiliario', nombre: 'Muebles cocina', calidad: 'media', precioUnit: 32000, unidad: 'ml', descriptorRender: 'muebles laminado roble', esDefault: true, sku: 'MOB-M' }, { id: 'suelo-p', categoria: 'suelo', nombre: 'Porcelánico', calidad: 'premium', precioUnit: 4500, unidad: 'm2', descriptorRender: 'porcelánico símil roble', esDefault: true, sku: 'SUE-P' }, ]; function inputs(partial: Partial): BudgetInputs { return { tipoReforma: 'cocina', m2Suelo: 16, alturaTecho: null, calidadGlobal: 'media', estructural: false, provincia: 'Madrid', materialSelections: {}, ...partial, }; } describe('computeBudget', () => { it('calcula partidas, subtotal, factor zona y total con números conocidos', () => { const r = computeBudget(inputs({}), config, catalog); const byKey = Object.fromEntries(r.partidas.map((p) => [p.key, p.importe])); expect(byKey.demolicion).toBe(24000); // 16*1500 expect(byKey.alicatado).toBe(140800); // 16*2800 + 40*2400 expect(byKey.fontaneria).toBe(19200); // 16*1200 expect(byKey.electricidad).toBe(16000); // 16*1000 expect(byKey.carpinteria).toBe(256000); // 8*32000 expect(byKey.mano_de_obra).toBe(48000); // 16*3000 expect(byKey.extras).toBe(32000); // 40*800 expect(byKey.licencia).toBeUndefined(); expect(r.subtotal).toBe(536000); expect(r.factorZona).toBe(1.1); expect(r.total).toBe(589600); // round(536000 * 1.1) }); it('confianza media (±15%) con m² pero sin selección exacta', () => { const r = computeBudget(inputs({}), config, catalog); expect(r.confianza).toBe('media'); expect(r.rango.min).toBe(501160); // round(589600*0.85) expect(r.rango.max).toBe(678040); // round(589600*1.15) }); it('confianza alta (±10%) con m² y selección exacta', () => { const r = computeBudget(inputs({ materialSelections: { suelo: 'suelo-p' } }), config, catalog); expect(r.confianza).toBe('alta'); expect(r.materialesRender).toContain('porcelánico símil roble'); }); it('confianza baja (±25%) sin m² ni selección', () => { const r = computeBudget(inputs({ m2Suelo: null }), config, catalog); expect(r.confianza).toBe('baja'); }); it('añade partida de licencia y amplía el máximo si hay cambio estructural', () => { const r = computeBudget(inputs({ estructural: true }), config, catalog); const byKey = Object.fromEntries(r.partidas.map((p) => [p.key, p.importe])); expect(byKey.licencia).toBe(30000); // 300€ mínimo expect(r.rango.max).toBe(835990); // ver nota del plan: subtotal+30000, ×1.1, banda ±15%, + (LICENCIA_MAX - LICENCIA_MIN) }); it('emite aviso cuando falta precio de una categoría', () => { const sinPintura = catalog.filter((c) => c.categoria !== 'pintura'); const r = computeBudget(inputs({}), config, sinPintura); expect(r.avisos.some((a) => a.includes('pintura'))).toBe(true); }); it('usa factor zona 1 para provincia desconocida', () => { const r = computeBudget(inputs({ provincia: 'Cuenca' }), config, catalog); expect(r.factorZona).toBe(1); expect(r.total).toBe(536000); // subtotal sin factor }); it('respeta un factor zona explícito de 0', () => { const configZero: PricingConfig = { ...config, factorZona: { ...config.factorZona, Madrid: 0 } }; const r = computeBudget(inputs({ provincia: 'Madrid' }), configZero, catalog); expect(r.factorZona).toBe(0); expect(r.total).toBe(0); }); it('no sube la confianza si la selección apunta a un material inexistente', () => { const r = computeBudget(inputs({ materialSelections: { suelo: 'no-existe' } }), config, catalog); expect(r.confianza).toBe('media'); // tiene m² pero la selección no resuelve -> no es alta }); it('omite carpintería y no rompe para tipos sin mobiliario (salón)', () => { const r = computeBudget(inputs({ tipoReforma: 'salon' }), config, catalog); const byKey = Object.fromEntries(r.partidas.map((p) => [p.key, p.importe])); expect(byKey.carpinteria).toBeUndefined(); expect(r.total).toBeGreaterThan(0); }); });