# Fermenty ERP — Mejoras estructurales y funcionales

> Hoja de ruta para terminar la parte web (ERP) **antes** de retomar API/app móvil.
> Principio rector: toda la lógica de dominio vive en *services* (no en controladores/Blade)
> para que la futura API y la app Flutter la reutilicen. Última actualización: **2026-06-18**.

Relacionado: `00_PROJECT_OVERVIEW.md`, `TAREAS_PRIORIZADAS.md`, `MOBILE_API_PLANNING.md`.

---

## Visión

Que el nuevo usuario sienta que entra en una comunidad, con una bienvenida cálida y un
espacio cuidado. Tras estructurar todo: una bienvenida + una **introducción paso a paso**
(tour) que explique el uso de la app y que se pueda relanzar desde el área privada.

---

## Estado y orden de ejecución

| # | Mejora | Tipo | Depende de | Plan | Estado |
|---|--------|------|-----------|------|--------|
| 6 | Límite Free: 2 fermentos activos → upsell Premium | Quick win | — | Free/Premium | ✅ hecho (2026-06-12) |
| — | Bienvenida + tour de introducción relanzable | UX | diseño | — | ⏳ andamiaje |
| 1 | Sistema de unidades US/EU (lb/kg, L/gal, °C/°F) | Fundacional | — | Free | ⏳ siguiente |
| 3 | **Receta base por guía + escalado por cantidad total** | Estructural | 1 | Free (core) | 🔵 en diseño (este doc) |
| 2 | ASISTENTE de preparación (paso a paso, mobile-first) | UX | 3 | Free | pendiente |
| 3+| Botones rápidos de log (PH/temp/acidez/nota) en preparación | UX | 1 | Free | pendiente |
| 4 | Calendario / planificador (cronología visual) | Visualización | — | Free | pendiente |
| 5 | Sincronización con Google Calendar (opt-in) | Integración | 4 | **Premium** | pendiente |
| — | Herramientas: calc. alcohol, tiempos por Tª, BRIX→densidad/alcohol | Futuro | 1 | Premium | futuro |
| — | Cursos / talleres videoguiados | Futuro | — | Premium | futuro |

**Secuencia:** (6) ✅ → bienvenida/tour (andamiaje) → (1) unidades → (3) receta base + escalado →
(2) asistente + (3+) logs rápidos → (4) calendario → (5) Google Calendar → herramientas/cursos.

---

## #6 — Límite de plan Free *(hecho)*

- `User::FREE_ACTIVE_BATCH_LIMIT = 2`, `User::activeBatchLimit()` (null = ilimitado/premium),
  `User::hasReachedActiveBatchLimit()`.
- Se aplica en `BatchController@create` y `@store` → redirige a `premium` con flash
  `batches.limit.free_reached`.
- La regla está en el modelo `User` para que la futura API la reutilice (solo cambia la
  respuesta: redirect web vs 403 JSON).

---

## #1 — Sistema de unidades (fundacional)

**Principio: almacenar siempre en métrico canónico (g, kg, ml, L, °C) y convertir en el borde**
(entrada/salida) según la preferencia del usuario. Esto evita ambigüedad de datos y hace
trivial el escalado (#3), que se calcula siempre en métrico.

- Nueva columna `users.unit_system` ENUM(`metric`,`imperial`) default `metric`
  (opcional: granular `temp_unit`, `volume_unit`, `weight_unit` si se quiere mezclar).
- Nuevo `app/Services/UnitService.php`:
  - `toDisplay($value, $unit, User $u)` → convierte + formatea.
  - `toCanonical($value, $unit)` → al guardar entradas en imperial.
  - Conversiones: volumen (ml/L ↔ fl oz/cup/gal), peso (g/kg ↔ oz/lb), temperatura (°C ↔ °F).
- Helper/Blade directive para mostrar magnitudes (`@qty`, `@temp`) y selector en perfil.
- Afecta a: vista de receta/ingredientes, alta de mediciones (#3+), historial, herramientas.

> `BatchIngredient.unit` y `Measurement.unit` ya existen como string libre; el cambio es
> normalizar a unidades canónicas + capa de conversión, no romper el esquema.

---

## #3 — Receta base por guía + escalado *(diseño en profundidad)*

### Decisión del producto (confirmada)
- **Cada guía tiene UNA receta base** con valores en **métrico EU** (L, kg, g, °C).
- Más adelante: **recetas variantes** para el mismo tipo de fermento y **recetas de usuario**.
- Al iniciar un lote, el usuario elige la **cantidad total** (L o kg) → se **escalan** los
  ingredientes proporcionalmente; el ASISTENTE muestra los valores reales por paso.

### Situación actual (importante)
- `FermentationGuide → GuideStep (min_day,max_day,title,description) → StepAction (texto) + GuideStepHint`.
- **Las guías NO tienen ingredientes** (solo texto descriptivo en `StepAction`).
- `BatchService::create()` copia pasos + genera notificaciones, pero **nunca crea `batch_ingredients`**.
- `Recipe.ingredients` es **JSON libre** (sin forma garantizada) → no sirve para escalar tal cual.

→ Falta una **receta base estructurada con un rendimiento de referencia** desde la que escalar.

### Modelo conceptual
- **Receta base**: lista de ingredientes + rendimiento de referencia, ligada a la **guía**.
- **Receta variante**: ingredientes alternativos para el mismo `ferment_type` (oficial o de usuario).
- **Lote**: se crea desde la receta base de la guía *o* desde una receta elegida; en la creación
  se **resuelven y escalan** los ingredientes a la cantidad objetivo y se **congelan** en el lote.

### Esquema propuesto (aditivo, sin pérdida de datos)

**1) `fermentation_guides` (+ columnas)** — rendimiento de referencia de la receta base:
```
base_yield_value  DECIMAL(8,3)   -- p.ej. 2.000
base_yield_unit   STRING         -- 'L' (líquidos) o 'kg' (sólidos: chucrut, kimchi)
```

**2) `guide_ingredients` (tabla nueva)** — las líneas de la receta base, en métrico:
```
id
guide_id         FK fermentation_guides
guide_step_id    FK guide_steps  NULL   -- (opc.) en qué paso se usa → "usa X ahora"
name             STRING
quantity         DECIMAL(10,3)          -- cantidad canónica (métrico)
unit             STRING                 -- 'g','kg','ml','L','unit'...
phase            STRING NULL            -- 'primary','flavoring','bottling' (agrupación)
is_optional      BOOL default false
order            INT
notes            STRING NULL
```
*Por qué tabla y no JSON:* edición en el admin, validación, y consulta por paso para el asistente.
(El JSON queda bien para recetas de usuario; aquí queremos algo editable y consultable.)

**3) `batches` (+ columnas)** — cantidad objetivo elegida por el usuario:
```
target_yield_value  DECIMAL(8,3) NULL
target_yield_unit   STRING NULL
```

**4) `batch_ingredients` (+ columna opcional)** — ya existe (`name,quantity,unit,phase,notes`).
Añadir para trazar origen y vincular al paso del asistente:
```
guide_ingredient_id  FK guide_ingredients NULL
guide_step_id        FK guide_steps NULL
```

**5) `recipes` (variantes y de usuario)** — mantener tabla; **estandarizar** `ingredients` JSON
a la misma forma de línea que `guide_ingredients` y añadir rendimiento:
```
base_yield_value, base_yield_unit   -- para escalar variantes igual que la base
```
`user_id NULL` = receta oficial/variante; `user_id` presente = receta de usuario (Premium, tarea #7).

### Lógica de escalado — `app/Services/ScalingService.php`
```
factor = target_total_metric / base_yield_value        (todo en métrico)
por ingrediente:  scaled = round(quantity * factor, precisión_por_unidad)
```
- En `BatchService::create()`: tras copiar pasos, **resolver ingredientes** (de la guía o receta),
  escalar y **snapshotear** en `batch_ingredients` con su `guide_step_id`.
- El lote guarda su propia copia escalada → inmune a cambios posteriores de la guía.

### Vínculo ingrediente ↔ paso (para "valores reales por paso")
- Preferido: `guide_ingredients.guide_step_id` (null = ingrediente general, se muestra al inicio).
- `phase` como agrupación/etiqueta complementaria.

### Impacto en el admin
- Editor de guías (`Admin\GuideController` / `GuideStepsController`) necesita una sección para
  gestionar la **receta base** (ingredientes + `base_yield`). Es trabajo de UI de admin nuevo.

### Migración y backfill
- Todo aditivo → migraciones seguras. Las guías existentes quedan **sin receta base** hasta que
  el admin la rellene; los lotes antiguos siguen sin `batch_ingredients` (como hoy). Sin pérdidas.

---

## #2 — ASISTENTE de preparación

- Backend listo: `BatchStep` por lote + `BatchService::advanceStep()` (completar y activar siguiente).
- El asistente es UI enfocada: **una pantalla por paso** (mobile-first incluso en web, para
  pre-validar la UX de la app), con confirmación y fondo diferenciado.
- Muestra, por paso: acciones (`StepAction`), pistas (`GuideStepHint`, algunas Premium) y los
  **ingredientes escalados** del paso (depende de #3).

## #3+ — Botones rápidos de log

- En la pantalla de preparación: botones **PH / Temperatura / Acidez / Nota** abren un modal que
  crea un `DailyLog` + filas `Measurement` (`type`,`value`,`unit`). "Acidez" = nuevo `type`.
- Unidades según #1 (°C/°F). Encaja con `BatchLogController` existente.

## #4 — Calendario / planificador

- Datos ya disponibles: `GuideStep.min_day/max_day` (ventanas por paso) + `FermentyNotification.scheduled_at`
  + `BatchEvent`. La cronología es sobre todo **visualización** (sin cambios de esquema).
- Converge con tareas abiertas **#3 (rutinas de notificación)** y **#6 (preferencias)** de `TAREAS_PRIORIZADAS.md`.

## #5 — Google Calendar (Premium)

- Ya se guardan tokens de Google (`user_social_providers`, columnas de token ampliadas), pero el
  login solo pide scope básico → hace falta **scope adicional de Calendar + re-consentimiento +
  refresco de token + manejo de errores**. Crear eventos para pasos futuros; opt-in en perfil.

---

## Mapa Free vs Premium (propuesta)

| Capacidad | Free | Premium |
|---|---|---|
| Fermentos activos a la vez | 2 | ilimitado |
| Receta base + escalado por cantidad | ✅ | ✅ |
| Asistente de preparación | ✅ | ✅ |
| Logs/mediciones básicos | ✅ | ✅ |
| Pistas avanzadas por paso | — | ✅ |
| Recetas propias / variantes | — | ✅ (tarea #7) |
| Sincronización Google Calendar | — | ✅ |
| Herramientas avanzadas (calculadoras) | — | ✅ |
| Cursos/talleres | — | ✅ |

---

## Decisiones abiertas (para confirmar antes de migrar esquema)

1. **Vínculo ingrediente↔paso:** ¿`guide_step_id` directo en `guide_ingredients` (recomendado) o
   solo agrupar por `phase`?
2. **Recetas de usuario:** ¿solo Premium (alinea con tarea #7)? ¿Y las variantes oficiales se
   abordan ahora o se difieren (de momento solo "receta base por guía")?
3. **Unidades:** ¿`unit_system` simple (metric/imperial) o granular (temp/volumen/peso por separado)?
4. **Redondeo del escalado:** reglas prácticas por unidad (p.ej. agua a 0,1 L, sal a 1 g) para
   que los valores escalados sean usables.
5. **`base_yield_unit`:** confirmar L para líquidos (kombucha, kéfir) y kg para sólidos
   (chucrut, kimchi) — el campo lo soporta, pero conviene fijar la convención por tipo.

---

## Refinamientos de lotes, notificaciones y progreso *(2026-06-18)*

**Política de notificaciones.** Solo se avisa (email/push) por **alertas, tareas pendientes/vencidas
o eventos automáticos** — nunca por acciones que el propio usuario acaba de hacer.
- Se **elimina el aviso «¡Lote iniciado!»** (regla `step`/`{step:1}` quitada del seeder + migración
  `2026_06_18_120000` que borra reglas y notificaciones no enviadas). Sus copias duplicadas
  (seeder sin índice único + `insertOrIgnore`) eran la causa del **email de inicio duplicado**.
  El evento `start` en la cronología (solo in-app) se mantiene.
- **Lote en pausa >3 días:** `NotificationService::createPausedReminderNotifications()` (corre en
  `notifications:check-batches`, diaria), anclado al último evento `paused`, cooldown semanal.
- **Resumen por email al finalizar con buena nota (≥4★):** `BatchSummaryMail` + `emails.batch-summary`
  (datos + evaluación + perfil de sabor + estadísticas + cronología), disparado en
  `BatchReviewController@store`. No en descartes ni pausas. Visible en `/dev/mail` (local).
- Nota: el push aún no se entrega; solo `processPendingEmailNotifications` envía (email).

**Catálogo de fermentos (`/fermentos`).** Las tarjetas usan **stretched-link** (no `<a>` envolvente)
para poder llevar enlaces dentro. Para el usuario autenticado, cada tarjeta muestra su lote en
curso de ese tipo: **«En curso: {nombre}»** (activo) y/o **«En pausa: {nombre}»** (pausado),
evitando empezar un duplicado. Al alcanzar el **límite Free (2 activos)**, el botón «Empezar» pasa
a CTA **«Hazte Premium para más»** → `route('premium')` (coherente con el gate de `BatchController`).

**Anillo de progreso más preciso.** `Batch::overallProgressPercentage()` = **tiempo por horas + 2%
por paso terminado** (no el `max(tiempo,pasos)` inicial, que daba demasiado crédito: 10 min de prep
no debe marcar 33%). Topado en **99% hasta completar** (entonces 100%). El tiempo
(`timelineProgressPercentage()`) se mide en **horas fraccionadas** sobre `default_duration_days*24`
y se ancla a `fermentationStartedAt()` (= `created_at` si empezó hoy, si no el inicio a las 00:00),
así un fermento de 24 h se mueve ~4%/h y uno de 7 días deja el 0% en la primera hora. Se usa en el
anillo de `batches/show` y en las tarjetas de dashboard/`batches/index` (con `steps` eager-loaded
para evitar N+1).
