Files
reformix-hackaton/docs/superpowers/specs/2026-05-30-motor-presupuesto-design.md
Carlos Narro bd07586b03 Add motor de presupuesto design spec
Diseño validado del motor de presupuesto: modelo híbrido partidas←precios
unitarios, medidas mínimas (m² suelo + supuestos), calidad B/M/P + catálogo
importable por CSV, y progressive disclosure de personalización en el funnel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 08:27:06 +02:00

138 lines
7.3 KiB
Markdown
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.
# Motor de presupuesto Reformix — Diseño
> Fecha: 2026-05-30 · Estado: aprobado (brainstorming) · Owner dominio: Goyo · Coord: Carlos
> Alcance: F2 (en sprint actual). El configurador multi-tenant real sigue siendo F1.5.
## Objetivo
Producir un presupuesto orientativo desglosado por partidas a partir de datos que
escalan "de menos a más": con el mínimo (tipo de reforma + calidad) ya sale un número,
y cada etapa del funnel (medidas, material exacto, llamada) lo afina. El reformista
define los precios en el panel; el catálogo se puede sembrar y actualizar por CSV.
Requisitos cubiertos: RF-C-21 (desglose por partidas + factor zona), RF-C-22 (licencia
si hay cambios estructurales), RF-D-07 (tabla de precios editable), RF-B-07 (DIN-A4 como
input de medidas, *fallback* stubbeable), RF-B-09 (disclaimer orientativo),
RNF-MAINT-01 (≥70% cobertura en `src/budget/*`).
## Decisiones de diseño (validadas con el usuario)
1. **Modelo híbrido partidas ← precios unitarios.** El reformista configura precios
unitarios (€/m² suelo por calidad, €/m² pared, €/m² pintura, €/ml mobiliario, mano
de obra). El motor calcula cantidades desde las medidas y agrupa el resultado en las
partidas de RF-C-21.
2. **Medidas mínimas = m² de suelo + supuestos.** El resto se deriva: perímetro ≈ 4·√(m²),
m² pared = perímetro × altura (2,5 m por defecto). Si no hay m², mediana por tipo.
El refinamiento con dimensiones reales (largo×ancho×alto) o DIN-A4 es posterior/opcional.
3. **Calidad = columna de precio por material (B/M/P)** más un **catálogo de materiales**
con precio e identidad propia, importable por CSV. El cliente elige una calidad global
por defecto; puede personalizar material exacto si quiere.
4. **Progressive disclosure.** Se fomenta lo básico (solo calidad). La personalización
(material exacto del catálogo) aparece sutil y opcional en el funnel, y el agente la
afina en la llamada. El material elegido alimenta el prompt del render para que la
imagen refleje exactamente lo presupuestado.
## Arquitectura
```
PANEL (CRUD) pricing_config + catalog_items (por tenant)
reformista · precios unitarios B/M/P por material
+ CSV import · mano de obra, factor zona, partidas
│ (lee de DB)
FUNNEL (cliente)
inputs por etapa ─► computeBudget(config, catalog, inputs) ─► BudgetResult
· m² suelo, calidad [ función PURA en src/budget/ ] · partidas[]
· (opc.) material exacto · subtotal, total
· (llamada) estructural · rango + confianza
· materiales→render
```
- **`src/budget/` (núcleo puro):** tipos, `computeBudget()`, derivación de cantidades,
agrupación en partidas, partida condicional de licencia, cálculo de rango+confianza.
Sin imports de DB ni de red. Es el módulo con cobertura ≥70% (RNF-MAINT-01).
- **DB (Drizzle):** extiende el schema existente (`src/db/schema.ts`).
- **Panel:** CRUD sobre config/catálogo + importador CSV. Tenant único "Reformas Ejemplo".
- **Funnel:** recoge inputs mínimos, llama al engine, persiste snapshot por etapa.
## Flujo de cálculo `computeBudget(config, catalog, inputs)`
1. **Cantidades** (con degradación):
- `m² suelo` aportado; si no, mediana por tipo (cocina 10, baño 5, salón 20, integral 70).
- `perímetro ≈ 4·√(m² suelo)`; `m² pared = perímetro × alturaTecho` (default 2,5 m).
- `ml mobiliario ≈ perímetro × factor_tipo` (solo cocina/baño).
2. **Precio unitario** por material: ítem exacto elegido del catálogo si existe; si no,
ítem `esDefault` de la calidad global.
3. **Partidas** (RF-C-21): cada partida = Σ(cantidad × precio unitario material) + mano de
obra. Categorías: demolición, alicatado, fontanería, electricidad, carpintería, mano de
obra, extras.
4. **Factor zona:** multiplicador por provincia (configurable) sobre el subtotal.
5. **Licencia** (RF-C-22): si `inputs.estructural = true`, partida "Licencia + Proyecto
técnico" con rango 3001.500 €.
6. **Rango + confianza:** total como `{ min, max, confianza }`. Menos datos → banda más
ancha (≈ ±25% solo con calidad; ≈ ±10% tras llamada con material exacto + estructural
confirmado).
## Modelo de datos (extensión Drizzle)
```
pricing_config (1 por tenant)
tenantId, alturaTechoDefault, factorZona (jsonb provincia→mult),
manoObra (jsonb partida→€), updatedAt
catalog_items (N por tenant)
id, tenantId, categoria (suelo|pared|pintura|mobiliario|...),
nombre, calidad (basica|media|premium), precioUnit (cents),
unidad (m2|ml|ud), descriptorRender (text), esDefault (bool), sku
leads (campos nuevos)
m2Suelo, alturaTecho, calidadGlobal, estructural,
materialSelections (jsonb categoria→catalogItemId),
desgloseSnapshot (jsonb: partidas+rango por pipeline_stage)
```
- Dinero en **enteros (cents)**, consistente con el schema actual.
- `desgloseSnapshot` guarda el resultado **en cada `pipeline_stage`** → permite analizar
cómo evoluciona la estimación lead a lead.
- `descriptorRender` de cada material se inyecta en el prompt del render.
## Panel del reformista (`/panel/precios`)
- **Catálogo editable:** tabla de `catalog_items` por categoría, precio por calidad inline;
crear/editar/borrar; marcar `esDefault` por calidad.
- **Importar CSV:** subida → parse con zod → preview filas válidas/erróneas → confirmar.
Cabeceras: `categoria,nombre,calidad,precio,unidad,descriptor_render,sku`. *Upsert* por
`sku`. Si hay errores de validación, se escriben **cero** filas.
- **Config general:** factor zona por provincia, mano de obra por partida, altura techo.
## Funnel (cliente) — progressive disclosure
- Por defecto: **tipo de reforma + calidad (B/M/P)** y, opcional, m² de suelo → estimación
+ render genérico de la calidad.
- Afordance sutil **"Personalizar materiales"** (colapsado): galería del catálogo para
elegir ítems exactos. Quien no la toca, usa el default.
- La **llamada** enriquece inputs (material exacto, estructural, medidas finas) → recálculo
+ render exacto.
## Seed (demo 11-jun-2026)
Sembrar `pricing_config` + un **catálogo demo** (suelos, alicatados, pinturas, mobiliario
en B/M/P con `descriptorRender`) para que la demo funcione sin cargar CSV.
## Fuera de alcance (ahora)
- Recálculo retroactivo de presupuestos ya guardados (los snapshots no se tocan).
- Calidad por elemento mezclada como input por defecto (refinamiento F1.5; el catálogo ya
lo permite a nivel de selección manual).
- Extracción real de medidas por visión/DIN-A4: se deja **stub** (`has_din_a4` flag +
hook de medidas) y se usa la degradación por medianas mientras tanto.
- Multi-tenant real (F1.5): todo opera sobre "Reformas Ejemplo".
- Dimensiones largo×ancho×alto como input mínimo (queda como refinamiento opcional).
## Verificación
- Tests unitarios de `computeBudget` con inputs conocidos: desglose ±1 € vs cálculo manual
(RF-C-21), partida de licencia presente con `estructural=true` (RF-C-22), degradación sin
m², estrechamiento del rango al añadir datos. Cobertura `src/budget/*` ≥70% (RNF-MAINT-01).
- Test de parser CSV: filas válidas/erróneas, upsert por sku, cero escrituras si hay error.