From bd07586b03e3c2386fb1965b86e0ff147f157790 Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Sat, 30 May 2026 08:27:06 +0200 Subject: [PATCH] Add motor de presupuesto design spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../2026-05-30-motor-presupuesto-design.md | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-30-motor-presupuesto-design.md diff --git a/docs/superpowers/specs/2026-05-30-motor-presupuesto-design.md b/docs/superpowers/specs/2026-05-30-motor-presupuesto-design.md new file mode 100644 index 0000000..4d864fe --- /dev/null +++ b/docs/superpowers/specs/2026-05-30-motor-presupuesto-design.md @@ -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.