Calculer automatiquement les régimes d'une recette à partir de ses ingrédients #131

Closed
opened 2026-09-09 13:19:24 +02:00 by kyuno · 1 comment
Owner

Contexte

Origine : ticket #3, puce 4 — « Faire en sorte que les régimes alimentaires associés aux recettes soient calculés automatiquement à partir des ingrédients ».

Aujourd'hui, RecipeDiet est explicitement manuel — le commentaire de schéma le dit : « a manual reminder set by whoever creates/edits the recipe, not computed from its ingredients » (schema.prisma:399-412). Choisi via DietTagSelect (grille de cases à cocher, DietTagSelect.tsx) dans RecipeFormPage/RecipeImportForm, stocké tel quel par createRecipe/updateRecipe (diets: { create: input.dietIds.map(...) }, recipe.service.ts:705), affiché par DietBadges.

La donnée pour le calcul existe déjà : IngredientDiet (schema.prisma:614-635) — les régimes avec lesquels chaque ingrédient est compatible, seedés par catégorie avec overrides. Exposée via IngredientView.diets (reference.service.ts:143-176, types/reference.ts:283-304). IngredientDiet omet volontairement omnivore (tout ingrédient qualifie) et glutenFree (dérivable de l'absence de l'allergène gluten).

Régimes seedés : omnivore, vegetarian, vegan, pescatarian, glutenFree (catalog.diets.*).

Comportement souhaité

Les régimes d'une recette sont dérivés : une recette suit un régime ssi tous ses ingrédients y sont compatibles.

recette.regimes = ∩ ingredient.diets        (sur les ingrédients non-placeholder)
                ∪ { glutenFree } si aucun ingrédient ne porte l'allergène gluten
  • glutenFree : cas spécial, dérivé des allergènes (cohérent avec la raison pour laquelle IngredientDiet ne le stocke pas).
  • omnivore : jamais affiché comme tag (déjà la posture actuelle).
  • Ingrédients placeholder (isPlaceholder, sans lien diet) : ils cassent toute garantie ⇒ tant qu'une recette en contient un non promu, elle ne peut être certifiée sur aucun régime (afficher un « ? » / rien). À confirmer.

Décision structurante (à trancher — bloquante)

Option A — Dérivé, RecipeDiet supprimé. dietIds disparaît de createRecipeSchema, DietTagSelect est retiré du formulaire, RecipeDiet est droppé, RecipeSummaryView.diets est calculé à la volée depuis les ingrédients. Le plus propre ; perd le tag manuel (peu de perte réelle : les 4 régimes non-triviaux sont tous décidables depuis les ingrédients) ; le filtre « convient au foyer » (suitableForHouseholdWhere, recipe.service.ts:318-359) doit passer d'un diets: { some: { dietId } } SQL à un filtre calculé.

Option B — Calculé + surchargeable. RecipeDiet reste, pré-rempli/verrouillé par le calcul, avec possibilité de retirer un régime (jamais en ajouter un que les ingrédients contredisent). Plus de code, migration douce.

Recommandation : Option A, alignée sur la formulation de la puce.

Logique pure partagée (packages/shared)

/** ∩ des régimes compatibles de chaque ingrédient, + glutenFree dérivé des allergènes. */
export function computeRecipeDiets(ingredients: {
  diets: { key: string }[];
  allergens: { key: string }[];
  isPlaceholder: boolean;
}[]): string[];

Colocalisée avec la logique recette, testable isolément (convention specs/dev-conventions.md : matchXxx() pur séparé du DB-backed).

Fichiers à toucher (Option A)

  • apps/api/prisma/schema.prisma — drop RecipeDiet + relation Recipe.diets ; migration.
  • packages/shared — computeRecipeDiets ; createRecipeSchema retire dietIds (schemas/recipe.ts:56-67) ; RecipeSummaryView.diets reste dans le type (toujours renvoyé, juste calculé).
  • recipe.service.ts — toRecipeSummaryView/toRecipeView calculent diets via computeRecipeDiets(ingredients) au lieu de recipe.diets.map(...) (recipe.service.ts:140-156) ; createRecipe/updateRecipe ne stockent plus rien ; assertDietsExist supprimé ; suitableForHouseholdWhere + le filtre dietIds de listRecipes repensés (post-filtre en mémoire, ou colonne dérivée — à trancher).
  • apps/web — retirer DietTagSelect de RecipeFormPage (RecipeFormPage.tsx:313) et RecipeImportForm (RecipeImportForm.tsx:423) ; DietBadges inchangé ; retirer dietIds des payloads ; petite légende « calculé automatiquement » sur le détail.

Découpage proposé

  1. Logique pure — computeRecipeDiets + tests, sans branchement.
  2. Lecture calculée — toRecipeView/toRecipeSummaryView dérivent diets ; les tags manuels existants sont ignorés à la lecture (transition).
  3. Écriture + UI — retrait de DietTagSelect/dietIds, drop RecipeDiet, refonte des filtres suitableForHousehold/dietIds.

Tests (obligatoires, cf. specs/dev-conventions.md)

  • Mocha : computeRecipeDiets (tous vegan ⇒ vegan+vegetarian ; un ingrédient viande ⇒ ∅ ; un placeholder ⇒ ∅ ; aucun ingrédient gluten ⇒ glutenFree) ; GET /recipes/:id renvoie les régimes dérivés ; listRecipes({ dietIds }) et suitableForHousehold filtrent sur la base calculée ; non-régression liste de courses/planning.
  • Cypress : le formulaire n'a plus de sélecteur de régime ; détail/tableau affichent des badges cohérents avec les ingrédients ; Gherkin création vegan → badge vegan automatique.

Documentation à mettre à jour

  • specs/batch-cooking-modele.md — suppression de recipe_diet, note sur la dérivation.
  • specs/backend-architecture.md — section recettes (régimes calculés, computeRecipeDiets, impact filtres planning).

Points restant à trancher en revue

  • Option A vs B (voir plus haut) — bloquant.
  • Recette avec ingrédient placeholder : aucun régime certifié, ou calcul « optimiste » en ignorant le placeholder ? (aucun = sûr.)
  • Filtre suitableForHousehold/dietIds : post-filtre en mémoire (simple, lecture large) vs colonne dérivée maintenue à l'écriture (rapide, redonde RecipeDiet sous un autre nom).
  • Recette importée : régimes calculés pareil une fois les ingrédients résolus — confirmer qu'aucune source ne fournit ses propres tags à conserver.

🤖 Filed by Claude Code

## Contexte Origine : ticket #3, puce 4 — *« Faire en sorte que les régimes alimentaires associés aux recettes soient calculés automatiquement à partir des ingrédients »*. Aujourd'hui, `RecipeDiet` est **explicitement manuel** — le commentaire de schéma le dit : *« a manual reminder set by whoever creates/edits the recipe, **not computed from its ingredients** »* ([schema.prisma:399-412](apps/api/prisma/schema.prisma#L399-L412)). Choisi via `DietTagSelect` (grille de cases à cocher, [DietTagSelect.tsx](apps/web/src/features/recipes/badges/DietTagSelect.tsx)) dans `RecipeFormPage`/`RecipeImportForm`, stocké tel quel par `createRecipe`/`updateRecipe` (`diets: { create: input.dietIds.map(...) }`, [recipe.service.ts:705](apps/api/src/modules/recipe/recipe.service.ts#L705)), affiché par `DietBadges`. La donnée pour le calcul **existe déjà** : `IngredientDiet` ([schema.prisma:614-635](apps/api/prisma/schema.prisma#L614-L635)) — les régimes avec lesquels chaque ingrédient est compatible, seedés par catégorie avec overrides. Exposée via `IngredientView.diets` ([reference.service.ts:143-176](apps/api/src/modules/reference/reference.service.ts#L143-L176), [types/reference.ts:283-304](packages/shared/src/types/reference.ts#L283-L304)). `IngredientDiet` **omet volontairement** `omnivore` (tout ingrédient qualifie) et `glutenFree` (dérivable de l'absence de l'allergène `gluten`). Régimes seedés : `omnivore`, `vegetarian`, `vegan`, `pescatarian`, `glutenFree` (`catalog.diets.*`). ## Comportement souhaité Les régimes d'une recette sont **dérivés** : une recette suit un régime **ssi tous ses ingrédients** y sont compatibles. ``` recette.regimes = ∩ ingredient.diets (sur les ingrédients non-placeholder) ∪ { glutenFree } si aucun ingrédient ne porte l'allergène gluten ``` - `glutenFree` : cas spécial, dérivé des allergènes (cohérent avec la raison pour laquelle `IngredientDiet` ne le stocke pas). - `omnivore` : jamais affiché comme tag (déjà la posture actuelle). - Ingrédients **placeholder** (`isPlaceholder`, sans lien diet) : ils cassent toute garantie ⇒ tant qu'une recette en contient un non promu, elle ne peut être certifiée sur aucun régime (afficher un « ? » / rien). À confirmer. ## Décision structurante (à trancher — bloquante) **Option A — Dérivé, `RecipeDiet` supprimé.** `dietIds` disparaît de `createRecipeSchema`, `DietTagSelect` est retiré du formulaire, `RecipeDiet` est droppé, `RecipeSummaryView.diets` est calculé à la volée depuis les ingrédients. Le plus propre ; perd le tag manuel (peu de perte réelle : les 4 régimes non-triviaux sont tous décidables depuis les ingrédients) ; le filtre « convient au foyer » (`suitableForHouseholdWhere`, [recipe.service.ts:318-359](apps/api/src/modules/recipe/recipe.service.ts#L318-L359)) doit passer d'un `diets: { some: { dietId } }` SQL à un filtre calculé. **Option B — Calculé + surchargeable.** `RecipeDiet` reste, pré-rempli/verrouillé par le calcul, avec possibilité de retirer un régime (jamais en ajouter un que les ingrédients contredisent). Plus de code, migration douce. Recommandation : **Option A**, alignée sur la formulation de la puce. ## Logique pure partagée (`packages/shared`) ```ts /** ∩ des régimes compatibles de chaque ingrédient, + glutenFree dérivé des allergènes. */ export function computeRecipeDiets(ingredients: { diets: { key: string }[]; allergens: { key: string }[]; isPlaceholder: boolean; }[]): string[]; ``` Colocalisée avec la logique recette, testable isolément (convention `specs/dev-conventions.md` : `matchXxx()` pur séparé du DB-backed). ## Fichiers à toucher (Option A) - `apps/api/prisma/schema.prisma` — drop `RecipeDiet` + relation `Recipe.diets` ; migration. - `packages/shared` — `computeRecipeDiets` ; `createRecipeSchema` retire `dietIds` ([schemas/recipe.ts:56-67](packages/shared/src/schemas/recipe.ts#L56-L67)) ; `RecipeSummaryView.diets` reste dans le type (toujours renvoyé, juste calculé). - `recipe.service.ts` — `toRecipeSummaryView`/`toRecipeView` calculent `diets` via `computeRecipeDiets(ingredients)` au lieu de `recipe.diets.map(...)` ([recipe.service.ts:140-156](apps/api/src/modules/recipe/recipe.service.ts#L140-L156)) ; `createRecipe`/`updateRecipe` ne stockent plus rien ; `assertDietsExist` supprimé ; `suitableForHouseholdWhere` + le filtre `dietIds` de `listRecipes` repensés (post-filtre en mémoire, ou colonne dérivée — à trancher). - `apps/web` — retirer `DietTagSelect` de `RecipeFormPage` ([RecipeFormPage.tsx:313](apps/web/src/pages/recipes/RecipeFormPage.tsx#L313)) et `RecipeImportForm` ([RecipeImportForm.tsx:423](apps/web/src/features/recipes/sources/RecipeImportForm.tsx#L423)) ; `DietBadges` inchangé ; retirer `dietIds` des payloads ; petite légende « calculé automatiquement » sur le détail. ## Découpage proposé 1. **Logique pure** — `computeRecipeDiets` + tests, sans branchement. 2. **Lecture calculée** — `toRecipeView`/`toRecipeSummaryView` dérivent `diets` ; les tags manuels existants sont ignorés à la lecture (transition). 3. **Écriture + UI** — retrait de `DietTagSelect`/`dietIds`, drop `RecipeDiet`, refonte des filtres `suitableForHousehold`/`dietIds`. ## Tests (obligatoires, cf. `specs/dev-conventions.md`) - **Mocha** : `computeRecipeDiets` (tous vegan ⇒ vegan+vegetarian ; un ingrédient viande ⇒ ∅ ; un placeholder ⇒ ∅ ; aucun ingrédient gluten ⇒ glutenFree) ; `GET /recipes/:id` renvoie les régimes dérivés ; `listRecipes({ dietIds })` et `suitableForHousehold` filtrent sur la base calculée ; non-régression liste de courses/planning. - **Cypress** : le formulaire n'a plus de sélecteur de régime ; détail/tableau affichent des badges cohérents avec les ingrédients ; Gherkin création vegan → badge vegan automatique. ## Documentation à mettre à jour - `specs/batch-cooking-modele.md` — suppression de `recipe_diet`, note sur la dérivation. - `specs/backend-architecture.md` — section recettes (régimes calculés, `computeRecipeDiets`, impact filtres planning). ## Points restant à trancher en revue - **Option A vs B** (voir plus haut) — bloquant. - Recette avec ingrédient placeholder : aucun régime certifié, ou calcul « optimiste » en ignorant le placeholder ? (aucun = sûr.) - Filtre `suitableForHousehold`/`dietIds` : post-filtre en mémoire (simple, lecture large) vs colonne dérivée maintenue à l'écriture (rapide, redonde `RecipeDiet` sous un autre nom). - Recette **importée** : régimes calculés pareil une fois les ingrédients résolus — confirmer qu'aucune source ne fournit ses propres tags à conserver. --- 🤖 Filed by [Claude Code](https://claude.com/claude-code)
kyuno added the
enhancement
planned
labels 2026-09-09 13:19:24 +02:00
kyuno added the
High
label 2026-09-09 13:24:15 +02:00
Author
Owner

Implémenté par la PR #137 (feat/auto-recipe-diets), mergée dans main (81b4a14). Option A retenue : RecipeDiet droppé (migration 20260909130000_drop_recipe_diet), dietIds retiré de createRecipeSchema, DietTagSelect retiré des formulaires. RecipeView.diets / RecipeSummaryView.diets dérivés via computeRecipeDietViews (@batch-cooking/shared, pur, testé). Placeholder ⇒ aucun régime certifié. Filtres suitableForHousehold / dietIds devenus des post-filtres en mémoire. Vérifié présent sur main.

Implémenté par la PR #137 (`feat/auto-recipe-diets`), mergée dans `main` (`81b4a14`). **Option A** retenue : `RecipeDiet` droppé (migration `20260909130000_drop_recipe_diet`), `dietIds` retiré de `createRecipeSchema`, `DietTagSelect` retiré des formulaires. `RecipeView.diets` / `RecipeSummaryView.diets` dérivés via `computeRecipeDietViews` (`@batch-cooking/shared`, pur, testé). Placeholder ⇒ aucun régime certifié. Filtres `suitableForHousehold` / `dietIds` devenus des post-filtres en mémoire. Vérifié présent sur `main`.
kyuno closed this issue 2026-09-09 20:49:21 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: kyuno/batchCooking#131
No description provided.