From b27b68908c8f6964b9ddfd96ef6803152d45940d Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Sat, 30 May 2026 12:18:48 +0200 Subject: [PATCH] feat: derive cantidades from minimal measurements Co-Authored-By: Claude Sonnet 4.6 --- mvp/b2c/src/budget/derive.ts | 39 ++++++++++++++++++++++ mvp/b2c/tests/budget/derive.test.ts | 50 +++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 mvp/b2c/src/budget/derive.ts create mode 100644 mvp/b2c/tests/budget/derive.test.ts diff --git a/mvp/b2c/src/budget/derive.ts b/mvp/b2c/src/budget/derive.ts new file mode 100644 index 0000000..0cdc97b --- /dev/null +++ b/mvp/b2c/src/budget/derive.ts @@ -0,0 +1,39 @@ +import type { BudgetInputs, PricingConfig, TipoReforma } from './types'; + +export interface Cantidades { + m2Suelo: number; + m2Pared: number; + mlMobiliario: number; + perimetro: number; + alturaTecho: number; +} + +const M2_MEDIANA: Record = { + cocina: 10, + bano: 5, + salon: 20, + comedor: 16, + integral: 70, + otro: 12, +}; + +// Metros lineales de mobiliario por metro de perímetro. Solo cocina/baño. +const FACTOR_MOBILIARIO: Partial> = { + cocina: 0.5, + bano: 0.3, +}; + +export function deriveCantidades(inputs: BudgetInputs, config: PricingConfig): Cantidades { + const m2Suelo = + inputs.m2Suelo != null && inputs.m2Suelo > 0 + ? inputs.m2Suelo + : M2_MEDIANA[inputs.tipoReforma]; + const alturaTecho = + inputs.alturaTecho != null && inputs.alturaTecho > 0 + ? inputs.alturaTecho + : config.alturaTechoDefault; + const perimetro = 4 * Math.sqrt(m2Suelo); + const m2Pared = perimetro * alturaTecho; + const mlMobiliario = perimetro * (FACTOR_MOBILIARIO[inputs.tipoReforma] ?? 0); + return { m2Suelo, m2Pared, mlMobiliario, perimetro, alturaTecho }; +} diff --git a/mvp/b2c/tests/budget/derive.test.ts b/mvp/b2c/tests/budget/derive.test.ts new file mode 100644 index 0000000..f5efffd --- /dev/null +++ b/mvp/b2c/tests/budget/derive.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { deriveCantidades } from '@/budget/derive'; +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 }, +}; + +function inputs(partial: Partial): BudgetInputs { + return { + tipoReforma: 'cocina', + m2Suelo: null, + alturaTecho: null, + calidadGlobal: 'media', + estructural: false, + provincia: null, + materialSelections: {}, + ...partial, + }; +} + +describe('deriveCantidades', () => { + it('usa m² aportados y deriva perímetro y paredes con números limpios', () => { + // m2Suelo=16 -> sqrt=4 -> perimetro=16 -> pared=16*2.5=40 -> mobiliario=16*0.5=8 (cocina) + const c = deriveCantidades(inputs({ m2Suelo: 16 }), config); + expect(c.m2Suelo).toBe(16); + expect(c.perimetro).toBe(16); + expect(c.m2Pared).toBe(40); + expect(c.mlMobiliario).toBe(8); + expect(c.alturaTecho).toBe(2.5); + }); + + it('cae a la mediana por tipo cuando no hay m²', () => { + const c = deriveCantidades(inputs({ tipoReforma: 'bano', m2Suelo: null }), config); + expect(c.m2Suelo).toBe(5); // mediana baño + }); + + it('usa la altura aportada por encima del default', () => { + const c = deriveCantidades(inputs({ m2Suelo: 16, alturaTecho: 3 }), config); + expect(c.alturaTecho).toBe(3); + expect(c.m2Pared).toBe(48); // 16 * 3 + }); + + it('no calcula mobiliario para tipos sin cocina/baño', () => { + const c = deriveCantidades(inputs({ tipoReforma: 'salon', m2Suelo: 16 }), config); + expect(c.mlMobiliario).toBe(0); + }); +});