# Fermenty — Recipe Layer Design (Guide → Recipe → Ingredients)

> Data-model sketch for promoting the "base recipe" into a first-class **Recipe**
> layer, designed end-to-end (variants, community sharing, re-use/forking,
> ratings, per-recipe hints) so we build a solid base now even though the first
> launch ships only a basic free version.
>
> Status: 🔵 in design. Related: `MEJORAS_ERP.md` (#3 base recipe + scaling),
> `00_PROJECT_OVERVIEW.md`, `TAREAS_PRIORIZADAS.md` (community posts ↔ recipe_id).
> Last updated: **2026-06-15**.

---

## 1. The shift in one sentence

Today the **base recipe lives inside the guide** (one per guide:
`fermentation_guides.base_yield_*` + `guide_ingredients`). We extract it into a
**Recipe** level so a single guide can carry many variants (Classic / Fruity /
a user's own), each with its own ingredients, optional steps, timings, hints and
reputation — while the **guide stays the immutable shared skeleton**.

```
FermentType  (category: Drink, Vegetable, Grains…)
   └─ FermentationGuide   ── the STRUCTURE: ordered steps, default timings, hints
        └─ Recipe          ── the DEPARTURE POINT: a variant within a guide
             ├─ RecipeIngredient   what goes in (per phase, per step)
             ├─ RecipeStep         per-recipe overlay: which steps + timing overrides
             └─ RecipeHint         this recipe's own tips
   Batch  = an instantiated, scaled, FROZEN snapshot of ONE Recipe
```

The machinery to instantiate a batch from a source (copy steps, scale & freeze
ingredients, schedule notifications) **already exists** in `BatchService` /
`ScalingService`; we re-point it from *guide* to *recipe*.

---

## 2. Core data model (ER sketch)

Mermaid renders on GitHub and most IDEs. New tables are marked `🆕`.

```mermaid
erDiagram
    FERMENT_TYPES        ||--o{ FERMENTATION_GUIDES : "has"
    FERMENTATION_GUIDES  ||--o{ GUIDE_STEPS          : "structure"
    GUIDE_STEPS          ||--o{ GUIDE_STEP_HINTS     : "guide hints"
    GUIDE_STEPS          ||--o{ STEP_ACTIONS         : "actions"

    FERMENTATION_GUIDES  ||--o{ RECIPES              : "variants"
    USERS                ||--o{ RECIPES              : "authors (null = official)"
    RECIPES              ||--o{ RECIPE_INGREDIENTS   : "what goes in"
    RECIPES              ||--o{ RECIPE_STEPS         : "step overlay"
    RECIPES              ||--o{ RECIPE_HINTS         : "own tips"
    GUIDE_STEPS          ||--o{ RECIPE_INGREDIENTS   : "attaches to (opt.)"
    GUIDE_STEPS          ||--o{ RECIPE_STEPS         : "overrides"

    RECIPES              ||--o{ BATCHES              : "instantiated as"
    FERMENTATION_GUIDES  ||--o{ BATCHES              : "denormalized guide_id"
    BATCHES              ||--o{ BATCH_STEPS          : "snapshot"
    BATCHES              ||--o{ BATCH_INGREDIENTS    : "scaled + frozen"
    GUIDE_STEPS          ||--o{ BATCH_STEPS          : "source step"
    RECIPE_INGREDIENTS   ||--o{ BATCH_INGREDIENTS    : "source line"

    RECIPES              ||--o{ RECIPE_REVIEWS       : "rated by"
    RECIPES              ||--o{ RECIPE_LIKES         : "liked by"
    RECIPES              ||--o{ RECIPE_FORK_REQUESTS : "re-use requests"
    RECIPES              ||--o| RECIPES              : "source_recipe_id (fork lineage)"
    RECIPES              ||--o| COMMUNITY_POSTS      : "shared via"
```

### ASCII overview (terminal-friendly)

```
                       ┌─────────────┐
                       │ FERMENT_TYPE│  category
                       └──────┬──────┘
                              │ 1..*
                       ┌──────▼───────────┐
                       │ FERMENTATION_GUIDE│  shared skeleton
                       └──┬───────────┬────┘
                  1..*    │           │ 1..*
            ┌─────────────▼──┐   ┌────▼────────┐
            │   GUIDE_STEP   │   │   RECIPE 🆕  │◄── user_id (null = official)
            │ min/max_day,   │   │ base_yield,  │◄── source_recipe_id (fork)
            │ is_optional    │   │ visibility,  │
            └──┬─────────┬───┘   │ status       │
               │         │       └──┬───┬───┬───┘
        hints/actions    │ attaches │   │   │
                         │   ┌──────▼┐ ┌▼──────────┐ ┌▼──────────┐
                         │   │RECIPE_│ │RECIPE_STEP│ │RECIPE_HINT│
                         │   │INGRED.│ │ overlay 🆕│ │   🆕      │
                         │   │  🆕   │ └───────────┘ └───────────┘
                         │   └───┬───┘
                         │       │ source line
                  ┌──────▼───────▼──────────────────────┐
                  │  BATCH = frozen snapshot of a RECIPE │
                  │  batch_steps + batch_ingredients     │
                  └──────────────────────────────────────┘
```

---

## 3. Table-by-table

### Existing (kept as-is, structure layer)
- `ferment_types` — category.
- `fermentation_guides` — skeleton. **Deprecate** `base_yield_value/unit` here once
  recipes own them (keep during migration, drop later).
- `guide_steps`, `guide_step_hints`, `step_actions` — unchanged.

### 🆕 `recipes` (the new departure point)
| column | type | notes |
|---|---|---|
| `id` | pk | |
| `guide_id` | fk → fermentation_guides | the skeleton it follows |
| `ferment_type_id` | fk | denormalized from guide for fast category filters |
| `user_id` | fk nullable | **null = official Fermenty recipe**; set = user-authored |
| `name` | string | "Classic Kombucha", "Fruity Kombucha" |
| `slug` | string | for public URLs (community) |
| `description` | text nullable | |
| `base_yield_value` | decimal(8,3) nullable | moved off the guide |
| `base_yield_unit` | string(10) nullable | |
| `visibility` | enum(`private`,`unlisted`,`public`) default `private` | |
| `status` | enum(`draft`,`pending`,`published`,`rejected`) default `draft` | moderation gate for public listing |
| `is_official` | bool default false | curated/seeded by Fermenty |
| `source_recipe_id` | fk nullable → recipes | fork lineage (provenance) |
| `likes_count` | uint default 0 | denormalized counter |
| `rating_avg` | decimal(3,2) nullable | denormalized from recipe_reviews |
| `rating_count` | uint default 0 | |
| `times_brewed` | uint default 0 | denormalized from batches |
| timestamps | | |

Indexes: `(guide_id)`, `(user_id, visibility)`, `(ferment_type_id, visibility, status)`, `(source_recipe_id)`.

### 🆕 `recipe_ingredients` (replaces role of `guide_ingredients`)
Mirrors `guide_ingredients` exactly — same canonical-metric convention so
`ScalingService` works untouched.
`recipe_id, guide_step_id?(fk), name, quantity(dec 10,3), unit, phase(primary|secondary), is_optional(bool), order(int), notes`

### 🆕 `recipe_steps` (thin overlay — only rows that deviate)
A recipe normally inherits the guide's steps verbatim. This table stores **only
deviations**: skip a step, or override its timing.
`recipe_id, guide_step_id(fk), is_included(bool default true), min_day?(int), max_day?(int), notes`
> Fruity Kombucha → row for the "2nd fermentation" step with `max_day = 1`.

### 🆕 `recipe_hints` (per-recipe tips, distinct from guide-wide hints)
`recipe_id, guide_step_id?(fk null = general), body(text), order(int), author_user_id?(fk)`

### Changed: `batches`
- `recipe_id` already exists (nullable) → becomes the **real driver** (required for
  new batches; `guide_id` derived from the recipe but kept denormalized).
### Changed: `batch_ingredients`
- add `recipe_ingredient_id` (fk nullable) alongside the existing
  `guide_ingredient_id`; the snapshot links back to the recipe line.

---

## 4. Community / sharing layer (designed now, shipped later)

Two **separate** "OK" mechanisms — they solve different problems:

1. **Moderation OK** — does this recipe get to appear publicly?
   Driven by `recipes.status` (`draft → pending → published`/`rejected`).
   Admin tooling reuses the existing admin panel pattern.

2. **Re-use / fork OK** — can another user copy someone's recipe into their
   account? For `public` recipes this is implicit (copy = create a new recipe with
   `source_recipe_id` set, preserving attribution). For gated re-use we keep an
   explicit request table:

### 🆕 `recipe_fork_requests` *(phase 3+, optional gating)*
`recipe_id, requester_user_id, status(pending|approved|rejected), responded_by?, responded_at?, message?`

### 🆕 `recipe_reviews` (rating + valoration per recipe)
Distinct from the existing `batch_reviews` (which evaluate one *batch outcome*).
A recipe review aggregates real experience and feeds `rating_avg`/`rating_count`.
`recipe_id, user_id, batch_id?(fk — proof of brew), rating(1-5), title?, body?, timestamps`
Unique `(recipe_id, user_id)`.

### 🆕 `recipe_likes`
Pivot `user_id, recipe_id` → drives `likes_count`.

### Reuse existing `community_posts`
A recipe is **shared to the feed** by creating a `community_posts` row that links
`recipe_id` (as already noted in `TAREAS_PRIORIZADAS.md`). The recipe stays the
canonical object; the post is just the social wrapper + comments.

---

## 5. Identity & ownership model

| `user_id` | `is_official` | `visibility` | meaning |
|---|---|---|---|
| null | true | public | Fermenty's curated recipe (seeded) |
| set | false | private | user's own, only they see it |
| set | false | unlisted | shareable by link, not in discovery |
| set | false | public + status=published | in community after moderation |

- **Forking** = clone a recipe (+ its ingredients/steps/hints) into the forker's
  account with `source_recipe_id` = original. Lineage is queryable for attribution
  and "based on X" credits.
- **Frozen batches are unaffected** by later recipe edits/deletes — the batch
  already snapshotted everything (`batch_steps`, `batch_ingredients`). `recipe_id`
  on a batch is `nullOnDelete`.

---

## 6. How batch creation changes

`BatchService::create()` today: load **guide** with steps+ingredients → copy steps,
scale ingredients, schedule notifications.

After: load **recipe** (with `guide.steps`, `recipe_steps` overlay,
`recipe_ingredients`) →
1. resolve effective steps = guide steps filtered/over-timed by `recipe_steps`,
2. `copySteps()` from the resolved set,
3. `snapshotIngredients()` from `recipe_ingredients` (same `ScalingService`),
4. notifications + start event unchanged,
5. `times_brewed++` on the recipe.

The scaling/freezing/notification internals **do not change** — only the source
of the lists.

---

## 7. Phased rollout (build solid, ship basic)

| Phase | Scope | Tables touched |
|---|---|---|
| **1 — Launch (free, basic)** ✅ **shipped 2026-06-15** | `recipes` + `recipe_ingredients` + `recipe_steps`; batch driven by recipe; per-step overlay (include/exclude + timing) frozen onto `batch_steps`; **full admin recipe CRUD** (variants, ingredients, step overlay); **recipe cover images** (`recipes.image`, falls back to ferment type) + **recipe-card batch creation** (no guide-first step); **owner-editable batch ingredients** (edit/add/remove the frozen snapshot per batch, without touching the recipe); data-migration lifted each guide's base recipe into one official "Clásica" recipe | recipes, recipe_ingredients, recipe_steps, batches, batch_ingredients, batch_steps |

> **Public recipe catalogue (shipped 2026-06-15):** `/recetas` (menu "Recetas",
> after Dashboard) lists recipes as image cards, **grouped by fermentation
> category**, each opening a readable recipe detail with a direct "Empezar". The
> old `/fermentos` catalogue still works but is no longer in the menu.

### Fermentation-category classification (shipped 2026-06-15)
A top-level axis above ferment types, classifying by **dominant fermentation
process**: `fermentation_categories` (Láctica, Acética, Alcohólica, Butírica…,
admin-managed + extensible) with `ferment_types.fermentation_category_id`. Used
to group the `/recetas` catalogue. Admin CRUD under `acp/clasificacion` + a
category select on the ferment-type form. Existing types were auto-assigned by a
data migration (best-effort; admins can reassign). Note: a ferment can involve
several processes (kombucha = acética + algo alcohólica); we store the dominant
one for grouping.
| **2 — User recipes** | user-authored private recipes; `recipe_hints`; recipe editor UI | recipe_hints |
| **3 — Community** | `public` visibility + moderation `status`; `recipe_reviews`, `recipe_likes`; forking via `source_recipe_id`; share to `community_posts` | recipe_reviews, recipe_likes, (+ community_posts link) |
| **4 — Gated re-use & moderation tools** | `recipe_fork_requests`; reports/admin moderation | recipe_fork_requests |

> We can create the full `recipes` table with all columns in Phase 1 (cheap) and
> simply leave the community columns unused until Phase 3, OR add them via small
> migrations later. Recommendation: **ship the full `recipes` schema in Phase 1**
> (counters/visibility/status default sensibly) so we never re-shape the central
> table; add the *satellite* tables (reviews/likes/forks) only when their phase
> lands.

---

## 8. Migration safety notes

- **Backfill:** for every guide with a base recipe, create one `recipes` row
  (`name = "<Guide> — Clásica"`, `is_official = true`, `user_id = null`), copy
  `guide_ingredients → recipe_ingredients`, move `base_yield_*`. Point existing
  batches' `recipe_id` at it (best-effort by guide).
- Keep `guide_ingredients` + guide `base_yield_*` **readable** through Phase 1;
  drop only after the recipe path is verified in production.
- Domain logic stays in services (`BatchService`, new `RecipeService`) per the
  ERP rule, so the future API/Flutter app reuses it verbatim.

---

## 9. Decisions & open questions

**Decided (2026-06-15):**
- **No recipe versioning.** Unlike guides (which carry `version`/`is_current`),
  recipes store only their *final* state. Users may freely edit their own recipes
  in place; we keep no history. "Fork-on-edit" via `source_recipe_id` covers the
  "based on someone else's" case — there is **no** `version`/`is_current` column on
  `recipes`.
- **`guide_id` is immutable once a recipe has batches.** A recipe stays bound to
  its guide; editing never moves it to another guide once batches exist. (Before
  any batch is brewed it could in principle be re-pointed, but the UI need not
  offer this.) Because batches snapshot everything at creation, in-place recipe
  edits never affect existing batches.

**Still open:**
1. Do user forks of an *official* recipe need moderation before going public, or only original public recipes?
2. Should `recipe_reviews` require a linked `batch_id` (proof-of-brew) to count toward `rating_avg`? (Recommended for trust.)

---

## 10. User recipe creation (Phase 2 design)

The data layer already supports user recipes (`recipes.user_id` set, `visibility`
= private, the same ingredient/step tables). This section designs the **flow**;
no code yet.

### Catalogue note (free tier)
In the **free basic** app there is **one recipe per guide** (the official
"Clásica"). The public catalogue (`/fermentos`) therefore stays a compact
category list — e.g. *Drinks*: Kombucha Clásica, Kéfir de agua, Ginger beer —
one card each. No large recipe-browsing page is needed until user/community
recipes multiply (Phase 3). So the existing ferment-type catalogue is fine for
launch; we just present each as "the recipe."

### Three creation entry points (in order of expected use)

1. **Fork an existing recipe — "Usar como base"** *(primary)*
   From any recipe (official or public), "Crear mi versión" clones it into the
   user's account: copies `recipe_ingredients` + `recipe_steps`, sets
   `user_id`, `visibility=private`, `source_recipe_id=original`. They land in the
   editor with everything pre-filled and just change what differs (the tea, a
   fruit, a time). Matches how people actually cook: start from a known recipe.

2. **Save a batch as a recipe — "Guardar como receta"** *(captures real brews)*
   A user who tweaked a batch's ingredients on the run (inline editing, shipped
   2026-06-15) can promote that batch into a recipe: new recipe under the batch's
   guide, ingredients built from the batch's (edited) `batch_ingredients`,
   `base_yield` = the batch's target yield. Closes the loop experiment → recipe.

3. **From scratch — "Receta nueva"** *(advanced)*
   Pick a guide (the structure), then build name/image/yield/ingredients. Steps
   default to the guide's; the overlay is an "advanced" collapsible.

### The editor (user-facing)
Reuse the admin recipe editor, simplified for end users:
- **Show:** name, image, description, base yield, ingredients (the same
  click-to-edit rows), and an *advanced* "Pasos" overlay (include/exclude +
  timing).
- **Hide / default:** `is_official=false`, `visibility=private`,
  `status=published` (so the user can immediately brew it). Community sharing
  (make public) is a separate Phase 3 action with its own moderation gate.
- Lives under a new **"Mis recetas"** area in the private zone (list / create /
  edit / delete), user-scoped (`Recipe::where('user_id', auth id)`).

### Editing & lifecycle
- Per §9: **no versioning** — users edit their recipes in place; existing batches
  are unaffected (they snapshot at creation).
- A user recipe with batches can still be edited; deletion is blocked when it has
  batches (nullify link would lose provenance) — same rule as the admin CRUD.

### Decisions needed before building Phase 2
- **Plan-gating:** is "create my own recipe" a **Premium** feature (free tier =
  brew the official recipe only), or free with **community sharing** being the
  Premium/moderated part? (Recommendation: creating + private use = consistent
  with plan limits; *publishing* to community = Phase 3 + moderation.)
- **Which entry points ship first?** Recommendation: **fork** + **save-from-batch**
  first (highest value, lowest friction); "from scratch" after.

---

## 11. Recipe-creation wizard (step-by-step) — ✅ shipped 2026-06-15

> **Model change shipped:** recipes now **own their steps** (`recipe_steps` carries
> title, description, días, optional, `notify_on_complete`, `actions` JSON),
> preloaded from the guide as a template and fully editable without touching it.
> Recipe ingredients attach to `recipe_step_id`; batches **snapshot** the recipe's
> steps onto `batch_steps` (self-contained; reads fall back to the guide for
> legacy batches). The per-step "notify on complete" schedules a reminder at the
> step's `max_day`. Admin wizard at `acp/recetas/crear` (STEP 1 basics → STEP N
> steps); the old overlay editor was retired. Build below was the design.

### Original design

A single **guided wizard** used in two contexts (admin **and** end users); the
only differences are ownership and which advanced fields show. Each screen saves
to a **draft recipe** (`status='draft'`) so progress is never lost; the recipe
only becomes brewable/visible at the final publish step.

### Steps
1. **Punto de partida** — choose the **fermento** (→ its current guide = the step
   skeleton). Optionally start *from an existing recipe* (fork) to pre-fill
   everything. Admin may pick a specific guide version; users get the current one.
   → creates/loads the draft recipe.
2. **Datos básicos** — name, description, **image**, base yield (value + unit).
3. **Ingredientes** — add/edit lines (name, cantidad, unidad, fase, paso,
   opcional). Pre-seeded from the guide's official recipe as a template so the
   user edits rather than starts empty. Reuses the click-to-edit row component.
4. **Pasos (overlay)** — the guide's steps listed; include/exclude each and tweak
   timing (días). *Premium:* attach an **image per step** (see §12). This is the
   "advanced" screen — collapsible/skippable; defaults inherit the guide.
5. **Revisar y publicar** — summary preview (like the public recipe detail).
   - **User:** save as **privada** (brewable immediately) or *Compartir con la
     comunidad* → `status=pending` (Phase 3 moderation).
   - **Admin:** set `visibility` / `status` / `is_official`.

### Build approach (reuse, don't duplicate)
- The wizard is a thin multi-screen shell over the **existing** recipe pieces:
  `RecipeController` fields form (step 2), recipe-ingredient CRUD (step 3),
  step-overlay sync (step 4). One `RecipeWizardController` orchestrates; each
  screen posts and advances, editing the same draft row.
- **Admin** enters it from *Recetas → Nueva receta*; **users** from *Mis recetas
  → Crear* (Phase 2) and the fork / save-from-batch entry points (§10).
- Draft cleanup: drafts with no activity can be pruned later; not critical for v1.

### Open decisions before building
- **Wizard vs. single page:** wizard (5 screens, friendlier for users) vs. the
  current single-page admin editor (faster for power admins). Could keep the
  single page for admin and the wizard for users — or use the wizard everywhere.
  *Recommendation:* one wizard everywhere for consistency; admin extra fields on
  the last screen.
- **Where users access it:** confirm the "Mis recetas" area (private zone) and
  whether creation is Premium-gated (ties to §10 plan-gating decision).

---

## 12. Backlog

- **Per-step recipe images (Premium).** Let the author attach an image to each
  recipe step during creation (wizard step 4), shown in the recipe detail and the
  batch step-by-step. Needs: `recipe_steps.image` (the overlay row already exists
  per step) + upload handling (reuse `ImageStore`) + Premium gate. Parked
  2026-06-15 at user request.
