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>
This commit is contained in:
137
docs/superpowers/specs/2026-05-30-motor-presupuesto-design.md
Normal file
137
docs/superpowers/specs/2026-05-30-motor-presupuesto-design.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# 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 300–1.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.
|
||||||
Reference in New Issue
Block a user