From 03ef99d67b523292ce7716c140a165dd677bad83 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Wed, 24 Jun 2026 12:33:12 +0000 Subject: [PATCH 1/4] feat(back-office): page Stock en tableau de bord (alertes + reappro en avant) Refonte de la page d'accueil Ingredients/Stock, jugee trop chargee et opaque. Desormais : un bandeau explique le lien stock -> disponibilite borne (un ingredient requis sous le seuil critique rend les produits qui l'utilisent indisponibles a la commande, RG-T21) ; un resume compte les ingredients critiques / en alerte / au-dessus du seuil ; une section "A reapprovisionner" met en avant les ingredients bas (critiques d'abord) avec barre de niveau + bouton Reapprovisionner direct ; la liste complete passe au second plan et le CRUD (creer / modifier / supprimer) est relegue. Les sous-pages (reappro, inventaire, mouvements, creation) restent inchangees. index() expose les compteurs par etat (testables). Tests : IngredientController +4 cas (bandeau, promotion d'un critique en section reappro, etat vide positif, compteurs par etat). PHP unit 409, JS 119, PHPStan L6. --- src/app/Controllers/IngredientController.php | 14 +- src/app/Views/admin/ingredients/index.php | 230 ++++++++++++------ src/public/admin/assets/css/admin.css | 226 +++++++++++++++++ tests/Unit/Admin/IngredientControllerTest.php | 58 +++++ 4 files changed, 453 insertions(+), 75 deletions(-) diff --git a/src/app/Controllers/IngredientController.php b/src/app/Controllers/IngredientController.php index d272e90..d4f581d 100644 --- a/src/app/Controllers/IngredientController.php +++ b/src/app/Controllers/IngredientController.php @@ -47,10 +47,22 @@ class IngredientController extends AdminController return $guard; } + $ingredients = $this->ingredientRepository()->all(); + + // Compteurs par bande pour le resume du tableau de bord (3 pastilles). + // Calcules cote serveur a partir de stock_band deja resolu par le depot, + // pour que la vue reste declarative et la valeur testable directement. + $counts = ['critical' => 0, 'low' => 0, 'normal' => 0]; + foreach ($ingredients as $row) { + $band = (string) ($row['stock_band'] ?? 'normal'); + $counts[$band] = ($counts[$band] ?? 0) + 1; + } + return $this->adminView('admin/ingredients/index', [ 'title' => 'Stock - Wakdo Admin', 'activeNav' => 'stock', - 'ingredients' => $this->ingredientRepository()->all(), + 'ingredients' => $ingredients, + 'bandCounts' => $counts, 'canManage' => $this->may($guard, 'ingredient.manage'), 'canRestock' => $this->may($guard, 'stock.manage'), 'canCount' => $this->may($guard, 'stock.count'), diff --git a/src/app/Views/admin/ingredients/index.php b/src/app/Views/admin/ingredients/index.php index 3204d7c..2b1e896 100644 --- a/src/app/Views/admin/ingredients/index.php +++ b/src/app/Views/admin/ingredients/index.php @@ -3,11 +3,15 @@ declare(strict_types=1); /** - * Liste du stock (READ_STOCK 9.3), injectee dans admin/layout.php. Affiche le - * pourcentage et la bande calcules (RG-2) ; les liens d'action sont conditionnes - * aux permissions (la garde reelle reste par-route). Texte echappe. + * Tableau de bord stock (READ_STOCK 9.3), injecte dans admin/layout.php. Oriente + * usage quotidien : on met en avant ce qui est bas a reapprovisionner, le CRUD de + * definition (config rare) est relegue. Le lien metier explique a quoi sert le stock : + * un ingredient requis sous le seuil critique rend les produits qui l'utilisent + * indisponibles sur la borne (RG-T21). Pourcentage/bande resolus cote depot ; les + * liens d'action restent conditionnes aux permissions (garde reelle par-route). Texte echappe. * * @var array> $ingredients + * @var array $bandCounts * @var bool $canManage * @var bool $canRestock * @var bool $canCount @@ -16,94 +20,172 @@ declare(strict_types=1); /** @var array> $rows */ $rows = isset($ingredients) && is_array($ingredients) ? $ingredients : []; +/** @var array $counts */ +$counts = isset($bandCounts) && is_array($bandCounts) ? $bandCounts : []; $esc = static fn (mixed $v): string => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); $csrf = htmlspecialchars($csrfToken ?? '', ENT_QUOTES, 'UTF-8'); $manage = (bool) ($canManage ?? false); $restock = (bool) ($canRestock ?? false); $count = (bool) ($canCount ?? false); -$bandLabel = static fn (string $band): string => match ($band) { - 'critical' => 'pill pill-danger', - 'low' => 'pill pill-warning', - default => 'pill pill-success', +$nCritical = (int) ($counts['critical'] ?? 0); +$nLow = (int) ($counts['low'] ?? 0); +$nNormal = (int) ($counts['normal'] ?? 0); + +// Les ingredients a reapprovisionner : critiques d'abord, puis en alerte. Le reste +// (au-dessus des seuils) va dans la liste calme "Tous les ingredients" plus bas. +$critical = []; +$low = []; +foreach ($rows as $row) { + $band = (string) ($row['stock_band'] ?? 'normal'); + if ($band === 'critical') { + $critical[] = $row; + } elseif ($band === 'low') { + $low[] = $row; + } +} +$toRestock = array_merge($critical, $low); + +$barClass = static fn (string $band): string => match ($band) { + 'critical' => 'stock-bar__fill stock-bar--critical', + 'low' => 'stock-bar__fill stock-bar--low', + default => 'stock-bar__fill stock-bar--normal', }; -$bandText = static fn (string $band): string => match ($band) { - 'critical' => 'Critique', - 'low' => 'Alerte', - default => 'Normal', + +/** + * Barre de niveau : conteneur + portion remplie (largeur = pct%, couleur = bande). + * La largeur est bornee a 100 pour rester dans le conteneur meme si le depot renvoie + * un pourcentage superieur. Style inline pour la largeur (deja la convention admin). + * + * @param array $row + */ +$renderBar = static function (array $row) use ($esc, $barClass): string { + $pct = (int) ($row['stock_pct'] ?? 0); + $width = max(0, min(100, $pct)); + $band = (string) ($row['stock_band'] ?? 'normal'); + $qty = (int) ($row['stock_quantity'] ?? 0); + $cap = (int) ($row['stock_capacity'] ?? 0); + + $state = match ($band) { + 'critical' => 'critique', + 'low' => 'en alerte', + default => 'au-dessus du seuil', + }; + $html = ''; + $html .= '
' . $pct . '%'; + $html .= '' . $esc((string) $qty) . ' / ' . $esc((string) $cap) . '
'; + + return $html; }; ?> -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
IngredientUniteStockNiveauStatut
Aucun ingredient.
- - / (%) - - - Actif - - Inactif - - - Mouvements - - Reappro - - - Inventaire - - - Modifier -
- - -
- Supprimer - -
+

+ Le stock pilote ce qui est commandable sur la borne. Un ingredient requis par une + recette qui passe sous son seuil critique rend les produits qui l utilisent + indisponibles a la commande. Tenez les niveaux a jour pour garder le menu ouvert. +

+ +
+
+ + critiques +
+
+ + en alerte +
+
+ + au-dessus du seuil
+ +
+

A reapprovisionner

+ +
+ Tous les ingredients sont au-dessus de leurs seuils. +
+ +
+ + +
+
+
+ + +
+ +
+ + + Reapprovisionner + +
+ +
+ +
+ +
+

Tous les ingredients

+ +
Aucun ingredient.
+ + + +
diff --git a/src/public/admin/assets/css/admin.css b/src/public/admin/assets/css/admin.css index 7e77e48..d563c17 100644 --- a/src/public/admin/assets/css/admin.css +++ b/src/public/admin/assets/css/admin.css @@ -1813,3 +1813,229 @@ tbody td.mono { gap: 10px; margin-top: 16px; } + +/* --- Stock dashboard (page d'accueil ingredients) --- */ +.stock-explainer { + background: var(--color-yellow-bg); + border: 1px solid var(--color-yellow-soft); + border-radius: var(--radius-lg); + padding: 12px 16px; + font-size: 13px; + line-height: 1.5; + color: var(--color-text-sec); + margin-bottom: 20px; +} + +.stock-summary { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-bottom: 28px; +} + +.stock-summary__item { + display: flex; + align-items: baseline; + gap: 10px; + background: var(--color-white); + border: 1px solid var(--color-border); + border-left-width: 4px; + border-radius: var(--radius-card); + padding: 18px 22px; + box-shadow: var(--shadow-card); +} + +.stock-summary__item--danger { border-left-color: var(--color-danger); } +.stock-summary__item--warning { border-left-color: var(--color-warning); } +.stock-summary__item--success { border-left-color: var(--color-success); } + +.stock-summary__count { + font-size: 28px; + font-weight: 700; + line-height: 1; + color: var(--color-text); +} + +.stock-summary__item--danger .stock-summary__count { color: var(--color-danger-text); } +.stock-summary__item--warning .stock-summary__count { color: var(--color-warning-text); } +.stock-summary__item--success .stock-summary__count { color: var(--color-success-text); } + +.stock-summary__label { + font-size: 13px; + color: var(--color-text-muted); +} + +.stock-section { + margin-bottom: 32px; +} + +.stock-section__title { + font-size: 16px; + font-weight: 700; + color: var(--color-text); + margin-bottom: 14px; +} + +.stock-empty { + background: var(--color-white); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 20px; + font-size: 14px; + color: var(--color-text-muted); +} + +.stock-empty--ok { + background: var(--color-success-bg); + border-color: var(--color-success-bg); + color: var(--color-success-text); +} + +/* Barre de niveau : conteneur gris + portion remplie coloree selon la bande. */ +.stock-bar { + height: 8px; + background: var(--color-neutral-bg); + border-radius: var(--radius-sm); + overflow: hidden; +} + +.stock-bar__fill { + display: block; + height: 100%; + border-radius: var(--radius-sm); +} + +.stock-bar--critical { background: var(--color-danger); } +.stock-bar--low { background: var(--color-warning); } +.stock-bar--normal { background: var(--color-success); } + +.stock-bar__meta { + display: flex; + justify-content: space-between; + margin-top: 6px; + font-size: 12px; +} + +.stock-bar__pct { font-weight: 600; color: var(--color-text); } +.stock-bar__qty { color: var(--color-text-muted); } + +/* Section "A reapprovisionner" : cartes mises en avant. */ +.stock-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr)); + gap: 16px; +} + +.stock-card { + background: var(--color-white); + border: 1px solid var(--color-border); + border-top-width: 3px; + border-radius: var(--radius-card); + padding: 16px 18px; + box-shadow: var(--shadow-card); + display: flex; + flex-direction: column; + gap: 12px; +} + +.stock-card--critical { border-top-color: var(--color-danger); } +.stock-card--low { border-top-color: var(--color-warning); } + +.stock-card__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.stock-card__name { + font-size: 15px; + font-weight: 600; + color: var(--color-text); +} + +.stock-card__unit { + font-size: 12px; + color: var(--color-text-muted); + margin-left: 6px; +} + +.stock-card__action { + height: 44px; + justify-content: center; + width: 100%; +} + +/* Section "Tous les ingredients" : liste calme, actions secondaires discretes. */ +.stock-list { + list-style: none; + background: var(--color-white); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.stock-list__row { + display: grid; + grid-template-columns: 1fr 200px auto; + align-items: center; + gap: 20px; + padding: 14px 18px; + border-bottom: 1px solid var(--color-border); +} + +.stock-list__row:last-child { + border-bottom: none; +} + +.stock-list__main { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.stock-list__name { + font-weight: 600; + color: var(--color-text); +} + +.stock-list__unit { + font-size: 12px; + color: var(--color-text-muted); +} + +.stock-list__actions { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.stock-list__crud { + display: inline-flex; + align-items: center; + gap: 6px; + padding-left: 6px; + margin-left: 2px; + border-left: 1px solid var(--color-border); +} + +.stock-list__inline-form { + display: inline; +} + +@media (max-width: 900px) { + .stock-summary { + grid-template-columns: 1fr; + } + .stock-list__row { + grid-template-columns: 1fr; + gap: 10px; + } + .stock-list__actions { + justify-content: flex-start; + } +} diff --git a/tests/Unit/Admin/IngredientControllerTest.php b/tests/Unit/Admin/IngredientControllerTest.php index 92af9ea..6601e3d 100644 --- a/tests/Unit/Admin/IngredientControllerTest.php +++ b/tests/Unit/Admin/IngredientControllerTest.php @@ -198,6 +198,64 @@ final class IngredientControllerTest extends TestCase self::assertStringContainsString('Alerte', $response->body()); } + public function testIndexShowsBusinessExplainerBanner(): void + { + // Le bandeau explique le lien metier stock -> disponibilite borne (RG-T21), + // l'info qui manquait dans l'ancien tableau brut. + $db = $this->permittedDb(); + $db->ingredientsRows = [$this->ingredient()]; + + $body = $this->controller($this->get('/admin/ingredients'), $db)->index()->body(); + + self::assertStringContainsString('stock-explainer', $body); + self::assertStringContainsString('borne', $body); + } + + public function testIndexPromotesLowAndCriticalIntoRestockSection(): void + { + // Un ingredient critique (3% < seuil 5) doit apparaitre dans la section + // "A reapprovisionner" mise en avant, pas seulement dans la liste calme. + $db = $this->permittedDb(); + $db->ingredientsRows = [$this->ingredient(['name' => 'Buns', 'stock_quantity' => 3])]; + + $body = $this->controller($this->get('/admin/ingredients'), $db)->index()->body(); + + self::assertStringContainsString('A reapprovisionner', $body); + self::assertStringContainsString('stock-section--restock', $body); + self::assertStringContainsString('Buns', $body); + self::assertStringContainsString('Critique', $body); + } + + public function testIndexShowsPositiveEmptyStateWhenNothingLow(): void + { + // Tous au-dessus des seuils -> etat vide positif dans la section restock. + $db = $this->permittedDb(); + $db->ingredientsRows = [$this->ingredient(['stock_quantity' => 100])]; // 100% -> normal + + $body = $this->controller($this->get('/admin/ingredients'), $db)->index()->body(); + + self::assertStringContainsString('au-dessus de leurs seuils', $body); + } + + public function testIndexCountsIngredientsPerBand(): void + { + // Resume en haut : 1 critique (3%), 1 alerte (8%), 1 au-dessus (100%). + $db = $this->permittedDb(); + $db->ingredientsRows = [ + $this->ingredient(['name' => 'Buns', 'stock_quantity' => 3]), + $this->ingredient(['name' => 'Cheddar', 'stock_quantity' => 8]), + $this->ingredient(['name' => 'Salade', 'stock_quantity' => 100]), + ]; + + $body = $this->controller($this->get('/admin/ingredients'), $db)->index()->body(); + + // Chaque compteur est verrouille a SON libelle (sinon une regex generique + // passerait meme avec les trois compteurs inverses). + self::assertMatchesRegularExpression('/stock-summary__count">1<\/span>\s*critiques/', $body); + self::assertMatchesRegularExpression('/stock-summary__count">1<\/span>\s*en alerte/', $body); + self::assertMatchesRegularExpression('/stock-summary__count">1<\/span>\s*au-dessus du seuil/', $body); + } + public function testIndexForbiddenWithoutStockRead(): void { $db = $this->permittedDb(); -- 2.45.3 From 10440bb266c567c24e2355681f23dcffa8367254 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 25 Jun 2026 07:58:19 +0000 Subject: [PATCH 2/4] docs(merise): synchronise le referentiel avec la base deployee (5 migrations additives + order_number reel) --- docs/merise/_diagrams/mcd-catalogue.mmd | 5 ++ .../_diagrams/mcd-ingredients-stock.mmd | 3 + docs/merise/_diagrams/mcd-order.mmd | 1 + docs/merise/_diagrams/mld-catalogue.mmd | 5 ++ .../_diagrams/mld-ingredients-stock.mmd | 3 + docs/merise/_diagrams/mld-order.mmd | 1 + docs/merise/dictionary.md | 79 +++++++++++++++++-- docs/merise/mcd.md | 22 +++++- docs/merise/mct.md | 4 +- docs/merise/mld.md | 63 +++++++++++++-- docs/merise/mlt.md | 4 +- docs/uml/use-cases.md | 2 +- 12 files changed, 171 insertions(+), 21 deletions(-) diff --git a/docs/merise/_diagrams/mcd-catalogue.mmd b/docs/merise/_diagrams/mcd-catalogue.mmd index 698a179..71cb272 100644 --- a/docs/merise/_diagrams/mcd-catalogue.mmd +++ b/docs/merise/_diagrams/mcd-catalogue.mmd @@ -13,6 +13,9 @@ erDiagram varchar name text description int price_cents + int maxi_variant_product_id FK + smallint size_cl + int base_product_id FK smallint vat_rate varchar image_path tinyint is_available @@ -46,6 +49,8 @@ erDiagram category ||--o{ product : "groups" category ||--o{ menu : "groups" menu ||--|| product : "anchors (burger_product_id)" + product ||--o{ product : "maxi_variant (maxi_variant_product_id)" + product ||--o{ product : "size_variant_of (base_product_id)" menu ||--o{ menu_slot : "defines_slot" menu_slot ||--o{ menu_slot_option : "lists" product ||--o{ menu_slot_option : "is_eligible_for" diff --git a/docs/merise/_diagrams/mcd-ingredients-stock.mmd b/docs/merise/_diagrams/mcd-ingredients-stock.mmd index dff0eee..f8de675 100644 --- a/docs/merise/_diagrams/mcd-ingredients-stock.mmd +++ b/docs/merise/_diagrams/mcd-ingredients-stock.mmd @@ -11,6 +11,9 @@ erDiagram int stock_capacity smallint pack_size varchar pack_label + smallint energy_kcal_100g + varchar nutrition_source + datetime nutrition_fetched_at smallint low_stock_pct smallint critical_stock_pct tinyint is_active diff --git a/docs/merise/_diagrams/mcd-order.mmd b/docs/merise/_diagrams/mcd-order.mmd index be06a07..c3b34f5 100644 --- a/docs/merise/_diagrams/mcd-order.mmd +++ b/docs/merise/_diagrams/mcd-order.mmd @@ -6,6 +6,7 @@ erDiagram enum source int acting_user_id FK enum service_mode + varchar service_tag enum status int total_ht_cents int total_vat_cents diff --git a/docs/merise/_diagrams/mld-catalogue.mmd b/docs/merise/_diagrams/mld-catalogue.mmd index 7519fdb..5837997 100644 --- a/docs/merise/_diagrams/mld-catalogue.mmd +++ b/docs/merise/_diagrams/mld-catalogue.mmd @@ -11,6 +11,9 @@ erDiagram int category_id FK varchar name int price_cents + int maxi_variant_product_id FK + smallint size_cl + int base_product_id FK smallint vat_rate tinyint is_available smallint display_order @@ -41,6 +44,8 @@ erDiagram category ||--o{ product : "category_id (RESTRICT)" category ||--o{ menu : "category_id (RESTRICT)" product ||--o{ menu : "burger_product_id (RESTRICT)" + product ||--o{ product : "maxi_variant_product_id (SET NULL)" + product ||--o{ product : "base_product_id (CASCADE)" menu ||--o{ menu_slot : "menu_id (CASCADE)" menu_slot ||--o{ menu_slot_option : "menu_slot_id (CASCADE)" product ||--o{ menu_slot_option : "product_id (RESTRICT)" diff --git a/docs/merise/_diagrams/mld-ingredients-stock.mmd b/docs/merise/_diagrams/mld-ingredients-stock.mmd index 556f22f..65b8731 100644 --- a/docs/merise/_diagrams/mld-ingredients-stock.mmd +++ b/docs/merise/_diagrams/mld-ingredients-stock.mmd @@ -6,6 +6,9 @@ erDiagram int stock_quantity int stock_capacity smallint pack_size + smallint energy_kcal_100g + varchar nutrition_source + datetime nutrition_fetched_at smallint low_stock_pct smallint critical_stock_pct tinyint is_active diff --git a/docs/merise/_diagrams/mld-order.mmd b/docs/merise/_diagrams/mld-order.mmd index f6ec1e3..68465aa 100644 --- a/docs/merise/_diagrams/mld-order.mmd +++ b/docs/merise/_diagrams/mld-order.mmd @@ -6,6 +6,7 @@ erDiagram enum source int acting_user_id FK enum service_mode + varchar service_tag enum status int total_ht_cents int total_vat_cents diff --git a/docs/merise/dictionary.md b/docs/merise/dictionary.md index e45529d..51e938e 100644 --- a/docs/merise/dictionary.md +++ b/docs/merise/dictionary.md @@ -4,7 +4,7 @@ **Version** : v0.3 — prod-like, 22 entites (19 prod-like + couche security-by-design, incl. les entites `login_throttle` et `pin_throttle`) **Date** : 2026-06-04 (ajouts security-by-design 2026-06-11) **Branche** : `feat/p1-conception` -**Statut** : prod-like — toutes les decisions D1-D8 + stock appliquees (voir `docs/notes/revue-alignement-p1.md` §7) ; couche security-by-design en cours (voir note 13) +**Statut** : prod-like — toutes les decisions D1-D8 + stock appliquees (voir `docs/notes/revue-alignement-p1.md` §7) ; couche security-by-design en cours (voir note 13) ; colonnes additives post-v0.3 des migrations 0003/0005/0006/0007 alignees sur le deploye (voir note 14) **Auteur** : BYAN (couche methodologie) --- @@ -114,6 +114,9 @@ Un article vendable unique, disponible a la carte ou comme composant dans un slo | `name` | VARCHAR(120) | NO | — | INDEX | `nom` | renomme depuis `nom` | | `description` | TEXT | YES | NULL | — | (ajoute) | renseigne plus tard via l'admin | | `price_cents` | INT UNSIGNED | NO | — | CHECK > 0 | `prix` (FLOAT) | conversion FLOAT -> INT centimes au seed (voir note 1) | +| `maxi_variant_product_id` | INT UNSIGNED | YES | NULL | FK -> `product(id)`, ON DELETE SET NULL | (migration 0006) | auto-reference : variante servie quand un menu est commande au format Maxi (ex. Moyenne Frite -> Grande Frite). Data-driven (la regle vit dans la donnee). SET NULL = degradation gracieuse : si la variante Grande est retiree du catalogue, le produit de base reste vendable, il perd seulement sa substitution Maxi. Voir note 14 | +| `size_cl` | SMALLINT UNSIGNED | YES | NULL | — | (migration 0007) | variante de TAILLE a la carte : volume en centilitres d'une boisson fontaine (ex. 30 / 50 cl). NULL = produit sans dimension taille (bouteille, non-boisson). La ligne de base ET la variante portent leur volume pour l'affichage du picker. Voir note 14 | +| `base_product_id` | INT UNSIGNED | YES | NULL | FK -> `product(id)`, ON DELETE CASCADE | (migration 0007) | auto-reference vers la ligne de base d'une variante de taille. NULL = produit de base ou autonome (visible dans la grille catalogue) ; NON NULL = variante de taille (masquee de la grille, atteinte via le picker). CASCADE : une variante de taille n'a pas de sens sans sa base (suppression de la base -> suppression de ses variantes). Voir note 14 | | `vat_rate` | SMALLINT UNSIGNED | NO | 100 | CHECK IN (55, 100) | (ajoute) | taux de TVA en pour-mille : 100 = 10%, 55 = 5,5%. Defaut 10%. Voir note 9 | | `image_path` | VARCHAR(255) | YES | NULL | — | `image` | chemin relatif, voir note 8 | | `is_available` | TINYINT(1) | NO | 1 | — | (ajoute) | bascule de disponibilite manuelle depuis l'admin | @@ -197,6 +200,9 @@ Ingredient elementaire utilise dans la composition des produits. Porte les donne | `stock_capacity` | INT | NO | — | CHECK > 0 | niveau "plein" de reference en unites = les 100% servant a calculer le pourcentage de stock. Le `CHECK > 0` protege aussi la division du pourcentage contre la division par zero | | `pack_size` | SMALLINT UNSIGNED | NO | 1 | CHECK > 0 | unites par pack de reapprovisionnement (ex. 100 pour un sac de 100 portions) | | `pack_label` | VARCHAR(80) | YES | NULL | — | libelle humain du pack (ex. "Sac 100 portions") | +| `energy_kcal_100g` | SMALLINT UNSIGNED | YES | NULL | — | enrichissement nutritionnel (migration 0005) : apport energetique pour 100 g, importe depuis l'API externe OpenFoodFacts (Cr 3.a.3). Nullable : un ingredient non enrichi reste valide. Voir note 14 | +| `nutrition_source` | VARCHAR(120) | YES | NULL | — | enrichissement nutritionnel (migration 0005) : provenance de la donnee (ex. "OpenFoodFacts"). Voir note 14 | +| `nutrition_fetched_at` | DATETIME | YES | NULL | — | enrichissement nutritionnel (migration 0005) : horodatage de l'import, pour tracer la fraicheur. Voir note 14 | | `low_stock_pct` | SMALLINT UNSIGNED | NO | 10 | CHECK BETWEEN 0 AND 100 | bande d’alerte, pourcentage de capacite : `stock_quantity <= stock_capacity * low_stock_pct/100` declenche l'indicateur de stock bas | | `critical_stock_pct` | SMALLINT UNSIGNED | NO | 5 | CHECK BETWEEN 0 AND 100 | seuil de rupture automatique, pourcentage de capacite : `stock_quantity <= stock_capacity * critical_stock_pct/100` rend le produit calcule en rupture | | `is_active` | TINYINT(1) | NO | 1 | — | desactiver les ingredients obsoletes sans supprimer | @@ -281,11 +287,12 @@ Transaction client : 1 commande = 1 panier valide a un instant donne. | Attribut | Type | NULL | Default | Contrainte | Notes | |---|---|---|---|---|---| | `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | -| `order_number` | VARCHAR(20) | NO | — | UNIQUE | format lisible par l'humain : `K`/`C`/`D`-YYYY-MM-DD-NNN. Prefixe par canal : K=kiosk, C=counter, D=drive. Voir note 4. | +| `order_number` | VARCHAR(20) | NO | — | UNIQUE | numero lisible : prefixe canal + id sequentiel, soit `K` / `C` / `D` (K=kiosk, C=counter, D=drive). Ecrit en deux temps (INSERT puis UPDATE avec `LAST_INSERT_ID()`). Voir note 4. | | `idempotency_key` | VARCHAR(36) | YES | NULL | UNIQUE | UUID genere par le client pour dedupliquer un `POST /api/orders` reessaye (anti-double-charge). UNIQUE rejette les doublons ; plusieurs NULL autorises. Security-by-design, voir note 13 | | `source` | ENUM('kiosk','counter','drive') | NO | — | INDEX | canal de saisie (qui a saisi la commande). Valeurs en anglais, voir note 5. | | `acting_user_id` | INT UNSIGNED | YES | NULL | FK -> `user(id)`, ON DELETE SET NULL | personnel back-office (counter/drive) ayant cree la commande, capture sous PIN. NULL pour `kiosk` (anonyme). Imputabilite ciblee sans imposer un login par personne sur la borne. Voir note 13 | | `service_mode` | ENUM('dine_in','takeaway','drive') | NO | — | — | mode de consommation, conserve pour les stats/KPI uniquement. Aucun role fiscal (voir note 9). La source `drive` implique le service_mode `drive` (contrainte croisee appliquee au niveau applicatif). | +| `service_tag` | VARCHAR(20) | YES | NULL | — | numero de chevalet pour le service EN SALLE (migration 0003), saisi a la borne quand le client choisit `dine_in` ; permet d'apporter la commande a la bonne table (B4). NULL pour `takeaway` / `drive`. Voir note 14 | | `status` | ENUM('pending_payment','paid','delivered','cancelled') | NO | 'pending_payment' | INDEX | machine a 4 etats : `pending_payment -> paid -> delivered` (+ `cancelled`). Voir note 6. | | `total_ht_cents` | INT UNSIGNED | NO | — | CHECK >= 0 | total hors TVA, snapshot a la validation de la commande | | `total_vat_cents` | INT UNSIGNED | NO | — | CHECK >= 0 | montant de TVA, snapshot | @@ -687,15 +694,28 @@ et evite tous les conflits. `order_item` est conserve comme nom de table de ligne : `item` n'est pas reserve, et le prefixe `order_` rend claire la relation parent. -### Note 4 — Prefixe de numero de commande par canal +### Note 4 — Prefixe de numero de commande par canal (existant : prefixe + id) -Format : `K`/`C`/`D`-YYYY-MM-DD-NNN (kiosk / counter / drive). +Format reel (decision utilisateur) : prefixe canal + id de la commande, soit `K` / `C` / +`D` (kiosk / counter / drive). Implemente dans `src/app/Order/OrderRepository.php` : la commande +est inseree avec un `order_number` provisoire vide, puis l'`order_number` est ecrit en `prefix . +LAST_INSERT_ID()` (ex. `K42`, `C7`, `D13`). -Rationale : le prefixe encode le canal, ce qui est utile pour une identification visuelle rapide -par le personnel cuisine et comptoir sans interroger la colonne `source`. Le compteur sequentiel NNN -repart chaque jour par canal. Sans collision sur une journee, vu le volume attendu. +Rationale : le prefixe encode le canal, utile pour une identification visuelle rapide par le personnel +cuisine et comptoir sans interroger la colonne `source`. Le suffixe est l'id sequentiel auto-incremente : +pas de compteur quotidien a tenir ni de `service_day` a gerer cote numerotation (rasoir d'Ockham, +mantra #37). -Alternative rejetee : prefixe neutre `W-` pour tous les canaux (plus simple, mais perd la lisibilite +Ecart assume avec la cible v0.x initiale `K-AAAA-MM-JJ-NNN` (compteur journalier par canal) : +cette derniere n'a pas ete retenue a l'implementation, jugee plus lourde sans valeur metier +proportionnelle pour le volume attendu. La forme acte ici est celle qui tourne. + +Compromis connu : un numero `prefixe + id` est sequentiel, donc devinable (un client peut incrementer +l'id). Couple a l'endpoint de paiement anonyme cote borne (lecture/encaissement par `order_number` +sans authentification), c'est une surface a surveiller. Piste d'amelioration : numero non sequentiel +(ex. suffixe aleatoire court) si le suivi anonyme par numero devait s'ouvrir davantage. + +Alternative rejetee : prefixe neutre `W` pour tous les canaux (plus simple, mais perd la lisibilite du canal pour le personnel). ### Note 5 — `source` vs `service_mode` (canal vs mode de consommation) @@ -910,6 +930,49 @@ est un backoff degressif aux bornes propres (PIN_THROTTLE_*). Meme purge cron qu References : `docs/notes/revue-alignement-p1.md` §7 (decisions D), carte d'impact security-by-design (2026-06-11). Modele de menace et matrice de classification des donnees : `PROJECT_CONTEXT.md` §19 (a venir). +### Note 14 — Colonnes additives post-v0.3 (migrations 0003 / 0005 / 0006 / 0007) + +Ces colonnes etendent le schema apres v0.3 par des migrations purement additives (ajout de colonnes +nullables et de FK auto-referentes ; aucune donnee existante a retro-remplir, aucune table nouvelle). +Le runner applique les `*.sql` dans l'ordre lexicographique via `schema_migrations`. Elles sont alignees +ici sur le schema reellement deploye. + +**Migration 0003 — `customer_order.service_tag` VARCHAR(20) NULL (AFTER service_mode).** Numero de +chevalet pour le service en salle (mode `dine_in`), saisi a la borne ; NULL pour `takeaway` / `drive`. +Permet d'apporter la commande a la bonne table (B4). Entite 3.10. + +**Migration 0005 — enrichissement nutritionnel de `ingredient` (AFTER pack_label).** +`energy_kcal_100g` SMALLINT UNSIGNED NULL, `nutrition_source` VARCHAR(120) NULL, +`nutrition_fetched_at` DATETIME NULL. Donnees importees depuis l'API externe OpenFoodFacts (Cr 3.a.3 : +exploitation d'informations externes dans le modele de donnees). Opt-in et egress maitrise : aucun appel +automatique au runtime borne ; la passerelle (`App\Catalogue\OpenFoodFactsGateway`) est invoquee seulement +par `IngredientController::enrich` (action explicite manager/admin). Toutes nullables : un ingredient non +enrichi reste valide. Entite 3.6. + +**Migration 0006 — `product.maxi_variant_product_id` INT UNSIGNED NULL, FK -> `product(id)` ON DELETE +SET NULL (AFTER price_cents).** Auto-reference : variante servie quand un menu est commande au format +Maxi (ex. Moyenne Frite -> Grande Frite), substituee cote serveur dans `OrderRepository::resolveSelections` +sans choix supplementaire. Approche data-driven (la regle vit dans la donnee, pas dans le code), et le +decrement de stock frappe alors le bon produit. SET NULL plutot que RESTRICT : si la variante Grande est +supprimee du catalogue, le produit de base reste vendable et perd seulement sa substitution Maxi +(degradation gracieuse) ; la reference est un confort metier, pas une integrite forte de commande (les +commandes figent deja leurs snapshots). Entite 3.2. + +**Migration 0007 — variante de TAILLE de `product` (AFTER price_cents).** `size_cl` SMALLINT UNSIGNED +NULL, `base_product_id` INT UNSIGNED NULL avec FK -> `product(id)` ON DELETE CASCADE. La dimension taille +des boissons fontaine (la maquette borne propose 30 / 50 cl) est modelisee en lignes produit distinctes +(meme approche que Moyenne/Grande Frite) : le domaine commande facture deja par `product_id`, le flux reste +inchange, la borne resout la taille choisie en `product_id`. `base_product_id` NULL = produit de base ou +autonome (visible dans la grille catalogue) ; NON NULL = variante de taille (masquee de la grille, atteinte +via le picker). CASCADE plutot que SET NULL (a la difference de 0006) : une variante de taille n'a aucun +sens sans sa base (une "Coca Cola 50 cl" orpheline n'est pas commercialisable), donc supprimer la base +emporte ses variantes de taille. Les deux groupings coexistent sur une boisson sans se confondre : +`base_product_id` pilote la selection de taille a la carte (picker 30/50 cl) ; `maxi_variant_product_id` +(0006) pilote la substitution Maxi en menu. Entite 3.2. + +References : `db/migrations/0003_order_service_tag.sql`, `0005_ingredient_nutrition.sql`, +`0006_product_maxi_variant.sql`, `0007_product_size_variant.sql`. + --- ## 5. Synthese du decompte des entites diff --git a/docs/merise/mcd.md b/docs/merise/mcd.md index 20a667c..54c796b 100644 --- a/docs/merise/mcd.md +++ b/docs/merise/mcd.md @@ -111,6 +111,9 @@ erDiagram varchar name text description int price_cents + int maxi_variant_product_id FK + smallint size_cl + int base_product_id FK smallint vat_rate varchar image_path tinyint is_available @@ -144,6 +147,8 @@ erDiagram category ||--o{ product : "groups" category ||--o{ menu : "groups" menu ||--|| product : "anchors (burger_product_id)" + product ||--o{ product : "maxi_variant (maxi_variant_product_id)" + product ||--o{ product : "size_variant_of (base_product_id)" menu ||--o{ menu_slot : "defines_slot" menu_slot ||--o{ menu_slot_option : "lists" product ||--o{ menu_slot_option : "is_eligible_for" @@ -159,6 +164,8 @@ erDiagram | C4 | defines_slot | menu | (1,N) | menu_slot | (1,1) | Un menu doit definir au moins un slot (boisson, accompagnement, sauce) pour avoir une composition personnalisable. Un slot appartient a exactement un menu. | | C5 | lists | menu_slot | (1,N) | menu_slot_option | (1,1) | Un slot doit lister au moins un produit eligible (sinon le client ne peut pas le remplir). Chaque ligne d'option appartient a exactement un slot. | | C6 | is_eligible_for | product | (0,N) | menu_slot_option | (1,1) | Un produit peut etre eligible pour un nombre quelconque de slots a travers tous les menus, ou aucun s'il n'est vendu qu'a la carte. Chaque ligne d'option reference exactement un produit. | +| C7 | maxi_variant (migration 0006) | product (base) | (0,1) | product (variante Maxi) | (0,N) | Auto-reference : un produit pointe vers 0 ou 1 variante servie en menu Maxi (`maxi_variant_product_id`, nullable) ; un produit peut etre la variante Maxi de plusieurs autres. ON DELETE SET NULL (degradation gracieuse). | +| C8 | size_variant_of (migration 0007) | product (base) | (0,N) | product (variante de taille) | (0,1) | Auto-reference : une variante de taille pointe vers sa ligne de base (`base_product_id`, nullable ; NULL = base/autonome) ; un produit de base peut avoir plusieurs variantes de taille (30/50 cl). ON DELETE CASCADE (une variante de taille n'a pas de sens sans sa base). | ### 4.3 Notes sur le sous-domaine Catalogue @@ -168,6 +175,8 @@ erDiagram **Format Normal / Maxi** : deux prix (`price_normal_cents`, `price_maxi_cents`) sur `menu` ; format enregistre au niveau de `order_item.format`. Aucun differentiel de prix au niveau du slot individuel n'est stocke (voir note 7 du dictionnaire). +**Variantes de produit (migrations 0006 / 0007, voir note 14 du dictionnaire)** : deux auto-references sur `product`, distinctes par leur role et leur comportement `ON DELETE`. `maxi_variant_product_id` (SET NULL) designe la variante servie quand un menu est commande au format Maxi (ex. Moyenne Frite -> Grande Frite), substituee cote serveur. `size_cl` + `base_product_id` (CASCADE) modelisent une variante de TAILLE a la carte (boisson 30/50 cl) en lignes produit : `base_product_id` NULL = produit de base/autonome visible au catalogue, NON NULL = variante masquee de la grille et atteinte via le picker. Les deux groupings coexistent sur une boisson sans se confondre. + --- ## 5. Sous-domaine : Ingredients & Stock @@ -188,6 +197,9 @@ erDiagram int stock_capacity smallint pack_size varchar pack_label + smallint energy_kcal_100g + varchar nutrition_source + datetime nutrition_fetched_at smallint low_stock_pct smallint critical_stock_pct tinyint is_active @@ -262,6 +274,8 @@ erDiagram **Disponibilite produit calculee (regle RG-T21, voir `mlt.md`)** : la commandabilite effective est derivee, pas stockee. Un produit est commandable quand `product.is_available = 1` ET que chaque ingredient non retirable (`is_removable = 0`) de son `product_ingredient` a `stock_quantity > stock_capacity * critical_stock_pct / 100`. Un ingredient requis atteignant la bande critique met le produit en rupture automatique sans ecriture et sans cascade ; un retrait manuel (`product.is_available = 0`) est une surcharge forte ; un reapprovisionnement au-dessus de la bande critique rend le produit commandable a nouveau de lui-meme. Un ingredient retirable/optionnel a la bande critique ne bloque pas le produit (seul son supplement devient indisponible). Le tableau de bord distingue un retrait manuel d'une rupture pilotee par le stock. +**Enrichissement nutritionnel (migration 0005, voir note 14 du dictionnaire)** : `energy_kcal_100g`, `nutrition_source` et `nutrition_fetched_at` (toutes nullables) stockent une donnee importee depuis l'API externe OpenFoodFacts (Cr 3.a.3 : exploitation d'informations externes dans le modele de donnees). Opt-in : l'import est declenche par `IngredientController::enrich` (action manager/admin), pas au runtime borne ; un ingredient non enrichi reste valide. + --- ## 6. Sous-domaine : Order @@ -277,6 +291,7 @@ erDiagram enum source int acting_user_id FK enum service_mode + varchar service_tag enum status int total_ht_cents int total_vat_cents @@ -382,6 +397,11 @@ enregistre l'employe de comptoir/drive qui a pris la commande sous PIN ; NULL po Cela ajoute une association `customer_order |o--o| user : "taken_by"` (cardinalite : une commande est prise par (0,1) user ; un user prend (0,N) commandes). Voir note 13 du dictionnaire. +**Numero de commande (existant) et service en salle (migration 0003)** : `order_number` est un attribut +non cle (UNIQUE) de forme `prefixe canal + id` (`K` / `C` / `D`) ; pas une association. Voir +note 4 du dictionnaire. `service_tag` (VARCHAR(20), nullable) porte le numero de chevalet du service en +salle (mode `dine_in`), saisi a la borne ; NULL pour `takeaway` / `drive`. Voir note 14 du dictionnaire. + --- ## 7. Sous-domaine : RBAC @@ -580,7 +600,7 @@ Le MCD reste au niveau conceptuel. Les decisions suivantes sont reportees au MLD des PK composites. 2. **PK technique vs identifiant metier** : `id INT UNSIGNED AUTO_INCREMENT` sur toutes les entites principales. `customer_order` porte en plus `order_number VARCHAR(20) UNIQUE` (lisible par un humain, - format `K/C/D-YYYY-MM-DD-NNN` par canal). + format prefixe canal + id : `K` / `C` / `D`). 3. **Regles ON DELETE** : CASCADE vs RESTRICT vs SET NULL. Detaillees dans le MLD. 4. **Contraintes CHECK** : exclusivite de polymorphisme sur `order_item`, contrainte croisee `source/service_mode` sur `customer_order`, invariant arithmetique sur les totaux. diff --git a/docs/merise/mct.md b/docs/merise/mct.md index 0295db2..c09cf02 100644 --- a/docs/merise/mct.md +++ b/docs/merise/mct.md @@ -143,7 +143,7 @@ Pour chaque operation, le document fournit : | **Synchronisation** | AND (les deux actions requises) | | **Condition** | Le panier contient au moins 1 article. Le numero de commande saisi est non vide. | | **Operation** | CREATE_ORDER | -| **Description** | Creation atomique de la commande : INSERT `customer_order` avec statut `pending_payment`, source `kiosk`, snapshot des totaux HT/TVA/TTC (calcules ligne par ligne en utilisant `vat_rate` snapshote par article). INSERT des lignes `order_item` avec `label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot`. INSERT `order_item_selection` pour chaque slot rempli dans un article de menu. INSERT `order_item_modifier` pour chaque modification d'ingredient. Decrement de `ingredient.stock_quantity` pour chaque ingredient consomme (ajuste par les modificateurs : retrait => pas de decrement ; ajout => decrement supplementaire) ; INSERT d'une ligne `stock_movement` de type `sale` par unite d'ingredient affectee. Les decrements de stock et l'insertion de la commande sont dans la meme transaction. Apres que le client a saisi son numero de commande, le statut passe `pending_payment -> paid` dans la meme transaction ; `paid_at` est positionne. Le systeme genere le numero de commande au format `K-YYYY-MM-DD-NNN`. | +| **Description** | Creation atomique de la commande : INSERT `customer_order` avec statut `pending_payment`, source `kiosk`, snapshot des totaux HT/TVA/TTC (calcules ligne par ligne en utilisant `vat_rate` snapshote par article). INSERT des lignes `order_item` avec `label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot`. INSERT `order_item_selection` pour chaque slot rempli dans un article de menu. INSERT `order_item_modifier` pour chaque modification d'ingredient. Decrement de `ingredient.stock_quantity` pour chaque ingredient consomme (ajuste par les modificateurs : retrait => pas de decrement ; ajout => decrement supplementaire) ; INSERT d'une ligne `stock_movement` de type `sale` par unite d'ingredient affectee. Les decrements de stock et l'insertion de la commande sont dans la meme transaction. Apres que le client a saisi son numero de commande, le statut passe `pending_payment -> paid` dans la meme transaction ; `paid_at` est positionne. Le systeme genere le numero de commande au format prefixe canal + id (`K`, voir dictionnaire note 4). | | **Entites MCD** | R: `product`, `menu`, `ingredient`, `product_ingredient` (snapshot) — W: `customer_order` (INSERT status `pending_payment` then UPDATE status `paid`, `paid_at`), `order_item` (INSERT N lines), `order_item_selection` (INSERT per menu slot chosen), `order_item_modifier` (INSERT per modification), `ingredient` (UPDATE stock_quantity), `stock_movement` (INSERT type `sale` per unit) | | **Resultat** | Commande creee (statut `paid` en fin d'operation), numero de commande affiche au client, evenement logique ORDER_CREATED emis vers le domaine de preparation | @@ -175,7 +175,7 @@ Pour chaque operation, le document fournit : | **Synchronisation** | Aucune | | **Condition** | L'acteur est authentifie et detient la permission `order.create`. La `source` est `counter` ou `drive` (auto-taggee depuis `role.order_source`). | | **Operation** | CREATE_COUNTER_ORDER | -| **Description** | Composition manuelle de la commande via le back-office : selectionner produits et menus, choisir le mode de service (`dine_in`/`takeaway`/`drive`), remplir les slots de menu, ajouter des modificateurs d'ingredient. Logique de creation identique a CREATE_ORDER (snapshot, decrement de stock dans la meme transaction, transition atomique `pending_payment -> paid`). La `source` est auto-taggee depuis `role.order_source` (counter -> `counter`, drive -> `drive`). Format du numero de commande : `C-YYYY-MM-DD-NNN` (comptoir) ou `D-YYYY-MM-DD-NNN` (drive). Contrainte croisee : si `source = 'drive'` alors `service_mode = 'drive'` (verifie a la creation). | +| **Description** | Composition manuelle de la commande via le back-office : selectionner produits et menus, choisir le mode de service (`dine_in`/`takeaway`/`drive`), remplir les slots de menu, ajouter des modificateurs d'ingredient. Logique de creation identique a CREATE_ORDER (snapshot, decrement de stock dans la meme transaction, transition atomique `pending_payment -> paid`). La `source` est auto-taggee depuis `role.order_source` (counter -> `counter`, drive -> `drive`). Format du numero de commande : prefixe canal + id (`C` comptoir, `D` drive). Contrainte croisee : si `source = 'drive'` alors `service_mode = 'drive'` (verifie a la creation). | | **Entites MCD** | R: `product`, `menu`, `menu_slot`, `menu_slot_option`, `ingredient`, `product_ingredient` — W: `customer_order` (INSERT status `pending_payment` then UPDATE status `paid`, `paid_at`), `order_item`, `order_item_selection`, `order_item_modifier`, `ingredient` (stock decrement), `stock_movement` (INSERT type `sale`) | | **Resultat** | Commande creee (statut `paid`), numero de commande communique au client | diff --git a/docs/merise/mld.md b/docs/merise/mld.md index 8d0c57d..4da8bd1 100644 --- a/docs/merise/mld.md +++ b/docs/merise/mld.md @@ -125,6 +125,9 @@ erDiagram int category_id FK varchar name int price_cents + int maxi_variant_product_id FK + smallint size_cl + int base_product_id FK smallint vat_rate tinyint is_available smallint display_order @@ -155,6 +158,8 @@ erDiagram category ||--o{ product : "category_id (RESTRICT)" category ||--o{ menu : "category_id (RESTRICT)" product ||--o{ menu : "burger_product_id (RESTRICT)" + product ||--o{ product : "maxi_variant_product_id (SET NULL)" + product ||--o{ product : "base_product_id (CASCADE)" menu ||--o{ menu_slot : "menu_id (CASCADE)" menu_slot ||--o{ menu_slot_option : "menu_slot_id (CASCADE)" product ||--o{ menu_slot_option : "product_id (RESTRICT)" @@ -171,6 +176,9 @@ erDiagram int stock_quantity int stock_capacity smallint pack_size + smallint energy_kcal_100g + varchar nutrition_source + datetime nutrition_fetched_at smallint low_stock_pct smallint critical_stock_pct tinyint is_active @@ -235,6 +243,7 @@ erDiagram enum source int acting_user_id FK enum service_mode + varchar service_tag enum status int total_ht_cents int total_vat_cents @@ -410,11 +419,14 @@ Pas de FK. Table racine du sous-domaine Catalogue. ### 4.2 `product` ``` -product (id, #category_id, name, [description], price_cents, vat_rate, +product (id, #category_id, name, [description], price_cents, + [#maxi_variant_product_id], [size_cl], [#base_product_id], vat_rate, [image_path], is_available, display_order, created_at, updated_at) PK : id - FK : category_id -> category(id) ON DELETE RESTRICT + FK : category_id -> category(id) ON DELETE RESTRICT + FK : maxi_variant_product_id -> product(id) ON DELETE SET NULL + FK : base_product_id -> product(id) ON DELETE CASCADE IDX : (category_id, is_available, display_order) CHK : price_cents > 0 CHK : vat_rate IN (55, 100) @@ -427,6 +439,9 @@ product (id, #category_id, name, [description], price_cents, vat_rate, | `name` | VARCHAR(120) | NO | Libelle du produit | | `description` | TEXT | YES | Description longue optionnelle | | `price_cents` | INT UNSIGNED | NO | Prix a la carte, TVA incluse, en centimes | +| `maxi_variant_product_id` | INT UNSIGNED | YES | FK -> product (auto-reference), ON DELETE SET NULL ; variante servie en menu Maxi (migration 0006, voir note de table) | +| `size_cl` | SMALLINT UNSIGNED | YES | Volume en cl d'une variante de taille de boisson ; NULL si pas de dimension taille (migration 0007) | +| `base_product_id` | INT UNSIGNED | YES | FK -> product (auto-reference), ON DELETE CASCADE ; ligne de base d'une variante de taille, NULL = base/autonome (migration 0007) | | `vat_rate` | SMALLINT UNSIGNED | NO | Pour-mille : 100 = 10%, 55 = 5.5% | | `image_path` | VARCHAR(255) | YES | Chemin relatif depuis la racine publique | | `is_available` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Bascule de disponibilite manuelle | @@ -437,6 +452,19 @@ product (id, #category_id, name, [description], price_cents, vat_rate, **ON DELETE RESTRICT** sur `category_id` : une categorie avec des produits ne peut pas etre supprimee. Empeche les produits orphelins. +**Auto-references de variante (migrations 0006 / 0007, voir note 14 du dictionnaire)** : deux groupings +distincts, tous deux pointant vers `product(id)`. +- `maxi_variant_product_id` (ON DELETE SET NULL) : variante servie quand un menu est commande au format + Maxi (ex. Moyenne Frite -> Grande Frite). SET NULL = degradation gracieuse, le produit de base reste + vendable si la variante Grande est supprimee. +- `size_cl` + `base_product_id` (ON DELETE CASCADE) : variante de TAILLE a la carte (boisson 30/50 cl) + modelisee en lignes produit. `base_product_id` NULL = produit de base/autonome (visible catalogue) ; + NON NULL = variante de taille (masquee de la grille, atteinte via le picker). CASCADE car une variante + de taille n'a pas de sens sans sa base. + +Les deux coexistent sur une boisson sans se confondre : `base_product_id` pilote la selection de taille a +la carte ; `maxi_variant_product_id` pilote la substitution Maxi en menu. + --- ### 4.3 `menu` @@ -529,6 +557,7 @@ Pas d'horodatages. Table de jointure pure. ``` ingredient (id, name, unit, stock_quantity, stock_capacity, pack_size, [pack_label], + [energy_kcal_100g], [nutrition_source], [nutrition_fetched_at], low_stock_pct, critical_stock_pct, is_active, created_at, updated_at) PK : id @@ -549,6 +578,9 @@ ingredient (id, name, unit, stock_quantity, stock_capacity, pack_size, [pack_lab | `stock_capacity` | INT NOT NULL | NO | Niveau "plein" de reference en unites = le 100% utilise pour calculer le pourcentage de stock ; CHECK > 0 protege aussi la division du pourcentage contre la division par zero | | `pack_size` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Unites par lot de reapprovisionnement | | `pack_label` | VARCHAR(80) | YES | Libelle humain du lot | +| `energy_kcal_100g` | SMALLINT UNSIGNED | YES | Apport energetique pour 100 g, importe depuis l'API externe OpenFoodFacts (migration 0005) | +| `nutrition_source` | VARCHAR(120) | YES | Provenance de la donnee nutritionnelle, ex. "OpenFoodFacts" (migration 0005) | +| `nutrition_fetched_at` | DATETIME | YES | Horodatage de l'import nutritionnel, trace la fraicheur (migration 0005) | | `low_stock_pct` | SMALLINT UNSIGNED NOT NULL DEFAULT 10 | NO | Bande d’alerte, pourcentage de la capacite (CHECK BETWEEN 0 AND 100) | | `critical_stock_pct` | SMALLINT UNSIGNED NOT NULL DEFAULT 5 | NO | Plancher de rupture automatique, pourcentage de la capacite (CHECK BETWEEN 0 AND 100 ; CHECK de table `critical_stock_pct < low_stock_pct`) | | `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Desactiver les ingredients obsoletes | @@ -577,6 +609,11 @@ bloque pas le produit (seul son supplement devient indisponible). Le tableau de retrait manuel (`is_available=0`) d'une rupture pilotee par le stock (`is_available=1` mais un ingredient requis est critique). +**Enrichissement nutritionnel (migration 0005, voir note 14 du dictionnaire)** : `energy_kcal_100g`, +`nutrition_source` et `nutrition_fetched_at` (toutes nullables) stockent une donnee importee depuis l'API +externe OpenFoodFacts (Cr 3.a.3). Opt-in : l'import est declenche par `IngredientController::enrich` +(action manager/admin), pas au runtime borne ; un ingredient non enrichi reste valide. + --- ### 4.7 `product_ingredient` @@ -811,7 +848,7 @@ Pas d'horodatages. Table de jointure pure. ``` customer_order (id, order_number, [idempotency_key], source, [#acting_user_id], - service_mode, status, + service_mode, [service_tag], status, total_ht_cents, total_vat_cents, total_ttc_cents, [paid_at], [delivered_at], [cancelled_at], created_at, updated_at) @@ -833,11 +870,12 @@ customer_order (id, order_number, [idempotency_key], source, [#acting_user_id], | Colonne | Type | NULL | Notes | |---|---|---|---| | `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | -| `order_number` | VARCHAR(20) | NO | Format `K/C/D-YYYY-MM-DD-NNN` par canal | +| `order_number` | VARCHAR(20) | NO | Prefixe canal + id sequentiel : `K`/`C`/`D` (existant, voir note de table) | | `idempotency_key` | VARCHAR(36) | YES | UUID client, UNIQUE ; deduplique un POST reessaye (security-by-design) | | `source` | ENUM('kiosk','counter','drive') | NO | Canal de saisie | | `acting_user_id` | INT UNSIGNED | YES | FK -> user ; personnel counter/drive sous PIN ; NULL pour kiosk | | `service_mode` | ENUM('dine_in','takeaway','drive') | NO | Mode de consommation (stats uniquement, pas de role fiscal) | +| `service_tag` | VARCHAR(20) | YES | Numero de chevalet du service en salle (`dine_in`), saisi a la borne ; NULL pour takeaway/drive (migration 0003) | | `status` | ENUM('pending_payment','paid','delivered','cancelled') NOT NULL DEFAULT 'pending_payment' | NO | Machine a 4 etats | | `total_ht_cents` | INT UNSIGNED | NO | Snapshot du total HT | | `total_vat_cents` | INT UNSIGNED | NO | Snapshot du montant de TVA | @@ -848,6 +886,17 @@ customer_order (id, order_number, [idempotency_key], source, [#acting_user_id], | `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Utilise comme base de `service_day` | | `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit | +**Numero de commande (existant, voir note 4 du dictionnaire)** : `order_number` = prefixe canal + id +sequentiel (`K` / `C` / `D`), ecrit en deux temps (INSERT avec numero provisoire vide, puis +UPDATE en `prefix . LAST_INSERT_ID()`) dans `OrderRepository`. La cible initiale `K-AAAA-MM-JJ-NNN` +(compteur journalier) n'a pas ete retenue : la forme `prefixe + id` evite un compteur quotidien. Compromis +connu : numero sequentiel donc devinable, couple a l'endpoint de paiement anonyme cote borne (piste +d'amelioration : numero non sequentiel). + +**Service en salle (migration 0003)** : `service_tag` (VARCHAR(20), nullable) porte le numero de chevalet +saisi a la borne pour le mode `dine_in` ; NULL pour `takeaway` / `drive`. Colonne additive, sans contrainte +de BD (la coherence avec `service_mode` est appliquee au niveau applicatif). + **Attribution du personnel (security-by-design)** : `acting_user_id` (FK -> `user`, ON DELETE SET NULL) enregistre le personnel counter/drive qui a pris la commande sous PIN ; NULL pour les commandes kiosk anonymes. Les commandes kiosk restent anonymes par conception. `stock_movement.user_id` couvre l'attribution des actions @@ -1235,11 +1284,11 @@ et que toutes les tables se rattachent au MCD. | Entite MCD | Table MLD | Type de mapping | Notes | |---|---|---|---| | `category` (C1) | `category` (4.1) | entite 1:1 | | -| `product` (C2) | `product` (4.2) | entite 1:1 | | +| `product` (C2) | `product` (4.2) | entite 1:1 | Additif post-v0.3 : `maxi_variant_product_id` (0006), `size_cl` + `base_product_id` (0007) | | `menu` (C3) | `menu` (4.3) | entite 1:1 | Nouveau : `burger_product_id`, `price_normal_cents`, `price_maxi_cents` | | `menu_slot` (C4) | `menu_slot` (4.4) | entite 1:1 | Nouvelle entite (v0.2) | | `menu_slot_option` (C5) | `menu_slot_option` (4.5) | Table de jointure (PK composite) | Nouvelle entite (v0.2) | -| `ingredient` (C6) | `ingredient` (4.6) | entite 1:1 | Nouvelle entite (v0.2) | +| `ingredient` (C6) | `ingredient` (4.6) | entite 1:1 | Nouvelle entite (v0.2) ; additif post-v0.3 : `energy_kcal_100g`, `nutrition_source`, `nutrition_fetched_at` (0005) | | `product_ingredient` (C7) | `product_ingredient` (4.7) | Table de jointure avec attributs | Nouvelle entite (v0.2) | | `allergen` (C8) | `allergen` (4.8) | entite 1:1 | Nouvelle entite (v0.2) | | `ingredient_allergen` (C9) | `ingredient_allergen` (4.9) | Table de jointure (PK composite) | Nouvelle entite (v0.2) | @@ -1248,7 +1297,7 @@ et que toutes les tables se rattachent au MCD. | `role_visible_source` (C12) | `role_visible_source` (4.12) | Table de jointure (PK composite) | Nouvelle entite (v0.2) | | `permission` (C13) | `permission` (4.13) | entite 1:1 | | | `role_permission` (C14) | `role_permission` (4.14) | Table de jointure (PK composite) | | -| `customer_order` (C15) | `customer_order` (4.15) | entite 1:1 | Renommee depuis `commande` ; machine a 4 etats ; horodatages de phase | +| `customer_order` (C15) | `customer_order` (4.15) | entite 1:1 | Renommee depuis `commande` ; machine a 4 etats ; horodatages de phase ; additif post-v0.3 : `service_tag` (0003) | | `order_item` (C16) | `order_item` (4.16) | entite 1:1 | Nouveau : `format`, `vat_rate_snapshot` ; CHECK de polymorphisme | | `order_item_selection` (C17) | `order_item_selection` (4.17) | entite 1:1 | Nouvelle entite (v0.2) | | `order_item_modifier` (C18) | `order_item_modifier` (4.18) | entite 1:1 | Nouvelle entite (v0.2) | diff --git a/docs/merise/mlt.md b/docs/merise/mlt.md index 37b626f..f40ac95 100644 --- a/docs/merise/mlt.md +++ b/docs/merise/mlt.md @@ -115,7 +115,7 @@ Ces regles s'appliquent a plusieurs operations et sont centralisees ici pour evi | **[PRE-3]** | Le corps JSON du POST est valide (validation de schema a la couche API) | | **[RG-1]** | Verification de disponibilite cote serveur : pour chaque article, verifier `product.is_available = 1` ou `menu.is_available = 1`. Si un article est indisponible, rejeter avec la liste des articles indisponibles. | | **[RG-2 — service_day]** | Le `service_day` d'une commande donnee est calcule a l'execution de la requete comme : `CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END`. La coupure est a 10:00. Ce n'est PAS stocke comme colonne — calcule uniquement a l'execution de la requete. La formule v0.1 avec `INTERVAL 4 HOUR 30 MINUTE` etait incorrecte et est abandonnee. | -| **[RG-3 — order number]** | Format du numero de commande : `K-YYYY-MM-DD-NNN` ou NNN est le compteur sequentiel pour le service_day courant pour la source `kiosk` (SELECT COUNT + 1 avec un verrou au niveau table ou un insert serialise pour eviter une generation en double sous concurrence). La source est `kiosk` (definie par l'endpoint kiosk, derivee du point d'entree public). | +| **[RG-3 — order number]** | Format du numero de commande : prefixe canal + id auto-incremente, soit `K` pour la source `kiosk` (ex. `K42`). Genere en deux temps dans la transaction : INSERT avec `order_number` provisoire vide, puis UPDATE `prefix . LAST_INSERT_ID()`. Pas de compteur par service_day (voir dictionnaire note 4). La source est `kiosk` (derivee de l'endpoint public). Le numero provisoire vide partage avant l'UPDATE reste une surface de robustesse a durcir sous forte concurrence (suivi au backlog). | | **[RG-4 — VAT by line]** | Pour chaque `order_item` : `vat_rate_snapshot` est copie depuis `product.vat_rate`. Montants de ligne : `unit_ttc = unit_price_cents_snapshot` ; `unit_ht = ROUND(unit_ttc * 1000 / (1000 + vat_rate_snapshot))` ; `unit_vat = unit_ttc - unit_ht`. Totaux de commande : `total_ttc_cents = SUM(unit_ttc * quantity)` sur toutes les lignes ; `total_ht_cents = SUM(unit_ht * quantity)` ; `total_vat_cents = total_ttc_cents - total_ht_cents`. Invariant : `total_ttc_cents = total_ht_cents + total_vat_cents` (verifie avant l'INSERT). | | **[RG-5 — atomic transaction]** | Toutes les ecritures dans une seule transaction de base de donnees : (1) INSERT `customer_order` (status `pending_payment`, source `kiosk`, service_mode depuis le panier, totaux calcules) ; (2) INSERT des lignes `order_item` (label_snapshot, unit_price_cents_snapshot, vat_rate_snapshot, quantity, format, item_type, product_id ou menu_id) ; (3) INSERT des lignes `order_item_selection` pour chaque slot rempli dans un article menu (order_item_id, menu_slot_id, product_id, label_snapshot) ; (4) INSERT des lignes `order_item_modifier` pour chaque modification d'ingredient (order_item_id, ingredient_id, action, extra_price_cents snapshot) ; (5) pour chaque ingredient consomme : calculer units = `(order_item.format = 'maxi' ? product_ingredient.quantity_maxi : product_ingredient.quantity_normal) * order_item.quantity`, ajuste par les modificateurs (remove => pas de decrement pour cet ingredient ; add => decrement supplementaire) ; appliquer le decrement atomique `UPDATE ingredient SET stock_quantity = stock_quantity - :units WHERE id = :id` (instruction unique auto-verrouillante, sans lecture-gate prealable, RG-T20) ; `stock_quantity` est signe et peut devenir negatif (ampleur de survente, remontee aux managers) — le decrement ne se conditionne pas a un plancher ; INSERT `stock_movement` (type `sale`, delta = -units, order_id, user_id = NULL pour le kiosk) ; (6) UPDATE `customer_order` SET status = `paid`, `paid_at = NOW()`. Les six etapes committent ensemble ou sont entierement annulees. | | **[RG-6 — cross-constraint]** | La source `kiosk` n'implique aucune contrainte particuliere de service_mode ; le client selectionne `dine_in` ou `takeaway`. La contrainte croisee drive (RG-T09) ne s'applique pas aux commandes provenant du kiosk. | @@ -163,7 +163,7 @@ Ces regles s'appliquent a plusieurs operations et sont centralisees ici pour evi | **[PRE-3]** | Le panier contient au moins 1 article | | **[RG-1]** | Logique de creation identique a CREATE_ORDER (RG-1 a RG-7 s'appliquent), avec les differences suivantes : `source` est auto-tagguee depuis `role.order_source` (role comptoir -> `counter`, role drive -> `drive`) ; `service_mode` est selectionne par le membre du personnel (`dine_in` / `takeaway` / `drive`) ; `user_id` est defini a l'id de l'utilisateur authentifie dans les lignes `stock_movement` (au lieu de NULL pour le kiosk). | | **[RG-2 — cross-constraint]** | Si `source = 'drive'` alors `service_mode` doit etre `'drive'` (RG-T09) ; verifie avant l'INSERT. HTTP 422 si viole. | -| **[RG-3 — order number]** | Format : `C-YYYY-MM-DD-NNN` pour la source comptoir ; `D-YYYY-MM-DD-NNN` pour la source drive. Le compteur sequentiel NNN est par source par service_day. | +| **[RG-3 — order number]** | Format : prefixe canal + id auto-incremente, soit `C` (comptoir) ou `D` (drive). Meme generation en deux temps que CREATE_ORDER RG-3. Pas de compteur par service_day (voir dictionnaire note 4). | | **[RG-4 — stock]** | Meme logique de decrement de stock que CREATE_ORDER RG-5 ; `stock_movement.user_id` est defini a l'id du membre du personnel authentifie. | | **[RG-5 — staff attribution + decrement]** | `customer_order.acting_user_id` est defini a l'id du membre du personnel authentifie (imputabilite ciblee sur les commandes comptoir/drive ; les commandes kiosk restent NULL). La re-validation des modificateurs cote serveur (3.3 RG-9), l'idempotence (RG-T19) et le decrement de stock atomique (RG-T20) s'appliquent a l'identique. Aucun PIN n'est requis pour creer une commande (la permission `order.create` suffit) ; la creation de commande n'est pas dans l'ensemble des actions sensibles. | | **[POST-1]** | Une ligne `customer_order` avec `status = 'paid'`, `source = 'counter'` ou `'drive'`, `paid_at` defini, `acting_user_id` defini. | diff --git a/docs/uml/use-cases.md b/docs/uml/use-cases.md index 7f7e6ab..a9df183 100644 --- a/docs/uml/use-cases.md +++ b/docs/uml/use-cases.md @@ -211,7 +211,7 @@ flowchart LR | Cas | Operation MCT | Permission | Description | Entites | |---|---|---|---|---| -| Saisir une commande comptoir/drive | 4.1 CREATE_COUNTER_ORDER | `order.create` | Composer une commande pour un client au comptoir (`counter`) ou au drive (`drive`). Logique identique a CREATE_ORDER ; `source` auto-tague depuis `role.order_source`. Numero `C-`/`D-YYYY-MM-DD-NNN`. | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `ingredient`, `stock_movement` | +| Saisir une commande comptoir/drive | 4.1 CREATE_COUNTER_ORDER | `order.create` | Composer une commande pour un client au comptoir (`counter`) ou au drive (`drive`). Logique identique a CREATE_ORDER ; `source` auto-tague depuis `role.order_source`. Numero prefixe canal + id (`C`/`D`, voir dictionnaire note 4). | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `ingredient`, `stock_movement` | | Consulter la file de preparation | 5.1 LIST_ORDERS_DISPLAY | `order.read` | Voir les commandes `paid` triees par `paid_at` croissant, filtrees par `role_visible_source` (counter voit kiosk+counter ; drive voit drive). Couleur KDS = `now - paid_at`. | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `role_visible_source` | | Remettre la commande | 6.1 DELIVER_ORDER | `order.deliver` | Geste unique `paid -> delivered`, `delivered_at = NOW()`. | `customer_order` | | Annuler une commande | 7.1 CANCEL_ORDER | `order.cancel` | Transition vers `cancelled` depuis `pending_payment`/`paid`, `cancelled_at = NOW()`. Re-credit du stock si `paid`. | `customer_order`, `ingredient`, `stock_movement` | -- 2.45.3 From 0604d743fc457071df2300c09c109969d7e64b9b Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 25 Jun 2026 07:58:27 +0000 Subject: [PATCH 3/4] docs: journal #94-#105 + ADR-0011 (POS tactile) + ADR-0012 (dashboard stock) + reindex --- .../0011-pos-tactile-tuiles-comptoir-drive.md | 43 ++++ docs/adr/0012-page-stock-tableau-de-bord.md | 41 ++++ docs/adr/README.md | 2 + ...5--audit-remediation-et-features-94-105.md | 210 ++++++++++++++++++ docs/journal/README.md | 7 +- 5 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0011-pos-tactile-tuiles-comptoir-drive.md create mode 100644 docs/adr/0012-page-stock-tableau-de-bord.md create mode 100644 docs/journal/2026-06-25--audit-remediation-et-features-94-105.md diff --git a/docs/adr/0011-pos-tactile-tuiles-comptoir-drive.md b/docs/adr/0011-pos-tactile-tuiles-comptoir-drive.md new file mode 100644 index 0000000..803be47 --- /dev/null +++ b/docs/adr/0011-pos-tactile-tuiles-comptoir-drive.md @@ -0,0 +1,43 @@ +# ADR-0011 — POS tactile a tuiles pour la saisie comptoir/drive + +- Statut : Accepte +- Date : 2026-06-25 + +## Contexte +La saisie de commande comptoir/drive (mlt 4.1, `CREATE_COUNTER_ORDER`) avait d'abord ete +livree comme formulaire-liste enrichi (#100) : une liste de produits avec champs de +quantite, prix, verrou du mode de service au canal drive, file des commandes recentes. +Cet ecran est destine a des equipiers non-techniques, sur tablette, dans un contexte de +caisse ou la rapidite compte. Le formulaire-liste se prete mal au tactile (petites cibles, +defilement) et ne ressemble pas au geste deja appris cote borne client. Options : +conserver et raffiner le formulaire-liste ; adopter un paradigme de caisse (POS) a tuiles +reutilisant l'UX borne ; integrer une bibliotheque de POS tierce. + +## Decision +La saisie comptoir/drive devient un **POS tactile a tuiles** (#104) calque sur la borne +client : onglets categories en haut, grille de tuiles produits/menus, panneau commande +persistant a droite ; un tap ajoute le produit, un produit a modificateurs ou un menu +ouvre une modale de composition. Le panier est construit cote client en vanilla JS +CSP-safe (`'self'`, zero handler inline) a partir d'un script JSON inerte, puis serialise +dans un champ cache `items_json`. Le **serveur reste seul juge** : il revalide la forme +(RG-T18), recalcule les prix et resout les modificateurs (RG-T16) ; les prix affiches cote +client sont indicatifs. Un **seul controleur** sert les deux canaux, la `source` +(counter/drive) etant derivee du chemin de la requete. + +## Consequences +- (+) Geste unifie borne/comptoir : moins d'apprentissage, reutilisation des patterns + composeur deja eprouves cote borne. +- (+) Cibles tactiles larges adaptees a la tablette ; zero jargon dev a l'ecran + (utilisateurs non-techniques). +- (+) Decoupage par chemin (`/drive...` vs `/counter...`) : les canaux restent etanches, + un equipier ne peut pas requalifier sa commande via un champ falsifie. +- (+) Sans framework front (coherent ADR-0002) ; coherent avec ADR-0001 (pas de + dependance tierce, donc pas de POS externe). +- (-) Le POS est interactif par nature : sans JS, la grille ne s'affiche pas (un message + invite a activer JS). Un repli legacy `qty_` reste accepte quand `items_json` est + absent, mais l'experience cible suppose JS. +- (-) Cet ecran a ete refondu deux fois a court intervalle (#100 puis #104) ; le palier + #100 est assume comme etape, pas comme dette cachee. +- Fichiers : `src/app/Controllers/CounterOrderController.php`, + `src/app/Views/admin/counter/new.php`, `src/public/admin/assets/js/counter-order.js`. + Detail : journal `docs/journal/2026-06-25--audit-remediation-et-features-94-105.md`. diff --git a/docs/adr/0012-page-stock-tableau-de-bord.md b/docs/adr/0012-page-stock-tableau-de-bord.md new file mode 100644 index 0000000..f311da0 --- /dev/null +++ b/docs/adr/0012-page-stock-tableau-de-bord.md @@ -0,0 +1,41 @@ +# ADR-0012 — Page Stock en tableau de bord (alertes + reapprovisionnement en avant) + +- Statut : Accepte +- Date : 2026-06-25 + +## Contexte +La page d'accueil Ingredients/Stock (domaine 8.8 + 9) etait une liste-CRUD exhaustive : +tous les ingredients, avec les actions creer/modifier/supprimer en premier plan. Elle +etait jugee trop chargee et opaque pour un equipier non-technique. Le besoin metier +quotidien est de voir vite ce qui manque et de reapprovisionner ; l'edition de fiches est +rare. De plus, le lien entre stock et disponibilite borne (un ingredient requis sous le +seuil critique rend indisponibles les produits qui l'utilisent, RG-T21, cf. ADR-0003) +n'etait pas visible a l'ecran. Options : garder la liste exhaustive triable ; basculer +vers un tableau de bord oriente action ; deux pages distinctes (dashboard + gestion). + +## Decision +La page Stock devient un **tableau de bord oriente action** (#105) : un bandeau explicite +le lien stock -> disponibilite borne (RG-T21) ; un resume compte les ingredients +critiques / en alerte / au-dessus du seuil ; une section "A reapprovisionner" met en avant +les ingredients bas (critiques d'abord) avec barre de niveau et bouton de +reapprovisionnement direct. La liste complete passe au second plan et le CRUD est relegue. +Les compteurs par etat sont **calcules cote serveur** dans `IngredientController::index()` +a partir du `stock_band` deja resolu par le depot, pour garder la vue declarative et la +valeur directement testable. Les sous-pages (reappro, inventaire, mouvements, creation) +restent inchangees. + +## Consequences +- (+) L'ecran s'aligne sur le geste quotidien (reperer le manque, reapprovisionner) plutot + que sur l'edition de fiches. +- (+) Le lien stock -> disponibilite borne (RG-T21) devient explicite a l'ecran, pas + seulement dans le code. +- (+) Compteurs testables (la logique de comptage est cote serveur, pas dans la vue) : + +4 cas de test `IngredientController` (bandeau, promotion d'un critique en section + reappro, etat vide positif, compteurs par etat). +- (-) La liste exhaustive et le CRUD sont moins immediats (un cran plus loin) : choix + assume, ces operations etant moins frequentes que la lecture d'alerte. +- (-) Le tri/filtre avance de l'ancienne liste n'est pas reconduit en premier plan. +- Coherent avec ADR-0002 (MVC rendu serveur) et ADR-0003 (disponibilite calculee depuis + le stock). Fichiers : `src/app/Controllers/IngredientController.php`, + `src/app/Views/admin/ingredients/index.php`, `src/public/admin/assets/css/admin.css`. + Detail : journal `docs/journal/2026-06-25--audit-remediation-et-features-94-105.md`. diff --git a/docs/adr/README.md b/docs/adr/README.md index 384b7e8..2a53cfb 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -18,6 +18,8 @@ une decision revisee donne une nouvelle fiche qui *supersede* l'ancienne (statut | [0008](0008-makefile-vers-compose-migrate.md) | Du Makefile a `docker compose up` (service wakdo-migrate) | Accepte | | [0009](0009-compose-standalone-et-prod-gitignore.md) | docker-compose.yml standalone + docker-compose.prod.yml gitignore | Accepte | | [0010](0010-cookie-secure-conditionnel-https.md) | Cookie de session Secure conditionnel au HTTPS | Accepte | +| [0011](0011-pos-tactile-tuiles-comptoir-drive.md) | POS tactile a tuiles pour la saisie comptoir/drive | Accepte | +| [0012](0012-page-stock-tableau-de-bord.md) | Page Stock en tableau de bord (alertes + reappro en avant) | Accepte | ## Modele de fiche diff --git a/docs/journal/2026-06-25--audit-remediation-et-features-94-105.md b/docs/journal/2026-06-25--audit-remediation-et-features-94-105.md new file mode 100644 index 0000000..04770cb --- /dev/null +++ b/docs/journal/2026-06-25--audit-remediation-et-features-94-105.md @@ -0,0 +1,210 @@ +# 2026-06-25 — Synthese : CD prod, SMTP reel, durcissement borne, POS comptoir/drive, dashboard stock (#94-#105) + +**Auteur : BYAN.** Retrospective de synthese couvrant douze PR mergees apres la session +du 2026-06-18 (front login + amorce P4 commande). Elles se regroupent en quatre fils : +mise en production reelle (#94-#97), finition du parcours borne client (#98, #99, #101, +#102, #103), et refonte de la saisie comptoir/drive et de la page stock cote back-office +(#100, #104, #105). Entree descriptive : on decrit ce qui est livre, pas ce qui est +promis. + +--- + +## Ce qui a ete livre (PR mergees) + +| PR | Bloc | Objet | +|----|------|-------| +| #94 | CD | Deploiement push-based vers Vision (prod) + preuve de version dans `GET /api/health` | +| #95 | CD | Modeles versionnes `docker-compose.prod.yml.example` + `.env.prod.example` | +| #96 | Auth | Envoi reel de l'email de reset via relais SMTP (Brevo) — client SMTP maison | +| #97 | CD | Passage des variables SMTP/MAIL au conteneur `wakdo-app` (correctif #96) | +| #98 | Borne | Menu Maxi agrandit la boisson en 50cl + transport du format choisi | +| #99 | Borne | Produit/menu en rupture de stock rendu non commandable (RG-T21) | +| #100 | Back-office | Refonte saisie comptoir/drive : prix, verrou du mode, navigation, file | +| #101 | Borne | Panier unique = panneau persistant (retrait de `cart.html` et `product.html`) | +| #102 | Borne | Confirmation avant l'abandon de la commande | +| #103 | Borne | Bascule des allergenes sur `/api/allergens` + menage des donnees/docs statiques | +| #104 | Back-office | Saisie comptoir/drive en POS tactile a tuiles (refonte de #100) | +| #105 | Back-office | Page Stock en tableau de bord (alertes + reapprovisionnement en avant) | + +--- + +## Bloc 1 — Mise en production reelle (#94, #95, #96, #97) + +### Ce qui a ete fait +- **CD push-based (#94)** : `.forgejo/workflows/deploy.yml` ouvre, sur push `main`, une + session SSH vers Vision (l'hote de prod). `scripts/deploy.sh` y recupere `main` en + fast-forward, ecrit un marqueur de version (`src/VERSION` : SHA + date), journalise une + ligne dans `deploy.log`, puis reconstruit et recree la stack. `GET /api/health` expose + desormais `version` et `deployed_at`, lus depuis ce marqueur : c'est la preuve cote app + qu'un deploiement a bien repris le dernier commit. Doc : `docs/architecture/deployment.md`. +- **Modeles de prod versionnes (#95)** : `docker-compose.prod.yml.example` et + `.env.prod.example` entrent au depot comme gabarits. Le fichier reel reste gitignore + (specifique a l'hote : Traefik, reseau externe), conformement a ADR-0009 ; le `.example` + documente la forme attendue. +- **SMTP reel (#96, #97)** : la reinitialisation de mot de passe envoyait jusque-la un mail + inerte. Un client SMTP maison (`SmtpClient`, `SmtpMailer`, transport via flux PHP + `StreamSmtpTransport` derriere l'interface `SmtpTransport`) parle a un relais reel + (Brevo en l'occurrence). `PasswordResetController` s'y branche. #97 corrige un oubli : + les variables SMTP/MAIL n'etaient pas transmises au conteneur `wakdo-app` (declarees + dans les deux fichiers compose). + +### Pourquoi — decisions et alternatives +- **Decision : CD par SSH, pas par Docker-in-CI.** Le runner Forgejo (sur Stark) n'a pas + acces au socket Docker, par choix de securite : un job CI ne pilote pas Docker sur son + hote. Le deploiement vers Vision se fait donc par SSH avec une *forced command* cote + serveur. *Alternative ecartee* : donner le socket Docker au runner — rejetee pour la + surface d'attaque. C'est le prolongement de la decision E2E-CI du 2026-06-18 (meme + contrainte de socket). +- **Decision : client SMTP maison plutot qu'une bibliotheque.** ADR-0001 fige le projet + sans Composer ni dependance tierce ; un client SMTP minimal (EHLO/AUTH/MAIL/RCPT/DATA + sur flux) reste coherent avec cette contrainte et reste testable via un faux transport + (`FakeSmtpTransport`). *Alternative ecartee* : `mail()` de PHP — sans relais + authentifie, la delivrabilite est aleatoire et la configuration sort du depot. +- **Decision : marqueur de version dans `src/VERSION` lu a chaud.** Le marqueur est sous + le mount du code (`./src` -> `/var/www/html`), donc relu sans rebuild. Cela donne une + preuve de deploiement observable de l'exterieur sans instrumentation supplementaire. + +### Criteres RNCP couverts +- **Bloc 3 - deploiement** : chaine de livraison continue tracable (`deploy.yml`, + `deploy.sh`, `deployment.md`), preuve de version exposee. +- **Bloc 1 - securite** : separation runner/prod sans socket Docker ; secrets de prod + hors du depot (gabarits `.example` seulement). + +--- + +## Bloc 2 — Finition du parcours borne client (#98, #99, #101, #102, #103) + +### Ce qui a ete fait +- **Menu Maxi -> boisson 50cl (#98)** : choisir le format Maxi d'un menu fait passer la + boisson de 33cl a 50cl. La migration `0007_product_size_variant` et le seed + `0006_drink_maxi_variant` portent la variante en base ; le format choisi est transporte + jusqu'au paiement (`checkout.js`, `page-product-menu.js`). Tests `OrderRepository` + + tests JS du composeur. +- **Rupture non commandable (#99, RG-T21)** : un produit (ou un menu dont un composant + requis) sous le seuil critique de stock est marque indisponible et non ajoutable a la + borne ; le serveur refuse aussi la creation cote `OrderRepository` (defense en + profondeur, le client n'est pas seul juge). Visuel d'indisponibilite cote `style.css`. +- **Panier unique = panneau persistant (#101)** : suppression des pages `cart.html` et + `product.html` ; le panier devient un panneau lateral persistant (`order-panel.js`) + present sur les pages au lieu d'une page dediee. Le net du diff est negatif + (-784 lignes) : c'est une simplification de l'architecture front borne. +- **Confirmation d'abandon (#102)** : abandonner la commande ouvre une modale de + confirmation (`confirm-modal.js`) au lieu de vider le panier au premier clic. +- **Allergenes via API (#103)** : la borne lisait des fichiers JSON statiques + (`allergens.json`, `categories.json`, `produits.json`) ; elle consomme desormais + `/api/allergens` (et le catalogue par API). Les JSON statiques et la doc afferente sont + retires (menage). `docs/api/conventions.md` et `docs/design/maquette-vs-build.md` mis a + jour. + +### Pourquoi — decisions et alternatives +- **Decision : rupture controlee cote serveur ET cote client (#99).** L'indisponibilite + est calculee au plus pres de la verite (RG-T21, ADR-0003) ; le client la reflete pour + l'UX, mais `OrderRepository` revalide a la creation. *Alternative ecartee* : masquer + cote client seulement — laisse une fenetre ou une commande forgee passerait. +- **Decision : panier persistant plutot que page panier (#101).** Sur une borne, l'aller- + retour vers une page panier ajoute une etape ; un panneau visible en permanence reduit + la navigation. Le retrait de deux pages reduit aussi la surface a maintenir. +- **Decision : source de verite unique pour les donnees borne (#103).** Les JSON statiques + dupliquaient le catalogue de la base ; ils pouvaient diverger silencieusement. Passer par + l'API supprime la duplication et fait converger borne et back-office sur le meme modele. + +### Criteres RNCP couverts +- **Bloc 2 - front client** : parcours borne complet (composeur, panier, abandon, + indisponibilite) sur donnees reelles par API. +- **Bloc 1 - regles metier** : RG-T21 (disponibilite calculee) appliquee de bout en bout. + +--- + +## Bloc 3 — Saisie comptoir/drive en POS tactile (#100 puis #104) + +### Ce qui a ete fait +- **#100** a d'abord refondu la saisie comptoir/drive en tant que formulaire enrichi : + prix affiches, verrou du mode de service au canal drive, navigation, file des commandes + recentes. `CounterOrderController` derive la `source` (counter/drive) du chemin de la + requete ; ajout de la liste des commandes recentes par canal. +- **#104** a ensuite remplace ce formulaire-liste par un **POS tactile a tuiles** facon + borne : onglets categories en haut, grille de tuiles produits/menus, panneau commande + persistant a droite. Pense pour la tablette (grandes cibles, un tap = ajout). Le panier + est construit cote client (`counter-order.js`, CSP `'self'`, vanilla, zero handler + inline) a partir d'un script JSON inerte, puis serialise dans un champ cache + `items_json`. Le serveur revalide la forme (RG-T18), recalcule les prix (RG-T16) et + resout les modificateurs : les prix cote client sont indicatifs. + +### Pourquoi — decisions et alternatives +- **Decision : POS a tuiles, reutilisant l'UX borne (#104).** Cf. ADR-0011. L'equipier + comptoir et le client borne font le meme geste (choisir des produits, composer) ; un + meme paradigme tuiles+panneau reduit l'apprentissage et reutilise les patterns deja + eprouves. *Alternative ecartee* : garder le formulaire-liste (#100) — moins adapte au + tactile et a la rapidite attendue d'une caisse. +- **Decision : serveur seul juge des prix et de la composition.** Le client propose, le + serveur fige (RG-T16). Coherent avec le reste du domaine commande. +- **Decision : un controleur pour deux canaux, source derivee du chemin.** Le decoupage + par chemin (`/drive...` vs `/counter...`) plutot que par parametre rend les deux canaux + etanches : un equipier ne peut pas requalifier sa commande en falsifiant un champ. + +### Comment — points techniques cles +- `CounterOrderController` : `source()` lue depuis le chemin ; `store()` decode + `items_json`, revalide, delegue a `createStaffOrder` (commande creee directement `paid`, + encaissement immediat, sans PIN — la permission `order.create` suffit). Repli legacy + `qty_` accepte quand `items_json` est absent (degradation sans JS). +- `counter-order.js` : construction des onglets/grille/panneau, modale de composition pour + produits a modificateurs et menus, serialisation a la soumission. + +### Criteres RNCP couverts +- **Bloc 2 - back-office** : ecran de caisse pour equipiers non-techniques (zero jargon), + CSP-safe sans framework front. +- **Bloc 1 - regles metier** : recalcul/revalidation serveur (RG-T16, RG-T18), etancheite + des canaux. + +--- + +## Bloc 4 — Page Stock en tableau de bord (#105) + +### Ce qui a ete fait +La page d'accueil Ingredients/Stock, jugee trop chargee et opaque, est refondue en +tableau de bord. Elle porte desormais : un bandeau expliquant le lien stock -> +disponibilite borne (un ingredient requis sous le seuil critique rend indisponibles les +produits qui l'utilisent, RG-T21) ; un resume comptant les ingredients critiques / en +alerte / au-dessus du seuil ; une section "A reapprovisionner" mettant en avant les +ingredients bas (critiques d'abord) avec barre de niveau et bouton de reapprovisionnement +direct. La liste complete passe au second plan et le CRUD est relegue. Les sous-pages +(reappro, inventaire, mouvements, creation) restent inchangees. `index()` expose des +compteurs par etat, calcules cote serveur a partir de `stock_band` deja resolu par le +depot, pour garder la vue declarative et la valeur testable. + +### Pourquoi — decisions et alternatives +Cf. ADR-0012. **Decision : dashboard oriente action plutot que liste-CRUD.** Le metier +quotidien d'un equipier stock est de voir vite ce qui manque et de reapprovisionner, pas +d'editer des fiches. Mettre les alertes et le bouton de reappro en avant aligne l'ecran +sur ce geste. *Alternative ecartee* : garder une liste exhaustive triable — exhaustive +mais muette sur l'urgence. + +### Criteres RNCP couverts +- **Bloc 2 - back-office** : ergonomie orientee tache pour utilisateur non-technique. +- **Bloc 1 - regles metier** : lien stock -> disponibilite (RG-T21) rendu explicite a + l'ecran. + +--- + +## Verifications +A la cloture du lot : suite JS 135 tests verte (verifiee en local), suites PHPUnit et PHPStan niveau 6 vertes en CI Forgejo, +`php -l` propre. Chaque PR est passee par la CI Forgejo (checks requis) avant merge natif. + +## Points d'amelioration conscients +- **#100 puis #104** : la refonte POS (#104) remplace une premiere refonte (#100) du meme + ecran a quelques jours d'intervalle. Le formulaire enrichi de #100 a servi de palier + avant le pivot tactile ; l'iteration est assumee plutot que masquee. +- **SMTP** : le client maison couvre le cas d'usage reset (un destinataire, texte). Il + n'est pas un agent mail generaliste ; tout besoin plus large (pieces jointes, files) + serait a reevaluer. + +## Liens vers artefacts +- Commits : `8c5d942` (#94) -> `03ef99d` (#105). +- ADR associes : `docs/adr/0011-pos-tactile-tuiles-comptoir-drive.md`, + `docs/adr/0012-page-stock-tableau-de-bord.md`. +- Fichiers principaux : `.forgejo/workflows/deploy.yml`, `scripts/deploy.sh`, + `src/app/Controllers/HealthController.php`, `src/app/Auth/SmtpClient.php`, + `src/app/Controllers/CounterOrderController.php`, + `src/public/admin/assets/js/counter-order.js`, + `src/app/Controllers/IngredientController.php`, + `src/app/Views/admin/ingredients/index.php`. diff --git a/docs/journal/README.md b/docs/journal/README.md index 73947df..96d25a0 100644 --- a/docs/journal/README.md +++ b/docs/journal/README.md @@ -33,8 +33,13 @@ Les fichiers sont ordonnes chronologiquement par leur nom. | 2026-06-04 | [conception-prodlike-revision](2026-06-04--conception-prodlike-revision.md) | Revue d'alignement P1 + decisions prod-like du modele de donnees (drop commande_event, nommage EN, TVA par produit apres fact-check BOFiP, perso menus/ingredients, allergenes, ~16 entites) | `feat/p1-conception` | | 2026-06-15 | [p3-throttle-pin-rg-t22](2026-06-15--p3-throttle-pin-rg-t22.md) | P3 securite : throttle du PIN d'action sensible (RG-T22) — design multi-agents + verification adversariale, dimension "utilisateur agissant", entite 22 `pin_throttle` | `feat/p3-pin-throttle` -> `dev` | | 2026-06-16 | [audit-reel-livrables-p2-p3](2026-06-16--audit-reel-livrables-p2-p3.md) | Verification sur pieces des livrables du 2026-06-15 (sweep 10 dimensions + adversarial) : socle SbD confirme, miss confirmes par gravite (php.ini non deploye, CI sans tests DB, XSS kiosk, liens morts...) et remediations | `docs/journal-audit-2026-06-16` -> `dev` (PR #19/#20/#21) | +| 2026-06-04 | [p1-merise-v0.2-rewrite-and-forgejo-migration](2026-06-04--p1-merise-v0.2-rewrite-and-forgejo-migration.md) | P1 Merise v0.2 (prod-like) reecrit + migration vers Forgejo auto-heberge | `feat/p1-conception` | +| 2026-06-17 | [makefile-to-compose-migrate](2026-06-17--makefile-to-compose-migrate.md) | Du Makefile a `docker compose up` : service one-shot `wakdo-migrate` (migrate + seed idempotents) | `feat/compose-migrate` -> `dev` | +| 2026-06-17 | [session-infra-doc-e2e](2026-06-17--session-infra-doc-e2e.md) | Session infra compose, documentation Forge, amorce des tests E2E Playwright | `dev` (PR #37/#38/#39 et suivantes) | +| 2026-06-18 | [front-login-ui-admin-p4-commande](2026-06-18--front-login-ui-admin-p4-commande.md) | Page login, refonte UI admin (equipiers non-techniques), humanisation des libelles, amorce P4 commande (creation + encaissement) | `dev` (PR #48 a #58) | +| 2026-06-25 | [audit-remediation-et-features-94-105](2026-06-25--audit-remediation-et-features-94-105.md) | Synthese #94-#105 : CD push-based vers Vision + preuve de version, modeles compose prod, SMTP reel reset (Brevo), durcissement borne (Maxi 50cl, rupture non commandable, panier persistant, confirm abandon, allergenes /api), POS tactile comptoir/drive, page Stock en tableau de bord | `dev`/`main` (PR #94 a #105) | -*Mis a jour a chaque nouvelle entree.* +*Mis a jour a chaque nouvelle entree. Les entrees sont ordonnees par leur nom de fichier (date) ; cet index les liste dans l'ordre de redaction.* --- -- 2.45.3 From aec417df3ebb863cf858bf136e494f7f4f0c6512 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 25 Jun 2026 07:58:27 +0000 Subject: [PATCH 4/4] docs: corrige des assertions documentaires fausses (upload images, fallback JSON, workflow commande) --- SECURITY.md | 2 +- docs/ARCHITECTURE.md | 18 +++++++++++------- docs/PROJECT_CONTEXT.md | 6 +++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 5c6aa3f..e450bae 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,7 +19,7 @@ d'authentification durci dans `docs/uml/security-sequence.md`. | Brute-force | double throttle : compteur par compte (`user`) + par IP (`login_throttle`), backoff degressif | | Sessions | cookies `HttpOnly` + `Secure` + `SameSite=Strict`, regeneration d'ID a la connexion (anti-fixation), idle 4h / absolu 10h | | Injection | PDO prepared statements exclusivement | -| Upload | validation MIME + taille, stockage hors webroot | +| Upload | non implemente (aucun flux d'upload livre) ; prevu : validation MIME + taille, stockage hors webroot | | En-tetes / PHP | `expose_php=Off`, `allow_url_fopen/include=Off`, `cgi.fix_pathinfo=0`, fonctions d'execution systeme desactivees | | RGPD | retention limitee (audit ~12 mois, throttle 24h, commandes ~3 ans), droit consultation/modif/suppression | | Secrets | `.env` gitignore, tenu hors de `.git/config` (credential helper lisant `.env`), secret-scan gitleaks en CI | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 526d8af..01fd87a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -17,13 +17,17 @@ Wakdo simule une borne de commande tactile de restauration rapide, avec back-off d'administration, workflow cuisine et API REST interne. Deux surfaces applicatives : - **Borne (kiosk)** — front statique (HTML/CSS/JS vanilla ES6) servi par Apache, - consommant des donnees (JSON statique en P5, API DB-backed au swap P4). + consommant l'API REST DB-backed (`/api/*`). Le repli JSON statique initial a ete + retire au profit d'un branchement direct sur l'API. - **Back-office + API** — application PHP rendue serveur (MVC maison) + endpoints `/api/*`, derriere authentification et RBAC. Trois canaux de commande (`source`) : `kiosk`, `counter`, `drive`. Le cycle de vie -d'une commande et la machine a etats sont decrits dans `docs/merise/` (domaine -commande = phase **P4**, schema en base mais workflow applicatif a venir). +d'une commande et la machine a etats sont decrits dans `docs/merise/`. Le domaine +commande est livre de bout en bout : creation et encaissement via l'API +(`POST /api/orders`, `POST /api/orders/{number}/pay` avec decrement de stock), +file cuisine (KDS), annulation et livraison cote back-office, saisie comptoir et +drive (POS tactile). --- @@ -132,8 +136,8 @@ src/app/ Views/ admin/* (pages back-office rendues serveur), auth/* (login/reset) src/public/ admin/ front controller + assets (CSS/JS) du back-office - borne/ front kiosk statique (index, categories, products, product, cart, - payment, confirmation) + assets JS modules + data JSON + borne/ front kiosk statique (index, categories, products, payment, + confirmation ; panier en panneau persistant) + assets JS modules ``` Conventions transverses : controleurs non-`final` (seam de test : sous-classe injectant @@ -165,7 +169,7 @@ Vue rendue dans admin/layout (sorties echappees, RG-T15) | ou JSON pour /api/* ``` La borne (kiosk) est servie en statique par Apache ; ses pages consomment les donnees -via `fetch` (JSON statique en P5 ; bascule sur `/api/*` DB-backed au swap P4). +via `fetch` sur l'API DB-backed (`/api/*`). --- @@ -214,7 +218,7 @@ Threat model STRIDE + classification des donnees : `docs/PROJECT_CONTEXT.md` sec `ingredient`, `product_ingredient`, `allergen`, `ingredient_allergen`, `stock_movement`. - **RBAC / comptes** : `user`, `role`, `permission`, `role_permission`, `role_visible_source`. -- **Commande (P4, schema pret)** : `customer_order`, `order_item`, +- **Commande (livre)** : `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`. - **Transverses** : `audit_log` (journal immuable), `login_throttle`, `pin_throttle`. diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index fe6632e..22a536e 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -117,7 +117,7 @@ Client Borne (Bloc 1) API (Bloc 2) BDD ### Compatibilite evaluation par bloc -- **Jury Bloc 1** : voit le front seul ; le front peut tomber en fallback sur JSON statiques fournis (`src/public/borne/data/*.json`) si l'API est indisponible. +- **Jury Bloc 1** : voit le front seul ; le front consomme les donnees via `fetch` sur l'API (`/api/*`). Le fallback JSON statique initialement envisage a ete retire (la borne est branchee directement sur l'API DB-backed). - **Jury Bloc 2** : voit le back-office + teste l'API via curl/Postman de maniere autonome, sans dependre du front. - **Jury Bloc 5** : lance `docker compose up` ou `docker compose up`, verifie la CI/CD, les crons, l'archi, les scripts. @@ -205,7 +205,7 @@ Reseaux : ### Bloc 1 — Borne client (Front) **IN scope :** -- Affichage dynamique menus + produits (charges par Ajax depuis API ou JSON fallback) +- Affichage dynamique menus + produits (charges par `fetch` depuis l'API `/api/*`) - Composition panier : produits unitaires OU menus (burger + accompagnement + boisson + sauce) - Options taille (normale / grande, +0,50 € sur grande) pour accompagnements et boissons - Options de personnalisation simples (ex : sans oignon, avec fromage) @@ -232,7 +232,7 @@ Reseaux : - **Manager** : catalogue (create/update), stock (reappro + inventaire), statistiques ; utilisateurs en **lecture seule** (`user.read`, pas de creation/modification/desactivation), pas d'acces RBAC - **Kitchen** : file des commandes `paid` triee par `paid_at` croissant, en **lecture seule** (KDS visuel) ; inventaire - **Counter** / **Drive** : saisir une commande (comptoir / drive-thru via casque/intercom), bouton "declarer livree" (geste unique `paid -> delivered`), annuler ; `source` auto-tague depuis `role.order_source` ; inventaire -- Upload images produits (validation type MIME + taille + stockage dans volume `wakdo_uploads`) +- Upload images produits : **non implemente** ; prevu (validation type MIME + taille + stockage dans volume `wakdo_uploads`) - Historique commandes par statut - Stats de base (commandes du jour, CA jour, produits top) -- 2.45.3