feat: derive cantidades from minimal measurements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-05-30 12:18:48 +02:00
parent 9b14dbfac5
commit b27b68908c
2 changed files with 89 additions and 0 deletions

View File

@@ -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<TipoReforma, number> = {
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<Record<TipoReforma, number>> = {
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 };
}

View File

@@ -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>): 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);
});
});