Files
reformix-hackaton/mvp/b2c/tests/budget/compute.test.ts

105 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>): 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);
});
});