feat: classement des ingredients par famille et filtrage du constructeur de recette #174

Merged
Corentin merged 1 commit from feat/ingredient-families into dev 2026-09-27 12:08:07 +02:00
Owner

Le constructeur de recette proposait les 50 ingredients du catalogue a plat, quelle que soit la categorie du produit. En composant un burger, l'equipier se voyait proposer Brownie, Gobelet, Dose Coca et Cheesecake au milieu des pains et des steaks. Signale depuis l'usage reel.

Classer, pas retirer

Le reflexe serait de sortir ces lignes du catalogue d'ingredients. Les donnees l'interdisent :

produit "Brownie"    -> recette : 1x ingredient "Brownie"
produit "Coca Cola"  -> recette : 1x "Dose Coca" + 1x "Gobelet"

C'est cette ligne de recette qui decremente le stock au paiement (RG-T20) et alimente la disponibilite calculee (RG-T21). Les retirer casserait le suivi de stock des desserts et des boissons. Le defaut n'etait pas leur presence, mais l'absence de toute classification.

Ce que la PR apporte

  • ingredient.family (dix familles) et category_ingredient_family : la correspondance est une donnee, une relation reelle du modele, pas un tableau ecrit en dur dans une classe PHP.
  • Filtrage du selecteur sur les deux ecrans qui composent une recette (formulaire produit et page recette dediee) : regroupement par famille, compteur visible, case "afficher tous les ingredients".
  • Le champ famille dans le formulaire ingredient et dans l'API JSON d'administration, meme validation des deux cotes ; collections Postman et Bruno regenerees depuis leur script source.
  • Reprise des 50 ingredients par migration (installation en service) et classement dans le jeu de donnees (installation neuve), avec un test qui verifie que les deux disent la meme chose.

Le filtre est souple, deliberement

Aucune validation serveur ne refuse une composition hors famille, et une case desactive le filtre. Une classification finit par rencontrer un cas qu'elle n'avait pas prevu (une salade avec des croutons) ; un blocage dur en ferait une impasse sans recours. Le filtre est une aide a la saisie, pas une regle de gestion : c'est ecrit tel quel dans la fiche de decision.

Sur doute, on montre trop plutot que rien

Famille absente, categorie sans correspondance, donnees illisibles : le resultat est "aucun filtre, on montre les 50", jamais une liste vide. Ce choix repond a l'incident du matin meme, ou un defaut de cache avait laisse l'equipier devant un constructeur ou rien ne se passait, sans message. Un selecteur qui se viderait en silence serait le meme defaut, par conception cette fois.

Une limite ouverte depuis juillet, fermee au passage

docs/adr/0015-allergenes-calcules-par-produit.md notait en consequences que Gobelet est un ingredient de recette alors que c'est un materiau au contact des denrees, et proposait un drapeau is_food. La famille contenant couvre le cas plus generalement : un drapeau binaire n'aurait rien fait pour le probleme signale, puisqu'un Brownie est un aliment. A dire tel quel : IngredientFamily::isFood() n'est appelee par aucun code de production a ce jour - une reponse disponible, pas une fonctionnalite branchee.

Verifications

Controle Resultat
PHPUnit (MariaDB reelle) 1725 tests, 5172 assertions, 0 echec (1698 avant)
PHPStan niveau 6 0 erreur
Tests JavaScript 374, 0 echec (356 avant)
Syntaxe des vues modifiees propre
Transtypage (object) avant encodage JSON verifie a l'execution

Tests ecrits avant le code, etat rouge constate.

Limites assumees, ecrites dans la fiche

  • Une categorie creee apres coup n'est pas filtree tant que personne ne l'a renseignee, et rien ne le signale.
  • La correspondance se change par migration, pas depuis le back-office.
  • L'import de produits par fichier ne connait pas la famille (en-tete a liste fermee, un ajout casserait les gabarits distribues) : un ingredient importe reste non classe.
  • L'API JSON remplace la ressource entiere : une modification qui omet la famille la remet a non classe. Comportement deja en place pour les autres champs optionnels, aligne plutot qu'exceptionnel.

Fiche de decision : docs/adr/0018-familles-ingredients-filtre-recette.md
Journal : docs/journal/2026-09-27--familles-ingredients-filtre-recette.md
Modele : dictionnaire 3.6 / 3.23 / note 16, MCD 5.1 / I8 / 5.3, MLD 4.6 / 4.23 (23 entites)

Le constructeur de recette proposait les **50 ingredients du catalogue a plat**, quelle que soit la categorie du produit. En composant un burger, l'equipier se voyait proposer `Brownie`, `Gobelet`, `Dose Coca` et `Cheesecake` au milieu des pains et des steaks. Signale depuis l'usage reel. ## Classer, pas retirer Le reflexe serait de sortir ces lignes du catalogue d'ingredients. Les donnees l'interdisent : ``` produit "Brownie" -> recette : 1x ingredient "Brownie" produit "Coca Cola" -> recette : 1x "Dose Coca" + 1x "Gobelet" ``` C'est cette ligne de recette qui decremente le stock au paiement (RG-T20) et alimente la disponibilite calculee (RG-T21). Les retirer casserait le suivi de stock des desserts et des boissons. Le defaut n'etait pas leur presence, mais l'absence de toute classification. ## Ce que la PR apporte - `ingredient.family` (dix familles) et `category_ingredient_family` : la correspondance est une **donnee**, une relation reelle du modele, pas un tableau ecrit en dur dans une classe PHP. - Filtrage du selecteur sur **les deux ecrans** qui composent une recette (formulaire produit et page recette dediee) : regroupement par famille, compteur visible, case "afficher tous les ingredients". - Le champ famille dans le formulaire ingredient **et** dans l'API JSON d'administration, meme validation des deux cotes ; collections Postman et Bruno regenerees depuis leur script source. - Reprise des 50 ingredients par migration (installation en service) et classement dans le jeu de donnees (installation neuve), avec un test qui verifie que les deux disent la meme chose. ## Le filtre est souple, deliberement Aucune validation serveur ne refuse une composition hors famille, et une case desactive le filtre. Une classification finit par rencontrer un cas qu'elle n'avait pas prevu (une salade avec des croutons) ; un blocage dur en ferait une impasse sans recours. **Le filtre est une aide a la saisie, pas une regle de gestion** : c'est ecrit tel quel dans la fiche de decision. ## Sur doute, on montre trop plutot que rien Famille absente, categorie sans correspondance, donnees illisibles : le resultat est "aucun filtre, on montre les 50", jamais une liste vide. Ce choix repond a l'incident du matin meme, ou un defaut de cache avait laisse l'equipier devant un constructeur ou rien ne se passait, sans message. Un selecteur qui se viderait en silence serait le meme defaut, par conception cette fois. ## Une limite ouverte depuis juillet, fermee au passage `docs/adr/0015-allergenes-calcules-par-produit.md` notait en consequences que `Gobelet` est un ingredient de recette alors que c'est un materiau au contact des denrees, et proposait un drapeau `is_food`. La famille `contenant` couvre le cas plus generalement : un drapeau binaire n'aurait rien fait pour le probleme signale, puisqu'un Brownie est un aliment. A dire tel quel : `IngredientFamily::isFood()` **n'est appelee par aucun code de production a ce jour** - une reponse disponible, pas une fonctionnalite branchee. ## Verifications | Controle | Resultat | |---|---| | PHPUnit (MariaDB reelle) | 1725 tests, 5172 assertions, 0 echec (1698 avant) | | PHPStan niveau 6 | 0 erreur | | Tests JavaScript | 374, 0 echec (356 avant) | | Syntaxe des vues modifiees | propre | | Transtypage `(object)` avant encodage JSON | verifie a l'execution | Tests ecrits avant le code, etat rouge constate. ## Limites assumees, ecrites dans la fiche - Une categorie creee apres coup n'est pas filtree tant que personne ne l'a renseignee, et rien ne le signale. - La correspondance se change par migration, pas depuis le back-office. - L'import de produits par fichier ne connait pas la famille (en-tete a liste fermee, un ajout casserait les gabarits distribues) : un ingredient importe reste non classe. - L'API JSON remplace la ressource entiere : une modification qui omet la famille la remet a non classe. Comportement deja en place pour les autres champs optionnels, aligne plutot qu'exceptionnel. Fiche de decision : `docs/adr/0018-familles-ingredients-filtre-recette.md` Journal : `docs/journal/2026-09-27--familles-ingredients-filtre-recette.md` Modele : dictionnaire 3.6 / 3.23 / note 16, MCD 5.1 / I8 / 5.3, MLD 4.6 / 4.23 (23 entites)
feat: classify ingredients by family and filter the recipe picker by product category
All checks were successful
CI / secret-scan (push) Successful in 21s
CI / php-lint (push) Successful in 25s
CI / static-tests (push) Successful in 2m26s
CI / js-tests (push) Successful in 38s
CI / shell-tests (push) Successful in 5s
CI / secret-scan (pull_request) Successful in 19s
CI / php-lint (pull_request) Successful in 24s
CI / static-tests (pull_request) Successful in 2m32s
CI / js-tests (pull_request) Successful in 40s
CI / shell-tests (pull_request) Successful in 7s
3adbabd1d8
The recipe builder offered all 50 catalogue ingredients flat, whatever the
product category: composing a burger proposed Brownie, Gobelet and Dose Coca
among the buns and patties.

Ingredients are classified, not removed. Brownie is a legitimate ingredient:
the Brownie product has 1x Brownie in its recipe, and that line is what
decrements its stock. Removing it would break dessert and drink stock tracking.

- ingredient.family (ten families) + category_ingredient_family, a real
  relation rather than a table hardcoded in a PHP class
- soft filter on both recipe screens: family optgroups, visible counter,
  "show all ingredients" escape, no server-side refusal of an off-family recipe
- unknown stays visible: null family, unmapped category, or unreadable
  correspondence data all mean "no filter", never an empty picker
- existing recipes are not rewritten, and the migration does not reclassify an
  ingredient already carrying a family
- family in the ingredient form and in the admin JSON API, same validation on
  both surfaces; Postman and Bruno collections regenerated from their script
- closes the is_food modelling limit left open by ADR-0015: the contenant
  family covers Gobelet without a redundant column

Dictionary, MCD and MLD updated (23 entities), ADR-0018 and a journal entry.
PHPUnit 1725 green, PHPStan level 6 clean, 374 JS tests green.
Corentin scheduled this pull request to auto merge when all checks succeed 2026-09-27 12:01:03 +02:00
Sign in to join this conversation.
No reviewers
No labels
auto-merge
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
AcadeNice/corentin_wakdo!174
No description provided.