From 64f5a279da2e6781a3d1e1a5e405622fe4803dcc Mon Sep 17 00:00:00 2001 From: Imugiii Date: Sat, 9 May 2026 07:03:27 +0000 Subject: [PATCH 1/8] docs(merise): add drawio XML sources for MCD diagrams Switch from Mermaid to drawio for MCD diagrams to gain manual layout control on the global view (10 entites + 10 associations, planarite intrinseque non resolue par Mermaid auto-layout). - mcd-global.drawio : 10 entites + 8 associations (vue compacte sans attributs) - mcd-catalogue.drawio : Categorie / Produit / Menu / MenuProduit avec attributs - mcd-commande.drawio : Commande / LigneCommande + polymorphisme vers Produit/Menu - mcd-rbac.drawio : User / Role / Permission / RolePermission Notation Merise (min,max) sur chaque cote d'association. Layout de depart a affiner manuellement dans drawio web (Edit Diagram -> XML). SVG a regenerer en exportant depuis drawio web. --- docs/merise/_diagrams/mcd-catalogue.drawio | 67 +++++++++ docs/merise/_diagrams/mcd-commande.drawio | 61 ++++++++ docs/merise/_diagrams/mcd-global.drawio | 154 +++++++++++++++++++++ docs/merise/_diagrams/mcd-rbac.drawio | 57 ++++++++ 4 files changed, 339 insertions(+) create mode 100644 docs/merise/_diagrams/mcd-catalogue.drawio create mode 100644 docs/merise/_diagrams/mcd-commande.drawio create mode 100644 docs/merise/_diagrams/mcd-global.drawio create mode 100644 docs/merise/_diagrams/mcd-rbac.drawio diff --git a/docs/merise/_diagrams/mcd-catalogue.drawio b/docs/merise/_diagrams/mcd-catalogue.drawio new file mode 100644 index 0000000..c9cf3e8 --- /dev/null +++ b/docs/merise/_diagrams/mcd-catalogue.drawio @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/merise/_diagrams/mcd-commande.drawio b/docs/merise/_diagrams/mcd-commande.drawio new file mode 100644 index 0000000..95063f1 --- /dev/null +++ b/docs/merise/_diagrams/mcd-commande.drawio @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/merise/_diagrams/mcd-global.drawio b/docs/merise/_diagrams/mcd-global.drawio new file mode 100644 index 0000000..6d010db --- /dev/null +++ b/docs/merise/_diagrams/mcd-global.drawio @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/merise/_diagrams/mcd-rbac.drawio b/docs/merise/_diagrams/mcd-rbac.drawio new file mode 100644 index 0000000..31e109d --- /dev/null +++ b/docs/merise/_diagrams/mcd-rbac.drawio @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b8cb3ef68d4089b7f0679877dfc9debbb2cca946 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 4 Jun 2026 10:19:25 +0000 Subject: [PATCH 2/8] docs(merise): commit P1 conception v0.1 (dictionary, MCD, MCT, MLT, MLD) + UML Baseline of the P1 conception work produced over sessions 5-7 (was uncommitted in the working tree). 11-entity model, French naming. Superseded next by the prod-like revision (English, ~16 entities) per the 2026-06-04 decision session - this commit preserves the baseline in history before that rewrite. --- docs/merise/_diagrams/mcd-catalogue.svg | 4 + docs/merise/_diagrams/mcd-commande.drawio | 36 +- docs/merise/_diagrams/mcd-commande.svg | 4 + docs/merise/_diagrams/mcd-global.drawio | 28 + docs/merise/_diagrams/mcd-global.svg | 4 + docs/merise/_diagrams/mcd-rbac.svg | 4 + docs/merise/_diagrams/mld-catalogue.drawio | 59 ++ docs/merise/_diagrams/mld-commande.drawio | 78 +++ docs/merise/_diagrams/mld-rbac.drawio | 56 ++ docs/merise/dictionary.md | 272 +++++----- docs/merise/mcd.md | 309 +++++++++++ docs/merise/mct.md | 598 +++++++++++++++++++++ docs/merise/mld.md | 525 ++++++++++++++++++ docs/merise/mlt.md | 588 ++++++++++++++++++++ docs/uml/sequence-passer-commande.md | 193 +++++++ docs/uml/state-commande.md | 144 +++++ docs/uml/use-cases.md | 222 ++++++++ 17 files changed, 2973 insertions(+), 151 deletions(-) create mode 100644 docs/merise/_diagrams/mcd-catalogue.svg create mode 100644 docs/merise/_diagrams/mcd-commande.svg create mode 100644 docs/merise/_diagrams/mcd-global.svg create mode 100644 docs/merise/_diagrams/mcd-rbac.svg create mode 100644 docs/merise/_diagrams/mld-catalogue.drawio create mode 100644 docs/merise/_diagrams/mld-commande.drawio create mode 100644 docs/merise/_diagrams/mld-rbac.drawio create mode 100644 docs/merise/mcd.md create mode 100644 docs/merise/mct.md create mode 100644 docs/merise/mld.md create mode 100644 docs/merise/mlt.md create mode 100644 docs/uml/sequence-passer-commande.md create mode 100644 docs/uml/state-commande.md create mode 100644 docs/uml/use-cases.md diff --git a/docs/merise/_diagrams/mcd-catalogue.svg b/docs/merise/_diagrams/mcd-catalogue.svg new file mode 100644 index 0000000..c3a2016 --- /dev/null +++ b/docs/merise/_diagrams/mcd-catalogue.svg @@ -0,0 +1,4 @@ + + + +
CATEGORIE
id : INT (PK)
libelle : VARCHAR (UNIQUE)
slug : VARCHAR (UNIQUE)
image_path : VARCHAR
ordre : SMALLINT
est_actif : BOOLEAN
CATEGORIEid : INT (PK)...
PRODUIT
id : INT (PK)
categorie_id : INT (FK)
libelle : VARCHAR
description : TEXT
prix_ttc_cents : INT
image_path : VARCHAR
est_disponible : BOOLEAN
ordre : SMALLINT
PRODUITid : INT (PK)...
MENU
id : INT (PK)
categorie_id : INT (FK)
libelle : VARCHAR
description : TEXT
prix_ttc_cents : INT
image_path : VARCHAR
est_disponible : BOOLEAN
ordre : SMALLINT
MENUid : INT (PK)...
MENU_PRODUIT (associative)
menu_id : INT (PK, FK)
produit_id : INT (PK, FK)
role : ENUM
position : SMALLINT
MENU_PRODUIT (associative)menu_id : INT (PK,...
regroupe
regroupe
(0,N)
(0,N)
(1,1)
(1,1)
regroupe
regroupe
(0,N)
(0,N)
(1,1)
(1,1)
fait_partie_de
fait_partie_de
(0,N)
(0,N)
(1,1)
(1,1)
compose
compose
(1,N)
(1,N)
(1,1)
(1,1)
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/merise/_diagrams/mcd-commande.drawio b/docs/merise/_diagrams/mcd-commande.drawio index 95063f1..a6ef277 100644 --- a/docs/merise/_diagrams/mcd-commande.drawio +++ b/docs/merise/_diagrams/mcd-commande.drawio @@ -5,8 +5,16 @@ - - + + + + + + + + + + @@ -55,6 +63,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/merise/_diagrams/mcd-commande.svg b/docs/merise/_diagrams/mcd-commande.svg new file mode 100644 index 0000000..2d4be3b --- /dev/null +++ b/docs/merise/_diagrams/mcd-commande.svg @@ -0,0 +1,4 @@ + + + +
COMMANDE
id : INT (PK)
numero : VARCHAR (UNIQUE)
source : ENUM (kiosk|comptoir|drive)
mode_consommation : ENUM (sur_place|a_emporter|drive)
statut : ENUM
total_ht_cents : INT
total_tva_cents : INT
total_ttc_cents : INT
tva_taux_pourmille : SMALLINT
paye_a : DATETIME
USER
id : INT (PK)
(detail dans RBAC)
COMMANDE_EVENT
id : INT (PK)
commande_id : INT (FK)
event_type : ENUM
from_statut : ENUM (NULL)
to_statut : ENUM
user_id : INT (FK, NULL)
payload : JSON (NULL)
created_at : DATETIME
LIGNE_COMMANDE
id : INT (PK)
commande_id : INT (FK)
type_item : ENUM (produit|menu)
produit_id : INT (FK, NULL)
menu_id : INT (FK, NULL)
libelle_snapshot : VARCHAR
prix_unitaire_ttc_cents_snapshot : INT
quantite : SMALLINT
PRODUIT
id : INT (PK)
(detail dans Catalogue)
MENU
id : INT (PK)
(detail dans Catalogue)
contient
(1,N)
(1,1)
refere_si_type_produit
(0,1)
(0,N)
refere_si_type_menu
(0,N)
Polymorphisme
Exactement UNE des deux references est non-nulle.
Discriminateur : type_item ∈ {produit, menu}.
Contrainte CHECK SQL au MLD.
journalise
(1,N)
(1,1)
declenche
(0,N)
(0,1)
(0,1)
Journal d'audit (event sourcing)
Append-only : aucun UPDATE / DELETE applicatif.
user_id NULL si auto-validation kiosk.
ON DELETE CASCADE cote commande_id.
ON DELETE SET NULL cote user_id.
\ No newline at end of file diff --git a/docs/merise/_diagrams/mcd-global.drawio b/docs/merise/_diagrams/mcd-global.drawio index 6d010db..962f01b 100644 --- a/docs/merise/_diagrams/mcd-global.drawio +++ b/docs/merise/_diagrams/mcd-global.drawio @@ -24,6 +24,9 @@ + + + @@ -148,6 +151,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/merise/_diagrams/mcd-global.svg b/docs/merise/_diagrams/mcd-global.svg new file mode 100644 index 0000000..1d26537 --- /dev/null +++ b/docs/merise/_diagrams/mcd-global.svg @@ -0,0 +1,4 @@ + + + +
CATEGORIE
PRODUIT
MENU_PRODUIT
MENU
LIGNE_COMMANDE
COMMANDE
COMMANDE_EVENT
USER
ROLE
ROLE_PERMISSION
PERMISSION
regroupe
(0,N)
(1,1)
regroupe
(0,N)
(1,1)
fait_partie_de
(0,N)
(1,1)
compose
(1,N)
(1,1)
contient
(1,N)
(1,1)
refere_si_type_produit
(0,1)
(0,N)
refere_si_type_menu
(0,1)
(0,N)
a_pour_role
(1,1)
(0,N)
possede
(0,N)
(1,1)
assignee_a
(0,N)
(1,1)
journalise
(1,N)
(1,1)
declenche
(0,N)
(0,1)
\ No newline at end of file diff --git a/docs/merise/_diagrams/mcd-rbac.svg b/docs/merise/_diagrams/mcd-rbac.svg new file mode 100644 index 0000000..e92d624 --- /dev/null +++ b/docs/merise/_diagrams/mcd-rbac.svg @@ -0,0 +1,4 @@ + + + +
USER
id : INT (PK)
email : VARCHAR (UNIQUE, RFC 5321)
password_hash : VARCHAR (argon2id)
nom : VARCHAR
prenom : VARCHAR
role_id : INT (FK)
est_actif : BOOLEAN
last_login_at : DATETIME
ROLE
id : INT (PK)
code : VARCHAR (UNIQUE)
libelle : VARCHAR
description : TEXT
est_actif : BOOLEAN
PERMISSION
id : INT (PK)
code : VARCHAR (UNIQUE, resource.action)
libelle : VARCHAR
description : TEXT
ROLE_PERMISSION (associative)
role_id : INT (PK, FK)
permission_id : INT (PK, FK)
a_pour_role
(1,1)
(0,N)
possede
(0,N)
(1,1)
assignee_a
(0,N)
(1,1)
\ No newline at end of file diff --git a/docs/merise/_diagrams/mld-catalogue.drawio b/docs/merise/_diagrams/mld-catalogue.drawio new file mode 100644 index 0000000..e292dbc --- /dev/null +++ b/docs/merise/_diagrams/mld-catalogue.drawio @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/merise/_diagrams/mld-commande.drawio b/docs/merise/_diagrams/mld-commande.drawio new file mode 100644 index 0000000..1a47a5d --- /dev/null +++ b/docs/merise/_diagrams/mld-commande.drawio @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/merise/_diagrams/mld-rbac.drawio b/docs/merise/_diagrams/mld-rbac.drawio new file mode 100644 index 0000000..0922801 --- /dev/null +++ b/docs/merise/_diagrams/mld-rbac.drawio @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/merise/dictionary.md b/docs/merise/dictionary.md index 70f7e40..326876e 100644 --- a/docs/merise/dictionary.md +++ b/docs/merise/dictionary.md @@ -167,8 +167,9 @@ Transaction client : 1 commande = 1 panier valide a un instant donne. |---|---|---|---|---|---| | `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | | `numero` | VARCHAR(20) | NO | - | UNIQUE | format humain ex : `K-2026-04-30-001`, genere a la creation | -| `mode_consommation` | ENUM('sur_place','a_emporter','drive') | NO | - | - | impacte la TVA et le flux operationnel | -| `statut` | ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') | NO | 'pending_payment' | INDEX | machine a etats (cf. MCT a venir) | +| `source` | ENUM('kiosk','comptoir','drive') | NO | - | INDEX | canal de saisie de la commande (cf. note 8) | +| `mode_consommation` | ENUM('sur_place','a_emporter','drive') | NO | - | - | mode de consommation fiscal et operationnel (impacte la TVA, cf. note 9) | +| `statut` | ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') | NO | 'pending_payment' | INDEX | machine a etats (cf. MCT) | | `total_ht_cents` | INT UNSIGNED | NO | - | CHECK >= 0 | snapshot calcule a la validation | | `total_tva_cents` | INT UNSIGNED | NO | - | CHECK >= 0 | snapshot | | `total_ttc_cents` | INT UNSIGNED | NO | - | CHECK > 0 | snapshot, doit valoir total_ht_cents + total_tva_cents (verification au MLT) | @@ -217,7 +218,36 @@ Argumentaire jury : integrite des donnees comptables. --- -### 3.7 `user` +### 3.7 `commande_event` + +Journal d'audit append-only : 1 ligne par changement d'etat d'une commande. Pattern +event sourcing simplifie (cf. note 10). Trace **qui** a fait **quoi**, **quand**, sur quelle +commande, avec quel contexte. Aucun update / delete autorise (immuable). + +| Attribut | Type | NULL | Defaut | Contrainte | Notes | +|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | +| `commande_id` | INT UNSIGNED | NO | - | FK -> `commande(id)`, ON DELETE CASCADE | si la commande disparait, son journal aussi | +| `event_type` | ENUM('CREATED','PAID','PREPARING_STARTED','READY','DELIVERED','CANCELLED') | NO | - | INDEX | type d'evenement, aligne sur la machine a etats | +| `from_statut` | ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') | YES | NULL | - | statut avant transition (NULL pour CREATED) | +| `to_statut` | ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') | NO | - | - | statut apres transition | +| `user_id` | INT UNSIGNED | YES | NULL | FK -> `user(id)`, ON DELETE SET NULL | NULL si auto-validation kiosk ou system event ; sinon = equipier qui a declenche | +| `payload` | JSON | YES | NULL | - | contexte additionnel : raison annulation, methode paiement, montant rembourse, etc. | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | INDEX | timestamp immuable de l'evenement | + +**Cle primaire** : `id`. + +**Index supplementaires** : +- `(commande_id, created_at)` pour requete "historique d'une commande" +- `(user_id, created_at)` pour requete "actions d'un equipier sur une periode" + +**Volume** : ~5-8 events par commande (1 CREATED + 1 PAID + 1 PREPARING + 1 READY + 1 DELIVERED, plus eventuels CANCELLED). Sur 6 mois, ~50k-80k lignes. + +**ON DELETE SET NULL sur `user_id`** : si un user est supprime (rare, cf. soft delete), les events restent (audit preserve) mais l'attribution est perdue. Le brief peut imposer `ON DELETE RESTRICT` si l'integrite de l'audit est critique. + +--- + +### 3.8 `user` Utilisateur du back-office (admin, manager, equipier) - **pas** les clients de la borne, qui ne sont pas authentifies. @@ -242,7 +272,7 @@ total = 254 (incluant le `@`). VARCHAR(254) est la valeur conforme spec. --- -### 3.8 `role` +### 3.9 `role` Roles utilisables dans le back-office (RBAC). Creables / modifiables / desactivables depuis l'UI admin (les permissions sont statiques, declarees en migration). @@ -262,7 +292,7 @@ via UI admin sans deploiement. --- -### 3.9 `permission` +### 3.10 `permission` Permissions granulaires assignables aux roles (ex : `produit.create`, `commande.read`). @@ -279,7 +309,7 @@ commande, stats). --- -### 3.10 `role_permission` (jointure) +### 3.11 `role_permission` (jointure) Mapping N-N entre roles et permissions. @@ -295,149 +325,9 @@ permissions, les autres roles un sous-ensemble). --- -## 4. Diagramme entites-relations (preview MCD) +## 4. Notes de modelisation -Diagramme rendu en Mermaid (visible directement dans GitHub et la plupart des viewers -markdown). La syntaxe `erDiagram` cible Merise : entites + cardinalites min/max. - -```mermaid -erDiagram - CATEGORIE { - int id PK - varchar libelle "UNIQUE" - varchar slug "UNIQUE" - varchar image_path - smallint ordre - boolean est_actif - datetime created_at - datetime updated_at - } - - PRODUIT { - int id PK - int categorie_id FK - varchar libelle - text description - int prix_ttc_cents "centimes" - varchar image_path - boolean est_disponible - smallint ordre - datetime created_at - datetime updated_at - } - - MENU { - int id PK - int categorie_id FK - varchar libelle - text description - int prix_ttc_cents "centimes" - varchar image_path - boolean est_disponible - smallint ordre - datetime created_at - datetime updated_at - } - - MENU_PRODUIT { - int menu_id PK_FK - int produit_id PK_FK - enum role "burger|accompagnement|boisson|sauce|dessert" - smallint position - } - - COMMANDE { - int id PK - varchar numero "UNIQUE" - enum mode_consommation "sur_place|a_emporter|drive" - enum statut "pending_payment|paid|preparing|ready|delivered|cancelled" - int total_ht_cents - int total_tva_cents - int total_ttc_cents - smallint tva_taux_pourmille - datetime paye_a - datetime created_at - datetime updated_at - } - - LIGNE_COMMANDE { - int id PK - int commande_id FK - enum type_item "produit|menu" - int produit_id FK_nullable - int menu_id FK_nullable - varchar libelle_snapshot - int prix_unitaire_ttc_cents_snapshot - smallint quantite - datetime created_at - } - - USER { - int id PK - varchar email "UNIQUE - RFC 5321" - varchar password_hash "argon2id" - varchar nom - varchar prenom - int role_id FK - boolean est_actif - datetime last_login_at - datetime created_at - datetime updated_at - } - - ROLE { - int id PK - varchar code "UNIQUE" - varchar libelle - text description - boolean est_actif - datetime created_at - datetime updated_at - } - - PERMISSION { - int id PK - varchar code "UNIQUE - resource.action" - varchar libelle - text description - datetime created_at - } - - ROLE_PERMISSION { - int role_id PK_FK - int permission_id PK_FK - } - - CATEGORIE ||--o{ PRODUIT : "regroupe" - CATEGORIE ||--o{ MENU : "regroupe" - MENU ||--|{ MENU_PRODUIT : "compose" - PRODUIT ||--o{ MENU_PRODUIT : "fait_partie_de" - COMMANDE ||--|{ LIGNE_COMMANDE : "contient" - LIGNE_COMMANDE }o--o| PRODUIT : "refere_si_type_produit" - LIGNE_COMMANDE }o--o| MENU : "refere_si_type_menu" - USER }o--|| ROLE : "a_pour_role" - ROLE ||--o{ ROLE_PERMISSION : "possede" - PERMISSION ||--o{ ROLE_PERMISSION : "assignee_a" -``` - -### Lecture des cardinalites Mermaid - -| Notation | Signification | -|---|---| -| `\|\|--o{` | exactement 1 -> 0 ou plusieurs | -| `\|\|--\|{` | exactement 1 -> 1 ou plusieurs (au moins 1 obligatoire) | -| `}o--\|\|` | 0 ou plusieurs -> exactement 1 | -| `}o--o\|` | 0 ou plusieurs -> 0 ou 1 (relation optionnelle) | - -**Cardinalites cles** : -- `MENU ||--|{ MENU_PRODUIT` : un menu doit avoir au moins 1 entree de composition (regle metier : un menu vide n'a pas de sens) -- `COMMANDE ||--|{ LIGNE_COMMANDE` : une commande sans ligne ne devrait pas exister (controle au MLT) -- `LIGNE_COMMANDE }o--o| PRODUIT` et `}o--o| MENU` : la ligne ne pointe que sur l'un des deux selon `type_item` (polymorphisme) -- `USER }o--|| ROLE` : un user doit avoir un role (`role_id` NOT NULL FK) - ---- - -## 5. Notes de modelisation +> Le diagramme entites-relations et les justifications de cardinalites sont documentes dans [`mcd.md`](mcd.md) (diagrammes drawio des 4 sous-domaines + recapitulatif global). Le dictionnaire ne dedouble pas cette vue pour eviter d'avoir deux sources de verite divergeantes. ### Note 1 - Pourquoi `INT UNSIGNED` en centimes pour les prix @@ -522,9 +412,93 @@ Choix retenu : 2 colonnes + 2 FKs + contrainte CHECK. Cout : 1 colonne supplemen la source ecole (max observe : 41 chars). Marge 3x. - `slug` : VARCHAR(60) - coherent avec les conventions URL kebab-case courantes. +### Note 8 - `source` vs `mode_consommation` (separation canal / fiscalite) + +Deux dimensions distinctes que la modelisation Wakdo separe explicitement : + +| | `source` | `mode_consommation` | +|---|---|---| +| Nature | canal de saisie de la commande (input) | mode de consommation (output) | +| Valeurs | kiosk, comptoir, drive | sur_place, a_emporter, drive | +| Decision metier | qui a saisi la commande, authentification, analytics | TVA applicable, gestion capacite salle | + +Les deux dimensions sont independantes pour `kiosk` et `comptoir` (un client a la borne peut choisir sur_place OU a_emporter ; idem au comptoir). Le `drive` est le seul cas ou les deux dimensions sont identiques : `source=drive` implique `mode_consommation=drive`. + +Cette contrainte croisee est verifiee a l'ecriture (MLT - precondition de l'operation `creer_commande`). En SQL elle pourrait etre exprimee par un CHECK : `CHECK (source != 'drive' OR mode_consommation = 'drive')`. + +### Note 9 - TVA en restauration rapide chez Wakdo + +Wakdo est un fast-food, pas un restaurant a service a table : quel que soit le `mode_consommation`, tout est servi en emballages papier (sur plateau pour `sur_place`, en sac pour `a_emporter` et `drive`). La distinction `sur_place` vs `a_emporter` ne porte donc pas sur le service mais sur : + +- **TVA applicable** : 10% pour la consommation immediate sur place, 5,5% pour les produits a emporter destines a la consommation differee (cf. service-public.fr article F31407, 2024) +- **Occupation salle** : le client `sur_place` consomme une place assise (utile si une feature capacite est ajoutee plus tard) + +Le taux de TVA est snapshote dans `commande.tva_taux_pourmille` au moment de la transaction pour preserver l'integrite historique si la legislation evolue. + +### Note 10 - Pattern event sourcing simplifie via `commande_event` + +Plutot que d'ajouter des colonnes `saisi_par_id`, `valide_par_id`, `prepare_par_id`, `livre_par_id` sur `commande` (denormalisation lourde, 4 FKs), Wakdo retient une table d'audit dediee `commande_event` (cf. entite 3.7). + +**Principe** : `commande` porte uniquement l'**etat courant** (`statut`). Chaque transition d'etat insere une ligne dans `commande_event` (append-only, immuable). Pour reconstituer l'historique d'une commande : `SELECT * FROM commande_event WHERE commande_id = ? ORDER BY created_at`. + +**Avantages** : +- Tracabilite complete sans charger `commande` de colonnes peu remplies +- Extensible : ajouter un nouveau type d'evenement (REFUNDED, RECLAIMED, ...) = ajouter une valeur a l'ENUM `event_type`, sans migration intrusive +- Compatible avec analytics fines : "temps moyen entre PAID et READY par equipier" via JOIN sur `(user_id, event_type)` + +**Couts assumes** : +- Pattern d'ecriture systematique a respecter : chaque service qui modifie `commande.statut` doit aussi inserer dans `commande_event`. A encapsuler dans un repository pour eviter les oublis. +- Volume table x5-x8 par rapport a `commande` +- Requete "qui a saisi cette commande" demande un join (pas de denormalisation `saisi_par_id` directe) + +Si le cout SQL devient penible plus tard, on pourra dupliquer `saisi_par_id` sur `commande` comme colonne denormalisee, sans changer le pattern event. + +**Defendable a l'oral** comme "audit log applicatif" ou "event sourcing simplifie", aligne sur les pratiques de tracabilite des SI en production. + +### Note 11 - Stockage des images : path en VARCHAR vs BLOB en DB + +Les colonnes `image_path` (entites `categorie`, `produit`, `menu`) stockent un **chemin relatif** au public root (ex : `/uploads/produits/burger-classique.jpg`), pas un chemin absolu serveur. Le PHP resout via un prefixe configure dans `.env` (`UPLOAD_DIR=public/uploads`). + +#### Pourquoi pas un BLOB en BDD ? + +L'alternative consistant a stocker les images en LONGBLOB dans MariaDB a ete consideree puis ecartee : + +| Critere | `image_path` VARCHAR (retenu) | BLOB en DB | +|---|---|---| +| Performance kiosk | Apache sert le fichier en ms (cache OS) | PHP lit la DB + streame, latence multipliee | +| Cache HTTP | ETag, Last-Modified, cache browser, CDN natifs | A reimplementer cote PHP | +| Backup BDD | Quelques Mo (paths uniquement) | Croissance Go (66 produits x ~200 Ko + variantes responsive) | +| Replication / dump | Rapide | Lente, ralentit les ACK | +| Pipeline image | `convert`, `webp`, optimisation = outils filesystem standards | A reinventer en PHP | +| Cout cloud (si migration) | Storage S3-like cheap | BDD storage cher | + +Pour un MVP fast-food avec borne tactile reactive, le filesystem est le choix par defaut documente dans la litterature web (cf. references). Le BLOB en DB se justifie pour des cas specifiques (fichiers sensibles avec acces controle par ligne, garantie ACID sur le contenu) qui ne s'appliquent pas a un catalogue produit public. + +#### Le "leak" de path n'en est pas un + +Argument souvent entendu : "stocker un chemin en DB expose la structure du serveur". Analyse : + +- `image_path` contient un chemin **relatif** (`/uploads/produits/...`), pas absolu. +- Cette URL est par definition **publique** : la borne kiosk affiche `` que n'importe quel visiteur voit dans le HTML. +- Pour acceder a la colonne `image_path` en DB, un attaquant doit deja avoir une breche DB (SQLi, credentials voles). A ce stade il a deja toutes les donnees metier (commandes, password_hash, etc.) ; connaitre `/uploads/produits/` est l'info la moins critique de la DB. + +#### Les vrais risques securite filesystem (traites par ailleurs) + +1. **Path traversal a l'upload** : valider que le nom de fichier upload passe par `basename()` + regex `^[a-z0-9_-]+\.(jpg|png|webp)$` cote service admin. +2. **MIME type spoof** : verifier le vrai MIME via `finfo_file()` (extension `.jpg` ne suffit pas). Desactiver l'execution PHP dans `/uploads/` via Apache (`php_flag engine off` + `FilesMatch .(php|phtml|phar)$ deny`). +3. **Stockage hors-webroot pour les fichiers sensibles** : pas applicable au catalogue public, mais regle de principe pour PDF de facturation, exports stats, etc. +4. **Validation taille** : `UPLOAD_MAX_SIZE_MB` dans `.env` + verification PHP cote upload. +5. **Nom non-predictible pour fichiers sensibles** : UUID au lieu du nom metier si l'image contient des donnees sensibles. Pas applicable a un catalogue public. + +#### Sources + +- OWASP File Upload Cheat Sheet (section "Filesystem storage") +- MariaDB Knowledge Base - LONGBLOB performance considerations +- Apache HTTP Server documentation - `mod_xsendfile` et serving static content + --- -## 6. A faire au prochain sprint (MCD) +## 5. A faire au prochain sprint (MCD) - Tracer le MCD avec les cardinalites precises (entites + associations + roles + cardinalites min/max) diff --git a/docs/merise/mcd.md b/docs/merise/mcd.md new file mode 100644 index 0000000..1bdbc8e --- /dev/null +++ b/docs/merise/mcd.md @@ -0,0 +1,309 @@ +# Modele Conceptuel des Donnees (MCD) - Wakdo + +**Phase Merise** : P1 - Conception, etape 2 (apres dictionnaire de donnees) +**Statut** : v0.1 +**Date** : 2026-04-30 +**Branche** : `feat/p1-conception` + +--- + +## 1. Objet du document + +Le MCD (Modele Conceptuel des Donnees) formalise les **entites** du domaine +Wakdo, leurs **associations** et les **cardinalites** qui regissent ces +associations. Il est la traduction normalisee du dictionnaire de donnees, et +sert de base au MLD (Modele Logique des Donnees) qui produira le schema +relationnel. + +A la difference du dictionnaire (qui detaille les attributs et types), le MCD +focalise sur la structure relationnelle : combien de X pour un Y, est-ce +obligatoire, peut-il y avoir des relations sans participants ? + +**Sources** : +- `docs/merise/dictionary.md` (entites + attributs identifies) +- `docs/PROJECT_CONTEXT.md` (regles metier : composition menu, parcours commande, RBAC) +- `docs/merise/_sources/` (donnees ecole : 9 categories + 53 produits + 13 menus) + +--- + +## 2. Notation Merise utilisee + +### Cardinalites au pied de l'association (style francais) + +A chaque extremite d'une association, la cardinalite `(min, max)` precise +combien de fois une instance de l'entite participe a l'association. + +``` +ENTITE_A (min,max) ----[ ASSOCIATION ]---- (min,max) ENTITE_B +``` + +| Notation | Lecture | Exemple | +|---|---|---| +| `(0,1)` | Optionnel, au plus 1 | Un user a (0,1) avatar | +| `(1,1)` | Obligatoire, exactement 1 | Un produit appartient a (1,1) categorie | +| `(0,N)` | Optionnel, illimite | Une categorie regroupe (0,N) produits | +| `(1,N)` | Obligatoire au moins 1, illimite | Une commande contient (1,N) lignes | + +Lecture francaise : "une instance de l'entite-source participe au moins MIN +fois et au plus MAX fois a l'association". + +### Convention nommage des associations + +Verbe a l'infinitif au sens metier, ex : `regroupe`, `compose`, `contient`, +`refere`, `a_pour_role`, `possede`. + +Les associations qui portent des attributs (= relations N-N enrichies) +deviennent des **entites associatives** au MLD (table de jointure avec +colonnes propres). + +--- + +## 3. Vue d'ensemble (diagramme global) + +Diagramme entites-relations dessine dans draw.io, exporte en SVG. Les +sources `.drawio` editables sont dans `_diagrams/`. Cardinalites Merise +`(min,max)` notees directement sur les associations. Recapitulatif des +cardinalites en section 5. + +![MCD - Diagramme global](_diagrams/mcd-global.svg) + +*Source : [`_diagrams/mcd-global.drawio`](_diagrams/mcd-global.drawio)* + +> **A regenerer** : le diagramme global doit etre mis a jour pour inclure l'entite `COMMANDE_EVENT` (11 entites au total) et l'attribut `source` sur `COMMANDE`. Le SVG actuel reflete l'etat anterieur a ces decisions. + +### Lecture rapide + +- Une `CATEGORIE` regroupe `(0,N)` `PRODUIT` ou `MENU` ; un `PRODUIT` ou un + `MENU` appartient a `(1,1)` categorie (chacun cote sa categorie de + rattachement). +- Un `MENU` est compose de `(1,N)` produits (un menu sans composition n'a pas + de sens metier) ; un `PRODUIT` peut faire partie de `(0,N)` menus + (independance). +- Une `COMMANDE` contient `(1,N)` `LIGNE_COMMANDE` (commande vide impossible) ; + une ligne appartient a `(1,1)` commande. +- Une `LIGNE_COMMANDE` refere `(0,1)` `PRODUIT` OU `(0,1)` `MENU` selon le + discriminateur `type_item` (polymorphisme). +- Une `COMMANDE` est journalisee par `(1,N)` `COMMANDE_EVENT` (au moins 1 event `CREATED`, append-only). +- Un `USER` declenche `(0,N)` `COMMANDE_EVENT` (NULL possible si event auto-kiosk). +- Un `USER` a `(1,1)` `ROLE` (un user sans role ne peut pas se connecter) ; + un `ROLE` peut etre porte par `(0,N)` users. +- Un `ROLE` possede `(0,N)` `PERMISSION` via `ROLE_PERMISSION` (matrice N-N). + +--- + +## 4. Decomposition par sous-domaine + +Le modele est segmente en 3 sous-domaines pour faciliter la lecture et +l'analyse : + +1. **Catalogue** : produits, menus, categories, composition +2. **Commande** : commande, lignes, references polymorphiques +3. **RBAC** : users, roles, permissions, mapping + +### 4.1 Sous-domaine Catalogue + +![MCD - Catalogue](_diagrams/mcd-catalogue.svg) + +*Source : [`_diagrams/mcd-catalogue.drawio`](_diagrams/mcd-catalogue.drawio)* + +#### Justification des cardinalites + +| Cote | Cardinalite | Justification | +|---|---|---| +| Categorie -> Produit | `(0,N)` cote Categorie | Une categorie peut etre creee a vide (ex : "petit dejeuner" ajoutee sans produit initial). Maximum illimite (au moins 53 produits dans la source actuelle, repartis sur 9 categories). | +| Categorie -> Produit | `(1,1)` cote Produit | Tout produit doit etre rattache a une categorie pour etre affiche sur la borne. Pas de produit "orphelin". | +| Categorie -> Menu | `(0,N)` cote Categorie | Toutes les categories sauf "menus" ont 0 menu. La categorie "menus" en a 13. | +| Categorie -> Menu | `(1,1)` cote Menu | Tout menu est rattache a la categorie "menus" (par construction du modele). Un menu sans categorie ne s'affiche pas sur la borne. | +| Menu -> MenuProduit | `(1,N)` cote Menu | Un menu doit avoir au moins 1 produit dans sa composition. Sans composition, le menu est invendable. | +| Produit -> MenuProduit | `(0,N)` cote Produit | Un produit peut exister independamment des menus (vente a la carte uniquement). Inversement, un produit peut entrer dans plusieurs menus differents (ex : frites moyennes presentes dans plusieurs combos). | + +#### Note sur l'entite associative `MENU_PRODUIT` + +`MENU_PRODUIT` est une entite associative : la relation N-N entre `MENU` et +`PRODUIT` porte deux attributs metier (`role` et `position`). Au MLD, elle +deviendra une table de jointure avec PK composite `(menu_id, produit_id)`. + +### 4.2 Sous-domaine Commande + +![MCD - Commande](_diagrams/mcd-commande.svg) + +*Source : [`_diagrams/mcd-commande.drawio`](_diagrams/mcd-commande.drawio)* + +> **A regenerer** : le diagramme `mcd-commande.drawio` doit etre mis a jour pour inclure l'entite `COMMANDE_EVENT` (cf. section 4.2.bis ci-dessous) et l'attribut `source` sur `COMMANDE`. Le SVG actuel reflete l'etat anterieur a ces decisions. + +#### Justification des cardinalites + +| Cote | Cardinalite | Justification | +|---|---|---| +| Commande -> LigneCommande | `(1,N)` cote Commande | Une commande sans aucune ligne n'a pas de sens metier. Au moment de la validation (passage de `pending_payment` a `paid`), au moins 1 ligne est presente. | +| Commande -> LigneCommande | `(1,1)` cote LigneCommande | Toute ligne appartient a exactement une commande. ON DELETE CASCADE (si commande supprimee, lignes aussi). | +| LigneCommande -> Produit | `(0,N)` cote Produit | Un produit peut etre commande des centaines de fois. Maximum non borne. | +| LigneCommande -> Produit | `(0,1)` cote LigneCommande | Selon `type_item` : si `'produit'` -> 1 produit reference ; si `'menu'` -> 0 (la colonne `produit_id` est NULL). | +| LigneCommande -> Menu | `(0,N)` cote Menu | Symetrique de Produit. | +| LigneCommande -> Menu | `(0,1)` cote LigneCommande | Selon `type_item` : si `'menu'` -> 1 menu reference ; si `'produit'` -> 0. | +| Commande -> CommandeEvent | `(1,N)` cote Commande | Toute commande a au moins 1 event (CREATED) ; en pratique 5-8 events sur tout son cycle de vie. | +| Commande -> CommandeEvent | `(1,1)` cote CommandeEvent | Chaque event appartient a exactement une commande. ON DELETE CASCADE. | +| User -> CommandeEvent | `(0,N)` cote User | Un equipier peut declencher 0 a N events (un nouveau user n'a encore rien fait). | +| User -> CommandeEvent | `(0,1)` cote CommandeEvent | NULL si event auto (kiosk paye via CB sans equipier) ou si le user a ete supprime (ON DELETE SET NULL preserve l'audit). | + +#### Note sur le polymorphisme + +La cardinalite `(0,1)` cote LigneCommande pour les deux associations +`refere_si_type_produit` et `refere_si_type_menu` reflete le polymorphisme : +**exactement une** des deux references est non-nulle, l'autre est nulle. +Cette regle d'exclusivite est a renforcer au MLD via une contrainte CHECK +SQL ou une regle applicative : + +```sql +CHECK ( + (type_item = 'produit' AND produit_id IS NOT NULL AND menu_id IS NULL) + OR (type_item = 'menu' AND menu_id IS NOT NULL AND produit_id IS NULL) +) +``` + +Voir `docs/notes/polymorphic-fk-snapshots.md` pour le detail du choix de +modelisation polymorphique. + +#### 4.2.bis Journal d'audit `COMMANDE_EVENT` (event sourcing simplifie) + +`COMMANDE_EVENT` est une entite append-only qui journalise chaque changement d'etat d'une commande, avec l'auteur de la transition (un `USER` ou NULL si auto). Pattern event sourcing simplifie (cf. note 10 du dictionnaire). + +Trois proprietes invariantes : + +1. **Append-only** : aucun UPDATE / DELETE applicatif sur `commande_event`. Garantie d'integrite de l'audit. +2. **Lien fort a la commande** : `ON DELETE CASCADE` cote `commande_id` (si la commande disparait, son journal aussi). +3. **Lien faible a l'user** : `ON DELETE SET NULL` cote `user_id` (si un equipier est supprime, les events restent avec `user_id = NULL` ; l'audit reste consultable, seule l'attribution individuelle est perdue). + +La contrainte croisee `(source, mode_consommation)` introduite par l'attribut `source` sur `COMMANDE` (cf. dictionnaire note 8) est verifiee au MLT lors de la creation de la commande, pas au MCD. + +### 4.3 Sous-domaine RBAC + +![MCD - RBAC](_diagrams/mcd-rbac.svg) + +*Source : [`_diagrams/mcd-rbac.drawio`](_diagrams/mcd-rbac.drawio)* + +#### Justification des cardinalites + +| Cote | Cardinalite | Justification | +|---|---|---| +| User -> Role | `(1,1)` cote User | Un user doit avoir un role pour acceder au back-office. Pas de connexion sans role. | +| User -> Role | `(0,N)` cote Role | Un role peut exister sans aucun user (ex : nouveau role cree dans l'UI admin avant d'etre assigne). | +| Role -> Permission (via ROLE_PERMISSION) | `(0,N)` cote Role | Un role peut avoir 0 permission (role "vide" ou "en construction") jusqu'a toutes les permissions (admin). | +| Role -> Permission (via ROLE_PERMISSION) | `(0,N)` cote Permission | Une permission peut etre assignee a 0 role (permission declaree mais pas encore distribuee) ou a plusieurs roles. | + +#### Note sur le modele RBAC + +Le RBAC retenu est **dynamique cote roles** (creables/modifiables via UI +admin) et **statique cote permissions** (declarees en migration, liees au +code applicatif). Voir `docs/notes/rbac-roles-permissions.md` pour le detail +du choix. + +--- + +## 5. Recapitulatif global des cardinalites + +| # | Association | Cote A | Cardinalite A | Cote B | Cardinalite B | +|---|---|---|---|---|---| +| 1 | regroupe (Categorie -> Produit) | Categorie | (0,N) | Produit | (1,1) | +| 2 | regroupe (Categorie -> Menu) | Categorie | (0,N) | Menu | (1,1) | +| 3 | compose (Menu <-> Produit via MenuProduit) | Menu | (1,N) | Produit | (0,N) | +| 4 | contient (Commande -> LigneCommande) | Commande | (1,N) | LigneCommande | (1,1) | +| 5 | refere_si_type_produit (LigneCommande -> Produit) | LigneCommande | (0,1) | Produit | (0,N) | +| 6 | refere_si_type_menu (LigneCommande -> Menu) | LigneCommande | (0,1) | Menu | (0,N) | +| 7 | journalise (Commande -> CommandeEvent) | Commande | (1,N) | CommandeEvent | (1,1) | +| 8 | declenche (User -> CommandeEvent) | User | (0,N) | CommandeEvent | (0,1) | +| 9 | a_pour_role (User -> Role) | User | (1,1) | Role | (0,N) | +| 10 | possede (Role <-> Permission via RolePermission) | Role | (0,N) | Permission | (0,N) | + +--- + +## 6. Cross-validation avec le dictionnaire de donnees + +Verification que chaque entite du dictionnaire est presente dans le MCD et +inversement. + +| Entite dictionnaire (section 3) | Presente dans MCD ? | Sous-domaine | +|---|---|---| +| `categorie` (3.1) | Oui | Catalogue | +| `produit` (3.2) | Oui | Catalogue | +| `menu` (3.3) | Oui | Catalogue | +| `menu_produit` (3.4) | Oui (entite associative) | Catalogue | +| `commande` (3.5) | Oui | Commande | +| `ligne_commande` (3.6) | Oui | Commande | +| `commande_event` (3.7) | Oui (journal d'audit) | Commande | +| `user` (3.8) | Oui | RBAC | +| `role` (3.9) | Oui | RBAC | +| `permission` (3.10) | Oui | RBAC | +| `role_permission` (3.11) | Oui (entite associative) | RBAC | + +Coherence : 100%. Aucune entite du dictionnaire n'est absente du MCD, aucune +entite du MCD n'est en plus du dictionnaire. + +--- + +## 7. Decisions reportees au MLD + +Le MCD reste au niveau conceptuel. Les decisions suivantes sont reportees au +MLD (modele logique des donnees, etape suivante) : + +1. **Resolution des entites associatives en tables** : `MENU_PRODUIT` et + `ROLE_PERMISSION` deviendront des tables de jointure avec PK composite. +2. **Choix des PK techniques vs metier** : pour les entites principales, PK + technique `id INT UNSIGNED AUTO_INCREMENT`. Pour `commande`, garder + un identifiant metier `numero` UNIQUE en plus de l'id technique. +3. **Index techniques** : non discutes au MCD (couche logique). Au MLD : + index sur les FK, sur les colonnes `est_actif`/`est_disponible`, sur les + colonnes utilisees dans les `WHERE`/`ORDER BY` frequents (`created_at`, + `statut`). +4. **Contraintes CHECK SQL** : la regle d'exclusivite polymorphique sur + `LIGNE_COMMANDE` sera materialisee via CHECK en MariaDB 10.2+, ou via + trigger sur versions anteriures. +5. **Triggers / vues** : non identifies au MCD. A evaluer au MLD pour les + denormalisations utiles (vue `commande_avec_total` ?). + +--- + +## 8. Coherence avec le MCT + +Le MCT (Modele Conceptuel des Traitements) decrira les operations metier qui +manipulent ces entites : + +- **Composer panier** (kiosk) : creation de COMMANDE statut `pending_payment` + insertion COMMANDE_EVENT `CREATED` +- **Valider payment** : transition COMMANDE statut `pending_payment` -> `paid` + insertion COMMANDE_EVENT `PAID` +- **Preparer commande** (cuisine) : transition `paid` -> `preparing` + insertion COMMANDE_EVENT `PREPARING_STARTED` +- **Marquer pret** : transition `preparing` -> `ready` + insertion COMMANDE_EVENT `READY` +- **Remettre client** : transition `ready` -> `delivered` + insertion COMMANDE_EVENT `DELIVERED` +- **Annuler** : transition vers `cancelled` (depuis tout statut sauf `delivered`) + insertion COMMANDE_EVENT `CANCELLED` +- **CRUD admin** : operations sur PRODUIT, MENU, CATEGORIE, USER, ROLE + +Cross-validation MCD <-> MCT (mantra #34) : verifier au MCT que chaque +entite du MCD participe a au moins un traitement, et que chaque traitement +manipule des entites existantes du MCD. + +Pre-validation rapide (intuitive, a re-valider au MCT) : + +| Entite MCD | Au moins 1 traitement attendu ? | +|---|---| +| Categorie | Oui (CRUD admin) | +| Produit | Oui (CRUD admin + ajout panier) | +| Menu | Oui (CRUD admin + ajout panier) | +| MenuProduit | Oui (composition menu admin) | +| Commande | Oui (cycle de vie complet) | +| LigneCommande | Oui (creation panier) | +| CommandeEvent | Oui (insere a chaque transition de statut) | +| User | Oui (CRUD admin + login + declenchement events) | +| Role | Oui (CRUD admin + assignation) | +| Permission | Oui (consultation + assignation matrice) | +| RolePermission | Oui (matrice admin) | + +--- + +## 9. A faire au prochain sprint (MCT) + +- Lister exhaustivement les operations metier (acteurs, evenements + declencheurs, regles de declenchement) +- Modeliser les flux entre acteurs (client kiosk, equipier comptoir, equipier + cuisine, equipier drive, manager, admin) +- Identifier les synchronisations (ex : passage de `paid` a `preparing` peut + etre manuel cuisine ou automatique selon volume) +- Cross-valider MCD <-> MCT exhaustivement diff --git a/docs/merise/mct.md b/docs/merise/mct.md new file mode 100644 index 0000000..4df7ae5 --- /dev/null +++ b/docs/merise/mct.md @@ -0,0 +1,598 @@ +# Modele Conceptuel des Traitements (MCT) - Wakdo + +**Phase Merise** : P1 - Conception, etape 3 (apres MCD) +**Statut** : v0.1 +**Date** : 2026-05-21 +**Branche** : `feat/p1-conception` +**Auteur methodologie** : BYAN + +--- + +## 1. Objet du document + +Le MCT (Modele Conceptuel des Traitements) decrit les **operations metier** du domaine Wakdo +sous la forme canonique Merise : **evenement declencheur -> operation -> resultat emis**. + +Il repond a la question : que se passe-t-il dans le domaine, et quand ? +Il ne repond pas a la question : qui fait quoi, sur quel poste, dans quel ordre organisationnel +(cette dimension est volontairement omise - le MOT est saute, raccourci agile assume, coheret +avec le cadre RNCP solo). + +Le MCT couvre : +- Le parcours commande de bout en bout (borne kiosk, comptoir, drive) +- La gestion du catalogue (manager/admin) +- La gestion des utilisateurs et roles (admin) +- La connexion au back-office (tous acteurs back) + +**Acteurs identifies** : + +| Acteur | Code | Interface | +|--------|------|-----------| +| Client (borne) | CLIENT | Kiosk tactile (public, non authentifie) | +| Accueil | ACCUEIL | Back-office, role `accueil` | +| Preparation (cuisine) | CUISINE | Back-office, role `preparation` | +| Manager / Administrateur | ADMIN | Back-office, role `admin` | +| Systeme | SYS | Logique interne API / PHP | + +**Cross-reference MCD** : chaque operation manipule des entites du MCD (section 9). Le MCT est +construit en coherence avec la machine a etats de `commande.statut` : + +``` +pending_payment -> paid -> preparing -> ready -> delivered + | | | | + +-------------+-----------+----------+-> cancelled (depuis tout etat non remis) +``` + +--- + +## 2. Conventions de representation + +### Format d'une operation + +``` +[EVENEMENT(S) DECLENCHEUR(S)] + | + | [REGLE DE SYNCHRONISATION / CONDITION] + v + ( OPERATION ) + | + v +[RESULTAT(S) EMIS] +``` + +**Synchronisations** : +- `ET` : tous les evenements doivent etre presents simultanement pour declencher l'operation +- `OU` : l'un quelconque des evenements suffit + +**Conditions** : exprimees entre crochets `[condition]` sur l'arc entrant. + +### Notation textuelle adoptee + +Pour chaque operation, le document presente : +- **Evenement(s) declencheur(s)** : ce qui arrive et provoque l'operation +- **Acteur(s)** : qui est a l'origine (OU qui valide) +- **Synchronisation** : `ET` / `OU` si plusieurs evenements, condition +- **Operation** : nom de l'operation, description de ce qu'elle fait +- **Entites MCD touchees** : lecture (R) ou ecriture (W) sur les entites du MCD +- **Resultat(s)** : ce qui est emis ou produit a l'issue de l'operation + +--- + +## 3. Domaine 1 - Parcours commande (borne kiosk) + +Ce domaine couvre le cycle de vie d'une commande initiee depuis la borne client. + +### 3.1 Charger le catalogue + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | Le client ouvre la borne (connexion au kiosk) | +| **Acteur** | CLIENT | +| **Synchronisation** | Aucune (evenement unique) | +| **Condition** | La borne est en service (dans la plage horaire 10h00-01h00) | +| **Operation** | CHARGER_CATALOGUE | +| **Description** | Recuperation de la liste des categories actives, des produits disponibles et des menus disponibles pour affichage sur la borne | +| **Entites MCD** | R : `categorie` (est_actif=1), `produit` (est_disponible=1), `menu` (est_disponible=1), `menu_produit` | +| **Resultat** | Catalogue charge, borne affiche la page d'accueil | + +--- + +### 3.2 Composer le panier + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | Le client selectionne un produit ou un menu sur la borne | +| **Acteur** | CLIENT | +| **Synchronisation** | Evenement repetable (OU : ajout produit, ajout menu, modification quantite, suppression item) | +| **Condition** | Le produit ou menu selectionne est disponible (`est_disponible=1`) | +| **Operation** | COMPOSER_PANIER | +| **Description** | Construction du panier en memoire : ajout d'un article (produit unitaire ou menu), avec eventuellement une option de taille (+0,50 EUR sur accompagnements et boissons), recalcul du total TTC. Le panier est une structure volatile cote client ; aucune ecriture en BDD a ce stade. | +| **Entites MCD** | R : `produit`, `menu`, `menu_produit` - W : aucune (etat volatile front) | +| **Resultat** | Panier mis a jour, total recalcule, affichage recapitulatif | + +--- + +### 3.3 Valider et passer la commande + +| Champ | Valeur | +|-------|--------| +| **Evenements declencheurs** | 1. Client confirme le panier (appui sur "Valider") ET 2. Client saisit son numero de commande | +| **Acteur** | CLIENT | +| **Synchronisation** | ET (les deux actions sont requises) | +| **Condition** | Le panier contient au moins 1 article. Le numero saisi est non vide. | +| **Operation** | PASSER_COMMANDE | +| **Description** | Creation de la commande en base : insertion d'une ligne `commande` avec statut `pending_payment`, snapshot du total HT/TVA/TTC au taux en vigueur, source `kiosk`. Creation des lignes `ligne_commande` avec snapshot des libelles et prix. Le systeme genere le numero de commande au format `K-YYYY-MM-DD-NNN`. Le client saisit ensuite son numero de commande (substitut de paiement dans le cadre RNCP) : la commande passe au statut `paid`. La transition `pending_payment -> paid` est atomique dans cette operation. | +| **Entites MCD** | R : `produit`, `menu` (snapshot prix/libelle) - W : `commande` (INSERT statut `pending_payment`, puis UPDATE statut `paid`), `ligne_commande` (INSERT N lignes), `commande_event` (INSERT 2 events : `CREATED` user_id=NULL puis `PAID` user_id=NULL — kiosk = auto-validation, pas d'equipier) | +| **Resultat** | Commande creee (statut `paid` en fin d'operation), numero affiche au client, evenement COMMANDE_CREEE emis vers le domaine preparation | + +--- + +### 3.4 Confirmer la commande au client + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | COMMANDE_CREEE (retour API 201 apres PASSER_COMMANDE) | +| **Acteur** | SYS | +| **Synchronisation** | Aucune | +| **Condition** | La reponse API contient un id, un numero et un statut `paid` (la transition `pending_payment -> paid` s'est executee dans PASSER_COMMANDE) | +| **Operation** | AFFICHER_CONFIRMATION | +| **Description** | Affichage de l'ecran de confirmation sur la borne avec le numero de commande. La borne se reinitialise ensuite pour le client suivant. | +| **Entites MCD** | R : aucune nouvelle lecture BDD (les donnees sont dans la reponse API) | +| **Resultat** | Ecran de confirmation affiche, borne disponible pour la commande suivante | + +--- + +## 4. Domaine 2 - Parcours commande (comptoir et drive) + +Ce domaine couvre la saisie manuelle d'une commande par un equipier accueil pour un client +au comptoir ou au drive. + +### 4.1 Saisir une commande manuelle + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'equipier accueil initie une nouvelle commande depuis le back-office | +| **Acteur** | ACCUEIL | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur est authentifie et possede la permission `commande.create`. La source est `comptoir` ou `drive`. | +| **Operation** | SAISIR_COMMANDE_MANUELLE | +| **Description** | Composition du panier via le back-office : selection de produits et menus, choix du mode de consommation, choix de la source (`comptoir` ou `drive`). Logique identique a PASSER_COMMANDE cote kiosk, a la difference que l'acteur est un equipier authentifie. La transition `pending_payment -> paid` est atomique dans cette operation (l'equipier valide le paiement du client). | +| **Entites MCD** | R : `produit`, `menu`, `menu_produit` - W : `commande` (INSERT statut `pending_payment`, puis UPDATE statut `paid`, source `comptoir` ou `drive`), `ligne_commande` (INSERT), `commande_event` (INSERT 2 events : `CREATED` user_id=acteur puis `PAID` user_id=acteur) | +| **Resultat** | Commande creee (statut `paid` en fin d'operation), numero imprime ou annonce au client | + +--- + +## 5. Domaine 3 - Preparation (cuisine) + +### 5.1 Consulter les commandes a preparer + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'equipier cuisine accede a sa vue ou rafraichit la liste | +| **Acteur** | CUISINE | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur est authentifie et possede la permission `commande.read`. | +| **Operation** | LISTER_COMMANDES_A_PREPARER | +| **Description** | Lecture des commandes de statut `paid` triees par `created_at` croissant (heure de passage croissante, tous canaux confondus). Affichage du numero, du contenu (lignes avec libelle snapshot), et de la source (kiosk/comptoir/drive). | +| **Entites MCD** | R : `commande` (statut=`paid`), `ligne_commande` | +| **Resultat** | Liste des commandes en attente de preparation affichee, triee par heure croissante | + +--- + +### 5.2 Marquer une commande en preparation + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'equipier cuisine clique sur "Prendre en charge" pour une commande | +| **Acteur** | CUISINE | +| **Synchronisation** | Aucune | +| **Condition** | La commande est au statut `paid`. L'acteur possede la permission `commande.update`. | +| **Operation** | MARQUER_EN_PREPARATION | +| **Description** | Transition de statut `paid` -> `preparing` sur la commande. Mise a jour de `updated_at`. La commande disparait de la file "a preparer" et passe dans la file "en preparation". | +| **Entites MCD** | W : `commande` (UPDATE statut `paid` -> `preparing`), `commande_event` (INSERT event `PREPARING_STARTED` user_id=acteur) | +| **Resultat** | Commande au statut `preparing`, evenement COMMANDE_EN_PREPARATION emis | + +--- + +### 5.3 Marquer une commande prete + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'equipier cuisine clique sur "Pret" pour une commande en preparation | +| **Acteur** | CUISINE | +| **Synchronisation** | Aucune | +| **Condition** | La commande est au statut `preparing`. L'acteur possede la permission `commande.update`. | +| **Operation** | MARQUER_PRETE | +| **Description** | Transition de statut `preparing` -> `ready`. Mise a jour de `updated_at`. La commande est desormais visible pour l'accueil qui peut la remettre au client. | +| **Entites MCD** | W : `commande` (UPDATE statut `preparing` -> `ready`), `commande_event` (INSERT event `READY` user_id=acteur) | +| **Resultat** | Commande au statut `ready`, evenement COMMANDE_PRETE emis vers l'accueil | + +--- + +## 6. Domaine 4 - Remise au client (accueil) + +### 6.1 Consulter les commandes pretes + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'equipier accueil accede a sa vue ou rafraichit la liste | +| **Acteur** | ACCUEIL | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur est authentifie et possede la permission `commande.read`. | +| **Operation** | LISTER_COMMANDES_PRETES | +| **Description** | Lecture des commandes de statut `ready`. Affichage du numero de commande, contenu, source. | +| **Entites MCD** | R : `commande` (statut=`ready`), `ligne_commande` | +| **Resultat** | Liste des commandes pretes affichee | + +--- + +### 6.2 Declarer une commande livree + +| Champ | Valeur | +|-------|--------| +| **Evenements declencheurs** | 1. La commande est au statut `ready` ET 2. L'equipier accueil clique sur "Livree" | +| **Acteur** | ACCUEIL | +| **Synchronisation** | ET | +| **Condition** | La commande est au statut `ready`. L'acteur possede la permission `commande.update`. | +| **Operation** | DECLARER_LIVREE | +| **Description** | Transition de statut `ready` -> `delivered`. Fin du cycle de vie de la commande. La commande passe en historique. | +| **Entites MCD** | W : `commande` (UPDATE statut `ready` -> `delivered`), `commande_event` (INSERT event `DELIVERED` user_id=acteur) | +| **Resultat** | Commande au statut `delivered`, cycle de vie termine | + +--- + +## 7. Domaine 5 - Annulation + +### 7.1 Annuler une commande + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | Un acteur autorise demande l'annulation d'une commande | +| **Acteur** | ACCUEIL ou ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | La commande est dans un statut annulable : `pending_payment`, `paid`, `preparing` ou `ready`. Seuls les statuts finaux `delivered` et `cancelled` ne peuvent pas transitionner vers `cancelled` : une commande reste annulable tant qu'elle n'a pas ete remise au client (modification, annulation ou remboursement). L'acteur possede la permission `commande.cancel`. | +| **Operation** | ANNULER_COMMANDE | +| **Description** | Transition du statut courant vers `cancelled`. Mise a jour de `updated_at`. La commande reste en base pour l'historique et les stats (pas de suppression physique). | +| **Entites MCD** | W : `commande` (UPDATE statut -> `cancelled`), `commande_event` (INSERT event `CANCELLED` user_id=acteur, `payload` peut contenir la raison) | +| **Resultat** | Commande au statut `cancelled`, visible dans l'historique admin | + +--- + +## 8. Domaine 6 - Gestion du catalogue (admin) + +### 8.1 Creer un produit + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin soumet le formulaire de creation de produit | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `produit.create`. La categorie ciblee existe et est active. Le libelle est non vide. Le prix est strictement positif. | +| **Operation** | CREER_PRODUIT | +| **Description** | Insertion d'un nouveau produit en base avec sa categorie, son libelle, son prix en centimes, son image (upload optionnel). `est_disponible` a `1` par defaut. | +| **Entites MCD** | R : `categorie` (validation FK) - W : `produit` (INSERT) | +| **Resultat** | Produit cree, retour a la liste des produits | + +--- + +### 8.2 Modifier un produit + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin soumet le formulaire de modification d'un produit existant | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `produit.update`. Le produit existe. Les nouvelles valeurs respectent les contraintes (prix > 0, libelle non vide). | +| **Operation** | MODIFIER_PRODUIT | +| **Description** | Mise a jour des colonnes modifiables (`libelle`, `description`, `prix_ttc_cents`, `image_path`, `est_disponible`, `ordre`, `categorie_id`). Les snapshots deja stockes dans `ligne_commande` ne sont pas affectes (integrite historique garantie par le design). | +| **Entites MCD** | W : `produit` (UPDATE) | +| **Resultat** | Produit mis a jour, liste produits rafraichie | + +--- + +### 8.3 Supprimer un produit + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin confirme la suppression d'un produit | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `produit.delete`. Le produit n'est pas compose dans un menu actif (FK `menu_produit.produit_id` avec ON DELETE RESTRICT). Verification prealable requise. | +| **Operation** | SUPPRIMER_PRODUIT | +| **Description** | Suppression physique du produit si aucune contrainte FK ne bloque. Si le produit est reference dans un menu, la suppression est bloquee (RESTRICT en base). La consequence metier est que l'admin doit d'abord retirer le produit de tous les menus qui le contiennent. | +| **Entites MCD** | W : `produit` (DELETE - bloque si reference dans `menu_produit`) | +| **Resultat** | Produit supprime OU erreur "produit utilise dans un menu" | + +--- + +### 8.4 Creer un menu + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin soumet le formulaire de creation de menu avec sa composition | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `menu.create`. Le libelle est non vide. Le prix est strictement positif. Au moins un produit de role `burger` est associe (contrainte metier : un menu sans burger n'a pas de sens). | +| **Operation** | CREER_MENU | +| **Description** | Insertion du menu (`menu`) puis insertion des lignes de composition (`menu_produit`) : pour chaque produit selectionne, un enregistrement avec son role (burger, accompagnement, boisson, sauce) et sa position. | +| **Entites MCD** | R : `produit` (validation des composants), `categorie` - W : `menu` (INSERT), `menu_produit` (INSERT N lignes) | +| **Resultat** | Menu cree avec sa composition, visible sur la borne | + +--- + +### 8.5 Modifier un menu + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin soumet le formulaire de modification d'un menu existant | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `menu.update`. Le menu existe. La composition modifiee conserve au moins un produit de role `burger`. | +| **Operation** | MODIFIER_MENU | +| **Description** | Mise a jour des colonnes du menu. Si la composition est modifiee : suppression de toutes les lignes `menu_produit` pour ce menu puis reinsertion (pattern delete-and-reinsert, plus simple que le diff ligne a ligne). Les snapshots deja commandes ne sont pas affectes. | +| **Entites MCD** | W : `menu` (UPDATE), `menu_produit` (DELETE + INSERT) | +| **Resultat** | Menu mis a jour | + +--- + +### 8.6 Supprimer un menu + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin confirme la suppression d'un menu | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `menu.delete`. La suppression d'un menu ne bloque pas les `ligne_commande` historiques (FK avec ON DELETE RESTRICT sur `ligne_commande.menu_id`). Verification prealable requise. | +| **Operation** | SUPPRIMER_MENU | +| **Description** | Suppression en cascade des lignes `menu_produit` (ON DELETE CASCADE), puis suppression du menu si aucune `ligne_commande` historique ne le reference. | +| **Entites MCD** | W : `menu_produit` (DELETE CASCADE), `menu` (DELETE - bloque si reference dans `ligne_commande`) | +| **Resultat** | Menu supprime OU erreur "menu present dans des commandes historiques" | + +--- + +### 8.7 Gerer les categories + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin cree, modifie ou desactive une categorie | +| **Acteur** | ADMIN | +| **Synchronisation** | OU (create, update, desactivation) | +| **Condition** | L'acteur possede la permission `categorie.manage`. Pour une desactivation : les produits et menus de la categorie sont desactives en cascade applicative (pas de FK CASCADE ici, logique PHP). | +| **Operation** | GERER_CATEGORIE | +| **Description** | CRUD sur l'entite `categorie`. La desactivation d'une categorie (`est_actif=0`) masque ses produits de la borne sans suppression physique. La suppression physique est bloquee si des produits ou menus y sont rattaches (ON DELETE RESTRICT). | +| **Entites MCD** | W : `categorie` (INSERT / UPDATE / DELETE conditionnel) | +| **Resultat** | Categorie creee / mise a jour / desactivee | + +--- + +## 9. Domaine 7 - Gestion des utilisateurs et roles (admin) + +### 9.1 Creer un utilisateur + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin soumet le formulaire de creation d'utilisateur | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `user.create`. L'email n'existe pas deja en base. Un role valide est selectionne. | +| **Operation** | CREER_USER | +| **Description** | Insertion de l'utilisateur avec hash du mot de passe (argon2id). L'email est unique. Le `role_id` est obligatoire (FK NOT NULL). | +| **Entites MCD** | R : `role` (validation FK) - W : `user` (INSERT) | +| **Resultat** | Utilisateur cree, peut se connecter au back-office | + +--- + +### 9.2 Modifier un utilisateur + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin soumet le formulaire de modification | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `user.update`. L'utilisateur existe. Si le mot de passe est fourni, il est rehache. | +| **Operation** | MODIFIER_USER | +| **Description** | Mise a jour des champs modifiables (`nom`, `prenom`, `email`, `role_id`, `est_actif`). Si un nouveau mot de passe est saisi, il remplace le hash existant. | +| **Entites MCD** | W : `user` (UPDATE) | +| **Resultat** | Utilisateur mis a jour | + +--- + +### 9.3 Desactiver un utilisateur + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin clique sur "Desactiver" pour un utilisateur | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `user.update`. L'admin ne peut pas se desactiver lui-meme (protection applicative). | +| **Operation** | DESACTIVER_USER | +| **Description** | Mise a jour de `est_actif=0`. La session active de l'utilisateur est invalidee au prochain acces (verification `est_actif` dans le middleware d'authentification). L'utilisateur n'est pas supprime, son historique reste tracable. | +| **Entites MCD** | W : `user` (UPDATE est_actif=0) | +| **Resultat** | Utilisateur desactive, acces back-office bloque | + +--- + +### 9.4 Gerer la matrice role-permissions + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'admin modifie l'assignation des permissions pour un role | +| **Acteur** | ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'acteur possede la permission `role.manage`. Les permissions selectionnees existent en base. | +| **Operation** | GERER_MATRICE_RBAC | +| **Description** | Mise a jour de la table `role_permission` pour un role donne : suppression des anciennes assignations et insertion des nouvelles (pattern delete-and-reinsert). Les permissions elles-memes sont statiques (declarees en migration, non modifiables via UI). | +| **Entites MCD** | R : `role`, `permission` - W : `role_permission` (DELETE + INSERT) | +| **Resultat** | Matrice RBAC mise a jour, prise en effet au prochain acces des utilisateurs portant ce role | + +--- + +## 10. Domaine 8 - Authentification back-office + +### 10.1 Se connecter au back-office + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | Un acteur soumet le formulaire de connexion | +| **Acteur** | ACCUEIL / CUISINE / ADMIN | +| **Synchronisation** | Aucune | +| **Condition** | L'email existe en base. Le mot de passe correspond au hash argon2id. L'utilisateur est actif (`est_actif=1`). | +| **Operation** | AUTHENTIFIER_USER | +| **Description** | Verification des identifiants. Si valides : regeneration de l'identifiant de session (protection contre la fixation de session), stockage du `user_id` et du `role_id` en session, mise a jour de `last_login_at`. Idle timeout : 4h. Absolute timeout : 10h. | +| **Entites MCD** | R : `user` (verification), `role` (chargement permissions) - W : `user` (UPDATE last_login_at) | +| **Resultat** | Session ouverte, redirection vers la vue correspondant au role | + +--- + +### 10.2 Se deconnecter du back-office + +| Champ | Valeur | +|-------|--------| +| **Evenement declencheur** | L'acteur clique sur "Deconnexion" ou la session expire | +| **Acteur** | ACCUEIL / CUISINE / ADMIN / SYS (expiration) | +| **Synchronisation** | OU | +| **Condition** | Une session valide est ouverte | +| **Operation** | DECONNECTER_USER | +| **Description** | Destruction de la session PHP (`session_destroy()`). La session est supprimee cote serveur. Le cookie de session est invalide. | +| **Entites MCD** | Aucune ecriture en base (la gestion de session est en PHP natif, hors BDD pour MVP) | +| **Resultat** | Session detruite, redirection vers la page de connexion | + +--- + +## 11. Machine a etats de commande.statut + +Synthese des transitions couvertes par les operations du MCT. + +``` + [CLIENT / ACCUEIL] + PASSER_COMMANDE + SAISIR_COMMANDE_MANUELLE + | + v + [ pending_payment ] (commande composee, paiement en attente) + | + [CLIENT / ACCUEIL] paiement confirme + (atomique dans PASSER_COMMANDE / SAISIR_COMMANDE_MANUELLE) + | + v + [ paid ] + | + [CUISINE] MARQUER_EN_PREPARATION + | + v + [ preparing ] + | + [CUISINE] MARQUER_PRETE + | + v + [ ready ] + | + [ACCUEIL] DECLARER_LIVREE + | + v + [ delivered ] (terminal, non annulable) + + + Depuis pending_payment / paid / preparing / ready : + [ACCUEIL ou ADMIN] ANNULER_COMMANDE + | + v + [ cancelled ] (terminal) +``` + +**Note sur la transition `pending_payment -> paid`** : dans le cadre RNCP, le paiement est +remplace par la saisie du numero de commande par le client (borne) ou par la validation de +l'equipier (comptoir/drive). La transition est atomique au sein des operations PASSER_COMMANDE +et SAISIR_COMMANDE_MANUELLE. Le statut `pending_payment` est visible en base le temps de la +transaction, et le statut final stocke est `paid`. Ce decoupage en deux etats reflete la +semantique metier (le client compose SA commande, PUIS il paie) et preserve la capacite +d'evolution vers un paiement reel sans migration destructive. + +--- + +## 12. Tableau de synthese des operations + +| # | Operation | Domaine | Acteur | Entites W | Entites R | +|---|-----------|---------|--------|-----------|-----------| +| 1 | CHARGER_CATALOGUE | Commande kiosk | CLIENT | - | categorie, produit, menu, menu_produit | +| 2 | COMPOSER_PANIER | Commande kiosk | CLIENT | - (volatile) | produit, menu, menu_produit | +| 3 | PASSER_COMMANDE | Commande kiosk | CLIENT | commande, ligne_commande, commande_event | produit, menu | +| 4 | AFFICHER_CONFIRMATION | Commande kiosk | SYS | - | - | +| 5 | SAISIR_COMMANDE_MANUELLE | Commande comptoir/drive | ACCUEIL | commande, ligne_commande, commande_event | produit, menu, menu_produit | +| 6 | LISTER_COMMANDES_A_PREPARER | Preparation | CUISINE | - | commande, ligne_commande | +| 7 | MARQUER_EN_PREPARATION | Preparation | CUISINE | commande, commande_event | - | +| 8 | MARQUER_PRETE | Preparation | CUISINE | commande, commande_event | - | +| 9 | LISTER_COMMANDES_PRETES | Remise client | ACCUEIL | - | commande, ligne_commande | +| 10 | DECLARER_LIVREE | Remise client | ACCUEIL | commande, commande_event | - | +| 11 | ANNULER_COMMANDE | Annulation | ACCUEIL / ADMIN | commande, commande_event | - | +| 12 | CREER_PRODUIT | Catalogue | ADMIN | produit | categorie | +| 13 | MODIFIER_PRODUIT | Catalogue | ADMIN | produit | - | +| 14 | SUPPRIMER_PRODUIT | Catalogue | ADMIN | produit | menu_produit | +| 15 | CREER_MENU | Catalogue | ADMIN | menu, menu_produit | produit, categorie | +| 16 | MODIFIER_MENU | Catalogue | ADMIN | menu, menu_produit | - | +| 17 | SUPPRIMER_MENU | Catalogue | ADMIN | menu_produit, menu | ligne_commande | +| 18 | GERER_CATEGORIE | Catalogue | ADMIN | categorie | produit, menu | +| 19 | CREER_USER | RBAC | ADMIN | user | role | +| 20 | MODIFIER_USER | RBAC | ADMIN | user | - | +| 21 | DESACTIVER_USER | RBAC | ADMIN | user | - | +| 22 | GERER_MATRICE_RBAC | RBAC | ADMIN | role_permission | role, permission | +| 23 | AUTHENTIFIER_USER | Auth | ALL BACK | user | user, role | +| 24 | DECONNECTER_USER | Auth | ALL BACK | - | - | + +**Total : 24 operations** couvrant la totalite du cycle de vie metier Wakdo. + +--- + +## 13. Cross-validation MCT -> MCD (mantra #34) + +Verification que chaque entite du MCD participe a au moins une operation du MCT. + +| Entite MCD | Operations qui la lisent | Operations qui l'ecrivent | Couverture | +|------------|--------------------------|--------------------------|------------| +| `categorie` | 1, 12, 15, 18 | 18 | OK | +| `produit` | 1, 2, 3, 5, 12, 14 | 12, 13, 14 | OK | +| `menu` | 1, 2, 3, 5, 15, 17 | 15, 16, 17 | OK | +| `menu_produit` | 1, 2, 5, 14 | 15, 16, 17 | OK | +| `commande` | 6, 9 | 3, 5, 7, 8, 10, 11 | OK | +| `ligne_commande` | 6, 9, 17 | 3, 5 | OK | +| `commande_event` | - (lecture via SELECT historique non listee comme operation) | 3, 5, 7, 8, 10, 11 | OK | +| `user` | 23 | 19, 20, 21, 23 | OK | +| `role` | 19, 22, 23 | 22 | OK | +| `permission` | 22 | - (statique, migration) | OK (*) | +| `role_permission` | - | 22 | OK | + +(*) `permission` est en lecture seule via les operations MCT : ses valeurs sont declarees en +migration SQL et ne sont pas modifiables via UI (RBAC statique cote permissions, dynamique +cote roles). Cette decision est documentee dans le MCD section 4.3. + +**Conclusion** : 11/11 entites couvertes. Coherence MCT <-> MCD validee. + +--- + +## 14. Points d'incoherence detectes et signalement + +Les points suivants necessite une attention ou une decision de l'auteur : + +### 14.1 Divergence `commande.statut` entre dictionnaire et PROJECT_CONTEXT - RESOLUE + +- **Machine canonique retenue** : `pending_payment -> paid -> preparing -> ready -> delivered` (transitions nominales) ; `cancelled` atteignable depuis tout etat non remis (`pending_payment`, `paid`, `preparing`, `ready`), pour couvrir modification, annulation et remboursement client. +- **Arbitrage** : la regle metier confirmee impose deux phases successives : le client compose sa commande (statut `pending_payment`), puis il paie (statut `paid`). PROJECT_CONTEXT utilisait un terme `pending` simplifie qui ne refletait pas cette distinction. La machine canonique du dictionnaire est la source de verite. La transition `pending_payment -> paid` est atomique dans les operations PASSER_COMMANDE et SAISIR_COMMANDE_MANUELLE dans le cadre RNCP (substitut de paiement = saisie du numero). Ce point est considere comme clos. + +### 14.2 Absence d'acteur `user` lie a `commande` - RESOLUE (2026-05-28) + +**Decision actee** : pas de colonne `user_id` directe sur `commande`, mais une table d'audit dediee `commande_event` (cf. dictionnaire 3.7, MCD 4.2.bis). Pattern event sourcing simplifie. Chaque operation qui modifie `commande.statut` insere une ligne dans `commande_event` avec l'utilisateur a l'origine de la transition (NULL si auto-validation kiosk). Tracabilite complete sans denormalisation lourde sur `commande`. + +### 14.3 Colonne `source` absente de `commande` dans le dictionnaire - RESOLUE (2026-05-28) + +**Decision actee** : ajout d'une colonne `source ENUM('kiosk','comptoir','drive')` sur `commande`, en plus de `mode_consommation`. Les deux dimensions sont **distinctes** : +- `source` = canal de saisie (kiosk / comptoir / drive) - input +- `mode_consommation` = mode de consommation fiscal (sur_place / a_emporter / drive) - output + +Contrainte croisee : `source = drive` implique `mode_consommation = drive` (verifiee au MLT lors de la creation de commande). Pour `kiosk` et `comptoir`, les deux dimensions sont independantes. Documente dans le dictionnaire notes 8 et 9. + +### 14.4 Stats et `service_day` + +PROJECT_CONTEXT documente une logique `service_day` (section 2). Le MCT ne couvre pas +l'agregation des stats (cron 04h30). Ce traitement est volontairement hors scope MCT (c'est +un traitement technique automatise, pas un traitement metier interactif). Il sera documente +dans le MLT (section cron). diff --git a/docs/merise/mld.md b/docs/merise/mld.md new file mode 100644 index 0000000..8e49cd4 --- /dev/null +++ b/docs/merise/mld.md @@ -0,0 +1,525 @@ +# Modele Logique des Donnees (MLD) - Wakdo + +**Phase Merise** : P1 - Conception, etape 5 (apres MCD, MCT, MLT) +**Statut** : v0.1 +**Date** : 2026-05-28 +**Branche** : `feat/p1-conception` +**Auteur methodologie** : BYAN + +--- + +## 1. Objet du document + +Le MLD transcrit le MCD en schema relationnel formel : 1 entite -> 1 table, chaque association traduite selon sa cardinalite, contraintes referentielles materialisees, index dimensionnes pour les acces frequents. + +C'est l'etape qui transforme la modelisation conceptuelle en specification implementable. Le DDL SQL (`db/migrations/0001_init_schema.sql`) sera derive directement de ce document a P2. + +**Source** : `dictionary.md` (types et contraintes par attribut), `mcd.md` (entites + cardinalites + decisions reportees), `mct.md` (operations + entites manipulees), `mlt.md` (regles de gestion + transitions + protection concurrence). + +**Cibles** : +- MariaDB 11.4 LTS (cf. `docker-compose.yml` service `wakdo-db`) +- Engine InnoDB (ACID, FKs, row-level locking, CHECK depuis 10.2.1) +- Charset `utf8mb4` collation `utf8mb4_unicode_ci` + +--- + +## 2. Conventions de notation + +### Notation relationnelle + +``` +TABLE_NAME (col1, col2, #col_fk, [col_optionnelle]) + + PK : col1 + UK : col2 + FK : col_fk -> AUTRE_TABLE(id) +``` + +| Symbole | Signification | +|---|---| +| `col` | Colonne NOT NULL | +| `[col]` | Colonne nullable | +| `#col` | Colonne FK (sans le diese : non-FK) | + +Cette notation reste proche de l'usage Merise francais (UNIRIS, ouvrages Nanci/Espinasse) : la cle primaire est soulignee dans les documents classiques, ici on prefixe par `PK` pour la portabilite ASCII. + +### Types + +Les types SQL exacts sont definis dans `dictionary.md` section 2 (Conventions generales) et reprecises dans chaque section de cette MLD. Conventions retenues : + +- `INT UNSIGNED AUTO_INCREMENT` pour toutes les PK techniques +- `INT UNSIGNED` pour tous les montants en centimes (anti-FLOAT cf. dictionary note 1) +- `VARCHAR()` avec longueur calibree selon dictionary note 7 +- `ENUM(...)` pour les valeurs metier stables (cf. dictionary note 2) +- `DATETIME` pour les timestamps (pas TIMESTAMP qui ferait du fuseau auto-implicite) + +--- + +## 3. Regles de traduction MCD -> MLD + +Les regles classiques de passage MCD -> MLD appliquees : + +### 3.1 Entite -> Table + +Chaque entite du MCD devient une table. L'identifiant conceptuel `id` devient PK technique `INT UNSIGNED AUTO_INCREMENT`. Les attributs gardent leurs noms et types. + +### 3.2 Association `(1,1) - (1,N)` -> FK simple + +L'entite cote `(1,1)` porte la FK vers l'entite cote `(0,N)` ou `(1,N)`. Exemple : + +``` +CATEGORIE (1,1) <--regroupe--> (0,N) PRODUIT + +devient + +CATEGORIE (id, libelle, ...) -- pas de FK +PRODUIT (id, #categorie_id, ...) -- FK vers CATEGORIE +``` + +### 3.3 Association `(0,N) - (0,N)` ou `(1,N) - (1,N)` -> Table de jointure + +L'association devient sa propre table avec PK composite des deux FKs. Exemple : + +``` +MENU (1,N) <--compose--> (0,N) PRODUIT (via MENU_PRODUIT) + +devient + +MENU_PRODUIT (#menu_id, #produit_id, role, position) + PK composite : (menu_id, produit_id) +``` + +### 3.4 Association porteuse d'attributs -> Table associative + +Si une association MCD porte des attributs propres (`role`, `position` sur `compose`), elle devient table meme si elle pourrait theoriquement etre une FK. Cas applique a `MENU_PRODUIT` et `ROLE_PERMISSION`. + +### 3.5 Polymorphisme -> 2 FKs nullables + discriminateur + +`LIGNE_COMMANDE` -> (`PRODUIT` ou `MENU`) traduit en 2 colonnes FK nullable + 1 colonne discriminateur : + +``` +LIGNE_COMMANDE (id, #commande_id, type_item, [#produit_id], [#menu_id], ...) + CHECK ((type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL) + OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL)) +``` + +Cf. `docs/notes/polymorphic-fk-snapshots.md` pour la justification. + +### 3.6 Audit (event sourcing) -> Table dediee + +`COMMANDE_EVENT` est une table append-only, traduction directe de l'entite MCD 3.7. Aucune denormalisation `user_id` sur `commande` (cf. dictionary note 10). + +--- + +## 4. Schema relationnel formel + +Les 11 tables qui composent le schema Wakdo, ordonnees par dependance (les tables sans FK d'abord, puis les tables qui dependent d'elles). + +### 4.1 `categorie` + +``` +categorie (id, libelle, slug, image_path, ordre, est_actif, created_at, updated_at) + + PK : id + UK : libelle + UK : slug +``` + +Types : +- `id INT UNSIGNED AUTO_INCREMENT` +- `libelle VARCHAR(80) NOT NULL` +- `slug VARCHAR(60) NOT NULL` +- `image_path VARCHAR(255) NULL` (cf. dictionary note 11) +- `ordre SMALLINT UNSIGNED NOT NULL DEFAULT 0` +- `est_actif TINYINT(1) NOT NULL DEFAULT 1` +- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` +- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` + +Aucune FK. Table racine du sous-domaine Catalogue. + +### 4.2 `produit` + +``` +produit (id, #categorie_id, libelle, [description], prix_ttc_cents, [image_path], + est_disponible, ordre, created_at, updated_at) + + PK : id + FK : categorie_id -> categorie(id) ON DELETE RESTRICT + IDX : (categorie_id, est_disponible, ordre) +``` + +Types : +- `id INT UNSIGNED AUTO_INCREMENT` +- `categorie_id INT UNSIGNED NOT NULL` +- `libelle VARCHAR(120) NOT NULL` +- `description TEXT NULL` +- `prix_ttc_cents INT UNSIGNED NOT NULL CHECK (prix_ttc_cents > 0)` +- `image_path VARCHAR(255) NULL` +- `est_disponible TINYINT(1) NOT NULL DEFAULT 1` +- `ordre SMALLINT UNSIGNED NOT NULL DEFAULT 0` +- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` +- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` + +**ON DELETE RESTRICT** sur `categorie_id` : impossible de supprimer une categorie qui contient des produits (protection metier, evite les orphelins). + +### 4.3 `menu` + +``` +menu (id, #categorie_id, libelle, [description], prix_ttc_cents, [image_path], + est_disponible, ordre, created_at, updated_at) + + PK : id + FK : categorie_id -> categorie(id) ON DELETE RESTRICT + IDX : (categorie_id, est_disponible, ordre) +``` + +Types : identiques a `produit` (meme structure, semantique distincte cf. dictionary note 3). + +### 4.4 `menu_produit` (table associative) + +``` +menu_produit (#menu_id, #produit_id, role, position) + + PK : (menu_id, produit_id) + FK : menu_id -> menu(id) ON DELETE CASCADE + FK : produit_id -> produit(id) ON DELETE RESTRICT + IDX : (menu_id, position) +``` + +Types : +- `menu_id INT UNSIGNED NOT NULL` +- `produit_id INT UNSIGNED NOT NULL` +- `role ENUM('burger','accompagnement','boisson','sauce','dessert') NOT NULL` +- `position SMALLINT UNSIGNED NOT NULL DEFAULT 0` + +**ON DELETE CASCADE** sur `menu_id` : si un menu est supprime, ses compositions le sont aussi. +**ON DELETE RESTRICT** sur `produit_id` : impossible de supprimer un produit utilise dans un menu (protection integrite menu). + +Pas d'`updated_at` (table de jointure, cf. dictionary note 5 : les jointures sont supprimees+recreees, pas modifiees). + +### 4.5 `commande` + +``` +commande (id, numero, source, mode_consommation, statut, + total_ht_cents, total_tva_cents, total_ttc_cents, tva_taux_pourmille, + [paye_a], created_at, updated_at) + + PK : id + UK : numero + IDX : (source, created_at) + IDX : (statut, created_at) + IDX : created_at + CHECK : source != 'drive' OR mode_consommation = 'drive' + CHECK : total_ttc_cents = total_ht_cents + total_tva_cents +``` + +Types : +- `id INT UNSIGNED AUTO_INCREMENT` +- `numero VARCHAR(20) NOT NULL` +- `source ENUM('kiosk','comptoir','drive') NOT NULL` +- `mode_consommation ENUM('sur_place','a_emporter','drive') NOT NULL` +- `statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') NOT NULL DEFAULT 'pending_payment'` +- `total_ht_cents INT UNSIGNED NOT NULL CHECK (total_ht_cents >= 0)` +- `total_tva_cents INT UNSIGNED NOT NULL CHECK (total_tva_cents >= 0)` +- `total_ttc_cents INT UNSIGNED NOT NULL CHECK (total_ttc_cents > 0)` +- `tva_taux_pourmille SMALLINT UNSIGNED NOT NULL` +- `paye_a DATETIME NULL` (NULL avant paiement, timestamp du passage en `paid`) +- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` +- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` + +**CHECK croise** `source/mode_consommation` (cf. dictionary note 8) : empeche les combinaisons invalides au niveau SGBD plutot que de se reposer uniquement sur le code applicatif. + +**CHECK montants** : invariant `TTC = HT + TVA` verifie en base (defense-in-depth contre les bugs de calcul applicatif). + +Aucune FK directe vers `user` : la tracabilite passe par `commande_event` (cf. 4.7). + +### 4.6 `ligne_commande` + +``` +ligne_commande (id, #commande_id, type_item, [#produit_id], [#menu_id], + libelle_snapshot, prix_unitaire_ttc_cents_snapshot, quantite, created_at) + + PK : id + FK : commande_id -> commande(id) ON DELETE CASCADE + FK : produit_id -> produit(id) ON DELETE RESTRICT + FK : menu_id -> menu(id) ON DELETE RESTRICT + IDX : commande_id + CHECK : (type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL) + OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL) +``` + +Types : +- `id INT UNSIGNED AUTO_INCREMENT` +- `commande_id INT UNSIGNED NOT NULL` +- `type_item ENUM('produit','menu') NOT NULL` +- `produit_id INT UNSIGNED NULL` +- `menu_id INT UNSIGNED NULL` +- `libelle_snapshot VARCHAR(120) NOT NULL` +- `prix_unitaire_ttc_cents_snapshot INT UNSIGNED NOT NULL CHECK (prix_unitaire_ttc_cents_snapshot > 0)` +- `quantite SMALLINT UNSIGNED NOT NULL DEFAULT 1 CHECK (quantite > 0)` +- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` + +**ON DELETE CASCADE** sur `commande_id` : si la commande disparait, ses lignes aussi. +**ON DELETE RESTRICT** sur `produit_id` et `menu_id` : impossible de supprimer un produit/menu reference par une commande historique (preserve les references meme si on snapshote). + +**CHECK polymorphisme** : exclusivite mutuelle `produit_id` / `menu_id` selon `type_item` (cf. dictionary note 6). + +### 4.7 `commande_event` + +``` +commande_event (id, #commande_id, event_type, [from_statut], to_statut, + [#user_id], [payload], created_at) + + PK : id + FK : commande_id -> commande(id) ON DELETE CASCADE + FK : user_id -> user(id) ON DELETE SET NULL + IDX : (commande_id, created_at) + IDX : (user_id, created_at) + IDX : (event_type, created_at) +``` + +Types : +- `id INT UNSIGNED AUTO_INCREMENT` +- `commande_id INT UNSIGNED NOT NULL` +- `event_type ENUM('CREATED','PAID','PREPARING_STARTED','READY','DELIVERED','CANCELLED') NOT NULL` +- `from_statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') NULL` +- `to_statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') NOT NULL` +- `user_id INT UNSIGNED NULL` +- `payload JSON NULL` +- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` + +**ON DELETE CASCADE** sur `commande_id` : si la commande est purgee, son journal disparait avec elle. +**ON DELETE SET NULL** sur `user_id` : si un equipier est supprime, les events restent (l'audit reste consultable, l'attribution individuelle est perdue). + +**Pas d'`updated_at`** : table append-only. Aucun UPDATE applicatif autorise (cf. mlt.md RG-T10). + +**Pas de CHECK croise from_statut/to_statut** : la verification de la machine a etats est applicative (mlt section 12), un CHECK SQL serait trop rigide (event_type peut prendre des valeurs non encore prevues). + +### 4.8 `user` + +``` +user (id, email, password_hash, nom, prenom, #role_id, est_actif, [last_login_at], + created_at, updated_at) + + PK : id + UK : email + FK : role_id -> role(id) ON DELETE RESTRICT + IDX : (est_actif, role_id) +``` + +Types : +- `id INT UNSIGNED AUTO_INCREMENT` +- `email VARCHAR(254) NOT NULL` (RFC 5321) +- `password_hash VARCHAR(255) NOT NULL` (argon2id, cf. `.env` `PASSWORD_ALGO`) +- `nom VARCHAR(60) NOT NULL` +- `prenom VARCHAR(60) NOT NULL` +- `role_id INT UNSIGNED NOT NULL` +- `est_actif TINYINT(1) NOT NULL DEFAULT 1` +- `last_login_at DATETIME NULL` +- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` +- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` + +**ON DELETE RESTRICT** sur `role_id` : impossible de supprimer un role qui a encore des users (passer par `est_actif = 0` sur le role avant de supprimer). + +### 4.9 `role` + +``` +role (id, code, libelle, [description], est_actif, created_at, updated_at) + + PK : id + UK : code +``` + +Types : +- `id INT UNSIGNED AUTO_INCREMENT` +- `code VARCHAR(40) NOT NULL` +- `libelle VARCHAR(80) NOT NULL` +- `description TEXT NULL` +- `est_actif TINYINT(1) NOT NULL DEFAULT 1` +- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` +- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` + +Aucune FK. Table racine du sous-domaine RBAC. + +### 4.10 `permission` + +``` +permission (id, code, libelle, [description], created_at) + + PK : id + UK : code +``` + +Types : +- `id INT UNSIGNED AUTO_INCREMENT` +- `code VARCHAR(60) NOT NULL` (format `.`) +- `libelle VARCHAR(120) NOT NULL` +- `description TEXT NULL` +- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` + +Pas d'`updated_at` : les permissions sont declarees en migration et ne sont pas modifiees via UI (cf. RBAC statique cote permissions, dictionary 3.10 et MCD 4.3). + +### 4.11 `role_permission` (table associative) + +``` +role_permission (#role_id, #permission_id) + + PK : (role_id, permission_id) + FK : role_id -> role(id) ON DELETE CASCADE + FK : permission_id -> permission(id) ON DELETE CASCADE + IDX : permission_id (acces inverse "quels roles ont cette permission ?") +``` + +Types : +- `role_id INT UNSIGNED NOT NULL` +- `permission_id INT UNSIGNED NOT NULL` + +**ON DELETE CASCADE des deux cotes** : suppression d'un role ou d'une permission supprime ses mappings. + +Pas de timestamps (table de jointure pure, cf. dictionary note 5). + +--- + +## 5. Recapitulatif des contraintes referentielles + +| FK | Reference | ON DELETE | Justification | +|---|---|---|---| +| `produit.categorie_id` | `categorie(id)` | RESTRICT | Pas d'orphelin produit | +| `menu.categorie_id` | `categorie(id)` | RESTRICT | Idem | +| `menu_produit.menu_id` | `menu(id)` | CASCADE | Composition disparait avec le menu | +| `menu_produit.produit_id` | `produit(id)` | RESTRICT | Pas de cascade : un produit reference dans un menu ne peut pas etre supprime sans amender la composition | +| `commande.--` | (aucune FK vers user) | - | Tracabilite via commande_event | +| `ligne_commande.commande_id` | `commande(id)` | CASCADE | Lignes disparaissent avec la commande | +| `ligne_commande.produit_id` | `produit(id)` | RESTRICT | Preserve l'integrite historique | +| `ligne_commande.menu_id` | `menu(id)` | RESTRICT | Idem | +| `commande_event.commande_id` | `commande(id)` | CASCADE | Journal disparait avec la commande | +| `commande_event.user_id` | `user(id)` | SET NULL | Audit conserve, attribution individuelle perdue | +| `user.role_id` | `role(id)` | RESTRICT | Pas d'user sans role | +| `role_permission.role_id` | `role(id)` | CASCADE | Mapping disparait avec le role | +| `role_permission.permission_id` | `permission(id)` | CASCADE | Mapping disparait avec la permission | + +**Cles** : +- **CASCADE** : la donnee dependante n'a pas de sens hors de son parent (lignes / events / mappings) +- **RESTRICT** : suppression du parent bloquee tant que des references existent (catalogue, role) +- **SET NULL** : preserve la donnee enfant en perdant le lien (audit event sans attribution) + +--- + +## 6. Index complementaires + +Au-dela des PK / UK / FK qui creent des index automatiquement, indexes ajoutes pour les requetes frequentes identifiees au MCT/MLT : + +| Table | Index | Justification (operation MCT) | +|---|---|---| +| `produit` | `(categorie_id, est_disponible, ordre)` | Chargement catalogue kiosk (op 1) : filtre par categorie + disponible + tri par ordre | +| `menu` | `(categorie_id, est_disponible, ordre)` | Idem produit | +| `menu_produit` | `(menu_id, position)` | Chargement composition d'un menu | +| `commande` | `(source, created_at)` | Stats "par canal" + tri chronologique | +| `commande` | `(statut, created_at)` | Files d'attente preparation/accueil (ops 6, 9) | +| `commande` | `created_at` | Stats agregations live | +| `ligne_commande` | `commande_id` | Recuperation des lignes d'une commande | +| `commande_event` | `(commande_id, created_at)` | Historique d'une commande | +| `commande_event` | `(user_id, created_at)` | Actions d'un equipier sur une periode | +| `commande_event` | `(event_type, created_at)` | Stats "combien de cancellations cette semaine ?" | +| `user` | `(est_actif, role_id)` | Login + permissions check (op 23) | +| `role_permission` | `permission_id` | Acces inverse "quels roles ont cette permission ?" | + +**Index NON ajoutes** (volontaire) : +- `commande.numero` : UK suffit, pas de range query attendue dessus +- `commande.mode_consommation` : faible cardinalite (3 valeurs), un index n'est pas rentable, full scan acceptable +- `commande.paye_a` : NULL pour la majorite des lignes (commande encore en cours), index peu utile + +--- + +## 7. Contraintes CHECK (MariaDB 10.2+) + +Verification au niveau SGBD pour les invariants critiques. Defense-in-depth contre les bugs applicatifs. + +| Table | CHECK | Pourquoi | +|---|---|---| +| `produit` | `prix_ttc_cents > 0` | Prix nul ou negatif = bug | +| `menu` | `prix_ttc_cents > 0` | Idem | +| `commande` | `total_ht_cents >= 0` | Plancher autorise (commande vide transitoire ?) | +| `commande` | `total_tva_cents >= 0` | Idem | +| `commande` | `total_ttc_cents > 0` | TTC nul = bug | +| `commande` | `total_ttc_cents = total_ht_cents + total_tva_cents` | Invariant arithmetique | +| `commande` | `source != 'drive' OR mode_consommation = 'drive'` | Contrainte croisee (dictionary note 8, mlt RG-T09) | +| `ligne_commande` | `prix_unitaire_ttc_cents_snapshot > 0` | Snapshot prix non nul | +| `ligne_commande` | `quantite > 0` | Quantite non nulle | +| `ligne_commande` | `(type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL) OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL)` | Polymorphisme exclusif (dictionary note 6) | + +--- + +## 8. Cross-validation entites MCD -> tables MLD + +| Entite MCD | Table MLD | Notes | +|---|---|---| +| `categorie` (3.1) | `categorie` (4.1) | 1:1 | +| `produit` (3.2) | `produit` (4.2) | 1:1 | +| `menu` (3.3) | `menu` (4.3) | 1:1 | +| `menu_produit` (3.4) | `menu_produit` (4.4) | Entite associative -> table de jointure avec PK composite | +| `commande` (3.5) | `commande` (4.5) | 1:1, attribut `source` ajoute (decision 2026-05-28) | +| `ligne_commande` (3.6) | `ligne_commande` (4.6) | 1:1, polymorphisme materialise par 2 FKs nullables + CHECK | +| `commande_event` (3.7) | `commande_event` (4.7) | 1:1, table append-only, decision 2026-05-28 | +| `user` (3.8) | `user` (4.8) | 1:1 | +| `role` (3.9) | `role` (4.9) | 1:1 | +| `permission` (3.10) | `permission` (4.10) | 1:1 | +| `role_permission` (3.11) | `role_permission` (4.11) | Entite associative -> table de jointure avec PK composite | + +**Conclusion** : 11/11 entites tracees. Aucune entite MCD ne reste sans table, aucune table MLD ne sort du modele conceptuel. + +--- + +## 9. Estimation volumes et taille + +| Table | Volume 6 mois | Taille moyenne ligne | Taille totale | +|---|---|---|---| +| `categorie` | ~10 | 200 octets | < 1 Ko | +| `produit` | ~70 | 400 octets | ~30 Ko | +| `menu` | ~15 | 400 octets | ~6 Ko | +| `menu_produit` | ~80 | 30 octets | ~2 Ko | +| `commande` | ~30k | 300 octets | ~9 Mo | +| `ligne_commande` | ~150k | 200 octets | ~30 Mo | +| `commande_event` | ~180k | 200 octets | ~36 Mo | +| `user` | ~20 | 500 octets | ~10 Ko | +| `role` | ~5 | 200 octets | ~1 Ko | +| `permission` | ~40 | 300 octets | ~12 Ko | +| `role_permission` | ~80 | 30 octets | ~2 Ko | + +**Total : ~75 Mo sur 6 mois**. Largement gerable par MariaDB sur le conteneur Wakdo (volume `wakdo_db_data` named volume, cf. `docker-compose.yml`). + +Les indexes ajoutent typiquement 30-50% du volume des tables, soit ~30 Mo supplementaires. **Estimation totale : ~100-110 Mo / 6 mois**. + +--- + +## 10. Decisions reportees au DDL et a P2 + +Les decisions suivantes sont laissees au DDL (`db/migrations/0001_init_schema.sql`) ou aux phases ulterieures, parce qu'elles concernent l'implementation et pas la modelisation logique : + +1. **Triggers ou colonnes generees** : `service_day` (cf. PROJECT_CONTEXT section 2) pourrait etre une `GENERATED ALWAYS AS (DATE_SUB(created_at, INTERVAL 4 HOUR + 30 MINUTE))` virtuelle pour eviter le calcul applicatif. A evaluer en P3 si les stats sont penibles. +2. **Partitionnement** : `commande_event` pourrait etre partitionne par mois si le volume depasse les estimations. Pas pour MVP. +3. **Foreign Key index** : MariaDB cree automatiquement un index sur la FK lors de la declaration, sauf si un index utilisable existe deja. A verifier explicitement dans le DDL. +4. **Collation** : `utf8mb4_unicode_ci` retenu (sensible diacritiques et casse pour les recherches). Si besoin de tri locale francais strict, passer en `utf8mb4_fr_0900_ai_ci` (MySQL 8) ou rester en `unicode_ci`. +5. **Engine** : `InnoDB` par defaut (ACID + FKs). Pas de MEMORY ni Archive. +6. **Charset emoji** : `utf8mb4` (4 octets / char max) couvre les emojis au cas ou ils apparaitraient dans `description` produit ou `payload` JSON. + +--- + +## 11. A faire au prochain sprint (DDL + Seed) + +1. **DDL** (`db/migrations/0001_init_schema.sql`) : transcrire ce MLD en CREATE TABLE executables, dans l'ordre des dependances (categorie -> produit/menu -> menu_produit -> commande -> ligne_commande/commande_event ; permission -> role -> role_permission ; user en dernier). + +2. **Seed** (`db/seeds/0001_demo_data.sql`) : INSERT pour : + - 9 categories + 53 produits + 13 menus a partir des JSON sources (`docs/merise/_sources/`), prix normalises en centimes + - 1 admin par defaut + roles (admin, manager, equipier-comptoir, equipier-drive) + - Permissions declarees (CRUD produit/menu/categorie/user/role + commande operationnelles) + - Quelques commandes d'exemple pour les demos + +3. **Export fallback JSON** (`scripts/export-fallback.{sh|php}`) : extrait des donnees seed vers `src/public/borne/data/*.json` pour le mode "Bloc 1 isole" (kiosk sans BDD pour les tests). + +4. **Tests de validation DDL** : verifier que : + - Toutes les CHECK contraintes sont declenchees comme attendu (tests d'integration) + - Les ON DELETE CASCADE / RESTRICT se comportent comme specifie + - Les indexes accelerent reellement les requetes ciblees (EXPLAIN sur les requetes types du MCT) + +5. **Migration tooling** : decider de l'outil (phinx, doctrine migrations, ou homemade PHP script). Cf. PROJECT_CONTEXT pour le choix retenu. diff --git a/docs/merise/mlt.md b/docs/merise/mlt.md new file mode 100644 index 0000000..6ef460c --- /dev/null +++ b/docs/merise/mlt.md @@ -0,0 +1,588 @@ +# Modele Logique des Traitements (MLT) - Wakdo + +**Phase Merise** : P1 - Conception, etape 4 (derive du MCT) +**Statut** : v0.1 +**Date** : 2026-05-21 +**Branche** : `feat/p1-conception` +**Auteur methodologie** : BYAN + +--- + +## 1. Objet du document + +Le MLT (Modele Logique des Traitements) raffine chaque operation du MCT en precisant : +- les **preconditions** (ce qui doit etre vrai avant l'execution) +- les **regles de traitement** (validations, calculs, logique metier) +- les **postconditions** (l'etat garanti apres succes) +- les **sorties** (donnees produites ou evenements emis) + +Il fait le lien entre le MCT (niveau conceptuel) et l'implementation PHP/SQL (niveau physique). +Toutes les references aux entites/attributs sont celles du dictionnaire de donnees +(`docs/merise/dictionary.md`) et du MCD (`docs/merise/mcd.md`). + +**Conventions de ce document** : +- `[PRE]` : precondition - doit etre satisfaite pour que l'operation s'execute +- `[RG]` : regle de gestion - logique metier appliquee pendant l'execution +- `[POST]` : postcondition - etat de la base garanti apres succes +- `[OUT]` : sortie - donnee ou evenement produit +- `[ERR]` : cas d'erreur - sortie alternative si une condition echoue + +--- + +## 2. Domaine 1 - Parcours commande (borne kiosk) + +### 2.1 CHARGER_CATALOGUE + +**Correspond a MCT section 3.1** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | La requete provient d'un client sur la borne (endpoint public, pas d'authentification requise) | +| **[PRE-2]** | La plage horaire courante est comprise dans la fenetre de service (10h00-01h00) ; hors fenetre, la borne affiche un message de fermeture | +| **[RG-1]** | Lecture de toutes les `categorie` avec `est_actif = 1`, ordonnees par `categorie.ordre ASC` | +| **[RG-2]** | Pour chaque categorie, lecture des `produit` avec `est_disponible = 1` et `categorie_id = categorie.id`, ordonnes par `produit.ordre ASC` | +| **[RG-3]** | Lecture de tous les `menu` avec `est_disponible = 1`, avec jointure sur `menu_produit` pour la composition (roles et positions) | +| **[RG-4]** | Les prix sont retournes en centimes (INT) ; la conversion en EUR est effectuee cote front | +| **[POST-1]** | Aucune ecriture en base. L'etat de la base est inchange. | +| **[OUT-1]** | Reponse JSON : `{data: {categories: [...], produits: {...}, menus: [...]}}` | +| **[ERR-1]** | Si la BDD est inaccessible : reponse `{data: null, error: {code: "DB_ERROR", message: "..."}}` et le front bascule sur le JSON fallback statique | + +--- + +### 2.2 COMPOSER_PANIER + +**Correspond a MCT section 3.2** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | Le catalogue a ete charge en memoire front (CHARGER_CATALOGUE effectue) | +| **[PRE-2]** | L'article selectionne (produit ou menu) est present dans le catalogue charge et a `est_disponible = 1` | +| **[RG-1]** | Le panier est une structure en memoire JavaScript (tableau d'items). Aucune persistance BDD a ce stade. | +| **[RG-2]** | Chaque item du panier contient : `type` (`produit` ou `menu`), `item_id`, `libelle`, `prix_unitaire_ttc_cents`, `quantite`, `options` (taille si applicable) | +| **[RG-3]** | Option grande taille : si `type = 'produit'` et le produit appartient aux categories `frites` ou `boissons`, l'option `grande_taille` ajoute 50 centimes au `prix_unitaire_ttc_cents` de cet item | +| **[RG-4]** | Si un item de meme `(type, item_id, options)` existe deja dans le panier, sa quantite est incrementee plutot qu'un nouvel item est ajoute | +| **[RG-5]** | Le total du panier est recalcule apres chaque modification : `SUM(prix_unitaire_ttc_cents * quantite)` sur tous les items | +| **[POST-1]** | Aucune ecriture en base. Etat panier en memoire mis a jour. | +| **[OUT-1]** | Affichage du recapitulatif panier avec le total TTC | +| **[ERR-1]** | Si un produit est passe a `est_disponible = 0` entre le chargement du catalogue et la validation, la verification se produit a l'etape PASSER_COMMANDE (validation serveur) | + +--- + +### 2.3 PASSER_COMMANDE + +**Correspond a MCT section 3.3** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | Le panier contient au moins 1 item (`items.length >= 1`) | +| **[PRE-2]** | Le numero de commande saisi par le client est non vide (validation front) | +| **[PRE-3]** | Le body JSON POST est valide (schema validation cote API) | +| **[RG-1]** | Pour chaque item du panier, le systeme verifie en base que le produit ou menu est encore `est_disponible = 1`. Si un item n'est plus disponible, la commande est rejetee avec un message liste des articles indisponibles. | +| **[RG-2]** | Determination du `tva_taux_pourmille` selon `mode_consommation` : `sur_place` = 1000 (10%), `a_emporter` = 550 (5,5%), `drive` = 550 (5,5%). Ref : service-public.fr article F31407 | +| **[RG-3]** | Calcul des montants (tout en centimes, entiers) : `total_ttc_cents = SUM(prix_unitaire_ttc_cents * quantite)` ; `total_ht_cents = ROUND(total_ttc_cents * 1000 / (1000 + tva_taux_pourmille))` ; `total_tva_cents = total_ttc_cents - total_ht_cents` | +| **[RG-4]** | Generation du numero de commande : format `K-YYYY-MM-DD-NNN` ou NNN est le compteur du jour de service courant (SELECT COUNT + 1 sur le `service_day` en cours, avec verrou pour eviter les doublons en concurrence) | +| **[RG-5]** | Insertion atomique (transaction) : INSERT `commande` puis INSERT N lignes `ligne_commande`. En cas d'echec partiel, rollback complet. | +| **[RG-6]** | Les snapshots `libelle_snapshot` et `prix_unitaire_ttc_cents_snapshot` sont copies depuis les entites courantes au moment de l'insertion (integrite historique). Ces valeurs ne sont pas modifiees apres insertion. | +| **[RG-7]** | La commande est inseree avec `statut = 'pending_payment'`. Une fois le numero de commande saisi par le client (substitut de paiement RNCP), le statut est mis a jour en `paid` dans la meme transaction. La transition `pending_payment -> paid` est atomique : aucun autre acteur ne peut observer le statut `pending_payment`. | +| **[POST-1]** | Une ligne `commande` existe en base avec `statut = 'paid'`, `source = 'kiosk'`, tous les montants calcules. La phase `pending_payment` n'est pas observable en dehors de la transaction. | +| **[POST-2]** | `N` lignes `ligne_commande` existent en base, referençant chacune soit un `produit_id` soit un `menu_id` (contrainte d'exclusivite verifiee) | +| **[POST-3]** | `commande.numero` est unique en base (contrainte UNIQUE sur la colonne) | +| **[OUT-1]** | Reponse HTTP 201 : `{data: {id: int, numero: string, statut: 'paid'}}` | +| **[OUT-2]** | Evenement logique COMMANDE_CREEE disponible pour le domaine preparation (la vue preparation se rafraichit - polling ou push selon implementation) | +| **[ERR-1]** | Panier vide : HTTP 422, `{error: {code: "EMPTY_CART"}}` | +| **[ERR-2]** | Article indisponible : HTTP 422, `{error: {code: "ITEM_UNAVAILABLE", items: [...]}}` | +| **[ERR-3]** | Erreur BDD / timeout : HTTP 500 avec rollback, `{error: {code: "DB_ERROR"}}` | + +--- + +### 2.4 AFFICHER_CONFIRMATION + +**Correspond a MCT section 3.4** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | La reponse API PASSER_COMMANDE a retourne HTTP 201 avec un objet `{id, numero, statut}` | +| **[RG-1]** | Le numero de commande est affiche en grand sur l'ecran de confirmation | +| **[RG-2]** | Apres un delai configurable (suggestion : 15 secondes), la borne se reinitialise automatiquement pour le client suivant | +| **[POST-1]** | Aucune ecriture en base | +| **[OUT-1]** | Ecran de confirmation affiche avec le numero | +| **[ERR-1]** | Si la reponse API est en erreur : affichage d'un message d'erreur generic et proposition de recommencer | + +--- + +## 3. Domaine 2 - Parcours commande (comptoir et drive) + +### 3.1 SAISIR_COMMANDE_MANUELLE + +**Correspond a MCT section 4.1** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie (session valide, `est_actif = 1`) | +| **[PRE-2]** | L'acteur possede la permission `commande.create` (verifiee via `role_permission`) | +| **[PRE-3]** | Le panier compose contient au moins 1 article | +| **[RG-1]** | Logique de creation identique a PASSER_COMMANDE (RG-1 a RG-7), a la difference suivante : la `source` est `comptoir` ou `drive` selon le canal selectionne par l'equipier. La meme sequence `pending_payment -> paid` est appliquee de facon atomique dans la transaction. | +| **[RG-2]** | Le `mode_consommation` est saisi par l'equipier (sur_place / a_emporter / drive) | +| **[RG-3]** | Le format du numero de commande est identique : `K-YYYY-MM-DD-NNN` (meme generateur, meme compteur du jour de service) | +| **[POST-1]** | Une ligne `commande` existe en base avec `statut = 'paid'`, `source = 'comptoir'` ou `'drive'`. Le statut `pending_payment` est transitoire et non observable hors transaction. | +| **[POST-2]** | `N` lignes `ligne_commande` existent, avec snapshots | +| **[OUT-1]** | Confirmation affichee dans le back-office, numero de commande communique au client | +| **[ERR-1]** | Memes cas d'erreur que PASSER_COMMANDE (ERR-1, ERR-2, ERR-3) | + +--- + +## 4. Domaine 3 - Preparation (cuisine) + +### 4.1 LISTER_COMMANDES_A_PREPARER + +**Correspond a MCT section 5.1** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, `est_actif = 1`, role `preparation` ou `admin` | +| **[PRE-2]** | L'acteur possede la permission `commande.read` | +| **[RG-1]** | Requete : `SELECT commande.*, ligne_commande.* FROM commande JOIN ligne_commande ON ... WHERE commande.statut = 'paid' ORDER BY commande.created_at ASC` | +| **[RG-2]** | Tous les canaux sont confondus (kiosk + comptoir + drive) | +| **[RG-3]** | Pour chaque commande, les lignes sont affichees avec `libelle_snapshot` et `quantite` (les snapshots sont utilises, pas de re-jointure sur produit/menu) | +| **[POST-1]** | Aucune ecriture en base | +| **[OUT-1]** | Liste des commandes au statut `paid`, ordonnee par heure croissante | + +--- + +### 4.2 MARQUER_EN_PREPARATION + +**Correspond a MCT section 5.2** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `commande.update` | +| **[PRE-2]** | La commande ciblee existe et son `statut = 'paid'` | +| **[RG-1]** | `UPDATE commande SET statut = 'preparing', updated_at = NOW() WHERE id = :id AND statut = 'paid'` | +| **[RG-2]** | La clause `AND statut = 'paid'` dans le UPDATE protege contre les mises a jour concurrentes (si deux equipiers cliquent simultanement, seul le premier reussit - le second recoit 0 rows affected) | +| **[POST-1]** | `commande.statut = 'preparing'`, `commande.updated_at` mis a jour | +| **[OUT-1]** | HTTP 200 ou redirection avec message de succes. La commande disparait de la liste "a preparer" et apparait dans la liste "en preparation". | +| **[ERR-1]** | Si `statut != 'paid'` au moment du UPDATE (concurrence) : HTTP 409 `{error: {code: "INVALID_TRANSITION"}}` | + +--- + +### 4.3 MARQUER_PRETE + +**Correspond a MCT section 5.3** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `commande.update` | +| **[PRE-2]** | La commande ciblee existe et son `statut = 'preparing'` | +| **[RG-1]** | `UPDATE commande SET statut = 'ready', updated_at = NOW() WHERE id = :id AND statut = 'preparing'` | +| **[RG-2]** | Meme protection contre la concurrence que MARQUER_EN_PREPARATION | +| **[POST-1]** | `commande.statut = 'ready'`, `commande.updated_at` mis a jour | +| **[OUT-1]** | La commande devient visible dans la vue "pretes" de l'accueil | +| **[ERR-1]** | Transition invalide : HTTP 409 `{error: {code: "INVALID_TRANSITION"}}` | + +--- + +## 5. Domaine 4 - Remise au client (accueil) + +### 5.1 LISTER_COMMANDES_PRETES + +**Correspond a MCT section 6.1** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `commande.read` | +| **[RG-1]** | `SELECT commande.*, ligne_commande.* FROM commande JOIN ligne_commande ON ... WHERE commande.statut = 'ready' ORDER BY commande.updated_at ASC` | +| **[RG-2]** | Tri par `updated_at` croissant : les commandes pretes depuis le plus longtemps apparaissent en premier | +| **[POST-1]** | Aucune ecriture en base | +| **[OUT-1]** | Liste des commandes au statut `ready` | + +--- + +### 5.2 DECLARER_LIVREE + +**Correspond a MCT section 6.2** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `commande.update` | +| **[PRE-2]** | La commande ciblee existe et son `statut = 'ready'` | +| **[RG-1]** | `UPDATE commande SET statut = 'delivered', updated_at = NOW() WHERE id = :id AND statut = 'ready'` | +| **[RG-2]** | `delivered` est un statut terminal : aucune transition n'est prevue depuis ce statut (contrainte applicative, non enfoercee en base) | +| **[POST-1]** | `commande.statut = 'delivered'`. Cycle de vie termine. La commande passe dans l'historique. | +| **[OUT-1]** | Confirmation de livraison affichee | +| **[ERR-1]** | Transition invalide : HTTP 409 `{error: {code: "INVALID_TRANSITION"}}` | + +--- + +## 6. Domaine 5 - Annulation + +### 6.1 ANNULER_COMMANDE + +**Correspond a MCT section 7.1** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `commande.cancel` | +| **[PRE-2]** | La commande ciblee existe | +| **[PRE-3]** | `commande.statut` est dans `['pending_payment', 'paid', 'preparing', 'ready']`. Seuls les statuts finaux `delivered` et `cancelled` ne permettent pas la transition vers `cancelled` : une commande reste annulable tant qu'elle n'a pas ete remise (modification, annulation ou remboursement a la demande du client). | +| **[RG-1]** | `UPDATE commande SET statut = 'cancelled', updated_at = NOW() WHERE id = :id AND statut IN ('pending_payment', 'paid', 'preparing', 'ready')` | +| **[RG-2]** | La commande n'est pas supprimee physiquement : elle reste en base pour l'historique et les stats (les commandes annulees sont exclues du CA mais comptees dans les volumes). | +| **[RG-3]** | Les lignes `ligne_commande` ne sont pas supprimees (ON DELETE CASCADE n'est pas declenche) : elles permettent de savoir ce qui avait ete commande. | +| **[POST-1]** | `commande.statut = 'cancelled'`, etat terminal | +| **[OUT-1]** | Confirmation d'annulation | +| **[ERR-1]** | Tentative d'annulation d'une commande deja remise ou annulee (`delivered`, `cancelled`) : HTTP 422 `{error: {code: "CANNOT_CANCEL_IN_STATE"}}` | +| **[ERR-2]** | Transition invalide (concurrence) : HTTP 409 `{error: {code: "INVALID_TRANSITION"}}` | + +--- + +## 7. Domaine 6 - Gestion du catalogue (admin) + +### 7.1 CREER_PRODUIT + +**Correspond a MCT section 8.1** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `produit.create` | +| **[PRE-2]** | Le `categorie_id` fourni correspond a une `categorie` existante et active | +| **[RG-1]** | Validation du formulaire : `libelle` non vide, `prix_ttc_cents > 0`, `categorie_id` valide | +| **[RG-2]** | Si une image est uploadee : validation du type MIME (JPEG, PNG, WEBP uniquement), taille max configurable (suggestion : 2 Mo), stockage dans le volume `wakdo_uploads`, enregistrement du chemin relatif dans `image_path` | +| **[RG-3]** | `est_disponible = 1` par defaut a l'insertion | +| **[RG-4]** | `ordre` est affecte a la valeur MAX(ordre) + 1 pour la categorie ciblee, ou 0 si premiere insertion | +| **[POST-1]** | Un enregistrement `produit` existe en base avec tous les champs valides | +| **[OUT-1]** | Redirection vers la liste des produits de la categorie, message de succes | +| **[ERR-1]** | Validation echouee : affichage des erreurs de champ inline | +| **[ERR-2]** | Image invalide (type ou taille) : message d'erreur specifique | + +--- + +### 7.2 MODIFIER_PRODUIT + +**Correspond a MCT section 8.2** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `produit.update` | +| **[PRE-2]** | Le `produit.id` cible existe en base | +| **[RG-1]** | Memes validations que CREER_PRODUIT sur les champs modifies | +| **[RG-2]** | Si une nouvelle image est uploadee, l'ancienne image est supprimee du filesystem (nettoyage du volume) | +| **[RG-3]** | Les `libelle_snapshot` et `prix_unitaire_ttc_cents_snapshot` dans les `ligne_commande` historiques ne sont pas modifies par ce traitement (integrite des commandes passees) | +| **[POST-1]** | `produit` mis a jour, `updated_at` rafraichi | +| **[OUT-1]** | Redirection vers la liste, message de succes | + +--- + +### 7.3 SUPPRIMER_PRODUIT + +**Correspond a MCT section 8.3** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `produit.delete` | +| **[PRE-2]** | Le `produit.id` cible existe en base | +| **[RG-1]** | Verification prealable (PHP) : le produit est-il reference dans `menu_produit` ? Si oui, afficher un message "Ce produit est utilise dans X menu(s) : [liste]. Retirez-le d'abord des menus." et bloquer. | +| **[RG-2]** | La FK `menu_produit.produit_id` est definie avec `ON DELETE RESTRICT` en base : meme si la verification applicative est contournee, la base bloque la suppression. | +| **[RG-3]** | Si le produit est reference dans des `ligne_commande` historiques (FK `ON DELETE RESTRICT`), la suppression est egalement bloquee. Gestion recommandee : desactiver le produit (`est_disponible = 0`) plutot que le supprimer. | +| **[POST-1]** | Si aucune contrainte : le produit est supprime de la base | +| **[OUT-1]** | Redirection vers la liste, message de succes | +| **[ERR-1]** | Produit utilise dans un menu : HTTP 422 ou affichage inline avec liste des menus bloquants | +| **[ERR-2]** | Produit dans des commandes historiques : message "Ce produit a deja ete commande. Desactivez-le plutot que de le supprimer." | + +--- + +### 7.4 CREER_MENU + +**Correspond a MCT section 8.4** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `menu.create` | +| **[PRE-2]** | Au moins un produit de role `burger` est inclus dans la composition | +| **[PRE-3]** | Tous les `produit_id` de la composition existent et sont `est_disponible = 1` | +| **[RG-1]** | Validation : `libelle` non vide, `prix_ttc_cents > 0`, composition valide (au moins burger) | +| **[RG-2]** | Transaction : INSERT `menu`, puis INSERT N lignes `menu_produit` avec `menu_id`, `produit_id`, `role`, `position` | +| **[RG-3]** | Les roles valides pour `menu_produit.role` sont : `burger`, `accompagnement`, `boisson`, `sauce`, `dessert` (ENUM en base) | +| **[POST-1]** | Un enregistrement `menu` et ses lignes `menu_produit` existent en base | +| **[OUT-1]** | Redirection vers la liste des menus, message de succes | +| **[ERR-1]** | Composition invalide (pas de burger) : message d'erreur metier | +| **[ERR-2]** | Produit de la composition indisponible : avertissement (le menu peut etre cree avec ce produit, mais sera potentiellement affiche comme "incomplet" sur la borne) | + +--- + +### 7.5 MODIFIER_MENU + +**Correspond a MCT section 8.5** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `menu.update` | +| **[PRE-2]** | Le `menu.id` cible existe | +| **[RG-1]** | Memes validations que CREER_MENU sur les champs modifies | +| **[RG-2]** | Si la composition est modifiee : `DELETE FROM menu_produit WHERE menu_id = :id`, puis INSERT des nouvelles lignes (pattern delete-and-reinsert, atomique en transaction) | +| **[RG-3]** | Les snapshots dans `ligne_commande` ne sont pas affectes | +| **[POST-1]** | `menu` mis a jour, composition `menu_produit` reconstruite | +| **[OUT-1]** | Redirection, message de succes | + +--- + +### 7.6 SUPPRIMER_MENU + +**Correspond a MCT section 8.6** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `menu.delete` | +| **[PRE-2]** | Le `menu.id` cible existe | +| **[RG-1]** | Verification prealable : le menu est-il reference dans des `ligne_commande` historiques ? FK `ON DELETE RESTRICT`. Si oui, proposer la desactivation (`est_disponible = 0`) plutot que la suppression. | +| **[RG-2]** | Si aucune `ligne_commande` ne le reference : DELETE du menu (cascade automatique sur `menu_produit` via `ON DELETE CASCADE`) | +| **[POST-1]** | Menu et ses lignes `menu_produit` supprimes | +| **[OUT-1]** | Redirection, message de succes | +| **[ERR-1]** | Menu present dans des commandes historiques : message "Ce menu a deja ete commande. Desactivez-le plutot que de le supprimer." | + +--- + +### 7.7 GERER_CATEGORIE + +**Correspond a MCT section 8.7** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `categorie.manage` | +| **[RG-CREATE]** | `libelle` et `slug` non vides et uniques en base. `ordre` affecte a MAX + 1. | +| **[RG-UPDATE]** | Mises a jour de `libelle`, `slug`, `image_path`, `ordre`, `est_actif` | +| **[RG-DEACTIVATE]** | La desactivation d'une categorie (`est_actif = 0`) ne desactive pas automatiquement les produits/menus enfants en base (pas de CASCADE sur `est_actif`). La logique PHP doit proposer a l'admin de desactiver aussi les produits/menus enfants, ou la borne filtre `categorie.est_actif = 1` ce qui masque de facto les produits de la categorie. | +| **[RG-DELETE]** | Suppression physique bloquee si des `produit` ou `menu` ont `categorie_id = categorie.id` (FK `ON DELETE RESTRICT`). Proposer la desactivation. | +| **[POST-CREATE]** | Nouveau enregistrement `categorie` en base | +| **[POST-UPDATE]** | `categorie` mis a jour, `updated_at` rafraichi | +| **[OUT-1]** | Confirmation, retour a la liste des categories | + +--- + +## 8. Domaine 7 - Gestion des utilisateurs et roles (admin) + +### 8.1 CREER_USER + +**Correspond a MCT section 9.1** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `user.create` | +| **[PRE-2]** | L'email fourni n'existe pas dans `user.email` (contrainte UNIQUE) | +| **[PRE-3]** | Le `role_id` fourni correspond a un `role` existant et actif | +| **[RG-1]** | Validation : `email` conforme RFC 5321 (validation PHP `FILTER_VALIDATE_EMAIL`), `nom` et `prenom` non vides, `role_id` valide | +| **[RG-2]** | Hash du mot de passe : `password_hash($password, PASSWORD_ARGON2ID)`. Longueur min du mot de passe : 8 caracteres. | +| **[RG-3]** | `est_actif = 1` par defaut | +| **[RG-4]** | `last_login_at = NULL` a la creation | +| **[POST-1]** | Enregistrement `user` en base avec `password_hash` argon2id, `role_id` valide | +| **[OUT-1]** | Redirection vers la liste des utilisateurs, message de succes | +| **[ERR-1]** | Email deja existant : message "Cet email est deja utilise" | +| **[ERR-2]** | Mot de passe trop court : message de validation inline | + +--- + +### 8.2 MODIFIER_USER + +**Correspond a MCT section 9.2** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `user.update` | +| **[PRE-2]** | Le `user.id` cible existe | +| **[RG-1]** | Si un nouveau mot de passe est fourni (champ non vide) : rehachage via `PASSWORD_ARGON2ID` et remplacement du hash existant | +| **[RG-2]** | Si le mot de passe n'est pas modifie (champ vide) : le hash existant est conserve sans modification | +| **[RG-3]** | L'email peut etre modifie sous contrainte UNIQUE (verification avant UPDATE) | +| **[POST-1]** | `user` mis a jour, `updated_at` rafraichi | +| **[OUT-1]** | Redirection, message de succes | + +--- + +### 8.3 DESACTIVER_USER + +**Correspond a MCT section 9.3** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `user.update` | +| **[PRE-2]** | L'acteur ne cible pas son propre compte (protection : `$targetUserId !== $currentUserId`) | +| **[RG-1]** | `UPDATE user SET est_actif = 0, updated_at = NOW() WHERE id = :id` | +| **[RG-2]** | La session eventuellemement active de cet utilisateur sera invalidee au prochain acces : le middleware verifie `user.est_actif = 1` a chaque requete authentifiee | +| **[POST-1]** | `user.est_actif = 0`. L'utilisateur ne peut plus se connecter. Son historique reste intact. | +| **[OUT-1]** | Redirection, message de succes | +| **[ERR-1]** | Tentative d'auto-desactivation : HTTP 403 `{error: {code: "SELF_DEACTIVATION_FORBIDDEN"}}` | + +--- + +### 8.4 GERER_MATRICE_RBAC + +**Correspond a MCT section 9.4** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | L'acteur est authentifie, permission `role.manage` | +| **[PRE-2]** | Le `role.id` cible existe | +| **[PRE-3]** | Les `permission_id` soumis existent tous en base | +| **[RG-1]** | Transaction : `DELETE FROM role_permission WHERE role_id = :id`, puis INSERT des nouvelles lignes `(role_id, permission_id)` pour chaque permission selectionnee | +| **[RG-2]** | Les permissions ne sont pas modifiables via cette operation : elles sont uniquement lues pour construire le formulaire de selection | +| **[RG-3]** | La modification prend effet immediatement pour les nouvelles requetes ; les sessions actives des users portant ce role verront la modification au prochain acces (la session stocke le `role_id` mais les permissions sont rechargees depuis la base a chaque verification) | +| **[POST-1]** | La table `role_permission` reflete exactement les permissions selectionnees pour ce role | +| **[OUT-1]** | Redirection, message de succes | + +--- + +## 9. Domaine 8 - Authentification back-office + +### 9.1 AUTHENTIFIER_USER + +**Correspond a MCT section 10.1** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | Le formulaire de connexion a ete soumis avec un email et un mot de passe | +| **[PRE-2]** | Le token CSRF du formulaire est valide (protection anti-CSRF) | +| **[RG-1]** | Lookup : `SELECT * FROM user WHERE email = :email AND est_actif = 1 LIMIT 1` | +| **[RG-2]** | Verification du mot de passe : `password_verify($password, $user->password_hash)`. Si echec : meme message d'erreur generic que si l'email n'existe pas (protection contre l'enumeration d'emails). | +| **[RG-3]** | Si succes : `session_regenerate(true)` (regeneration de l'ID de session, protection contre la fixation de session) | +| **[RG-4]** | Stockage en session : `$_SESSION['user_id']`, `$_SESSION['role_id']`, `$_SESSION['logged_in_at']` | +| **[RG-5]** | Mise a jour : `UPDATE user SET last_login_at = NOW() WHERE id = :id` | +| **[RG-6]** | Timeouts de session : idle timeout 4h (detection via timestamp de derniere activite en session), absolute timeout 10h (detection via `logged_in_at`) | +| **[POST-1]** | Session PHP ouverte avec `user_id` et `role_id`. `user.last_login_at` mis a jour. | +| **[OUT-1]** | Redirection vers la vue par defaut du role (preparation -> file d'attente, accueil -> commandes pretes, admin -> dashboard) | +| **[ERR-1]** | Identifiants incorrects ou compte inactif : message generic "Email ou mot de passe incorrect" (pas de distinction pour eviter l'enumeration) | +| **[ERR-2]** | Token CSRF invalide : HTTP 403 | + +--- + +### 9.2 DECONNECTER_USER + +**Correspond a MCT section 10.2** + +| Tag | Contenu | +|-----|---------| +| **[PRE-1]** | Une session valide est ouverte (`session_id()` non vide, `$_SESSION['user_id']` present) | +| **[RG-1]** | `$_SESSION = []` (vider les donnees de session) | +| **[RG-2]** | Si le cookie de session existe, l'expirer : `setcookie(session_name(), '', time() - 3600, '/', '', true, true)` | +| **[RG-3]** | `session_destroy()` | +| **[POST-1]** | Session PHP detruite. Aucun acces authentifie possible avec l'ancien cookie. | +| **[OUT-1]** | Redirection vers la page de connexion | + +--- + +## 10. Traitements automatises - Crons (hors interactions utilisateur) + +Ces traitements sont executes par le service `wakdo-cron` (container Alpine + PHP CLI) dans +la fenetre de maintenance 01h30-09h30 (hors service actif). Ils sont hors scope MCT +(traitements techniques, pas de declencheur utilisateur) mais sont documentes ici pour +coherence avec PROJECT_CONTEXT section 7 (Bloc 5 DevOps). + +### 10.1 Agregation des stats (cron 04h30) + +| Tag | Contenu | +|-----|---------| +| **[TRIGGER]** | Cron : `30 4 * * *` | +| **[RG-1]** | Calcul du `service_day` ecoule : `J-1` si execution a 04h30 (dans la fenetre 01h-10h du jour J, le `service_day` a agregger est J-1) | +| **[RG-2]** | `service_day` pour une commande : `CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at - INTERVAL 1 DAY) ELSE DATE(created_at) END` | +| **[RG-3]** | Agregations calculees par `service_day` : nombre de commandes, CA TTC (somme `total_ttc_cents` des commandes `statut != 'cancelled'`), top produits (par `libelle_snapshot`, COUNT occurrences dans `ligne_commande`) | +| **[POST-1]** | Stats disponibles pour la vue dashboard admin (requetes directes sur `commande` filtrees par `service_day` ou table d'agregation si implementee) | + +### 10.2 Purge des sessions expirees (cron toutes les 15 min) + +| Tag | Contenu | +|-----|---------| +| **[TRIGGER]** | Cron : `*/15 * * * *` | +| **[RG-1]** | Si les sessions PHP sont stockees en fichiers (defaut) : `find /tmp/sessions -mmin +240 -delete` (suppression des fichiers de session vieux de plus de 4h) | +| **[RG-2]** | Si les sessions sont en base (option) : `DELETE FROM php_sessions WHERE updated_at < NOW() - INTERVAL 4 HOUR` | +| **[POST-1]** | Sessions expirees supprimees. Les utilisateurs inactifs depuis plus de 4h seront forces a se reconnecter. | + +### 10.3 Backup BDD (cron 03h00) + +| Tag | Contenu | +|-----|---------| +| **[TRIGGER]** | Cron : `0 3 * * *` | +| **[RG-1]** | `mysqldump` de la base `wakdo` vers un fichier date dans le volume backup | +| **[RG-2]** | Retention : conservation des 7 derniers dumps (suppression des plus anciens) | +| **[POST-1]** | Dump SQL disponible pour restauration | + +--- + +## 11. Tableau recapitulatif des regles de gestion transverses + +Ces regles s'appliquent a plusieurs operations et sont centralisees ici pour eviter la +repetition. + +| Code RG | Libelle | Operations concernees | +|---------|---------|----------------------| +| **RG-T01** | Verification CSRF sur tous les formulaires POST/PUT/DELETE du back-office | AUTH, toutes ops admin | +| **RG-T02** | Verification session active + `est_actif = 1` sur chaque requete authentifiee | Toutes ops domaines 2-7 | +| **RG-T03** | Verification permission via `role_permission` avant execution de l'operation | Toutes ops domaines 2-7 | +| **RG-T04** | Tous les montants monetaires sont manipules en centimes (INT). Conversion EUR uniquement en sortie. | 2.3, 3.1, 7.1, 7.4 | +| **RG-T05** | Les snapshots (`libelle_snapshot`, `prix_unitaire_ttc_cents_snapshot`) ne sont pas modifies apres insertion dans `ligne_commande` (integrite historique des commandes). | 2.3, 7.2, 7.5 | +| **RG-T06** | Toutes les requetes SQL passent par PDO avec prepared statements. Aucune concatenation de donnees utilisateur dans une requete SQL. | Toutes operations | +| **RG-T07** | Les transitions de statut `commande` incluent `AND statut = ` dans la clause WHERE pour proteger contre les mises a jour concurrentes | 4.2, 4.3, 5.2, 6.1 | +| **RG-T08** | Les operations de creation/modification de catalogue ou users se font en transaction atomique quand elles touchent plusieurs tables | 2.3, 7.4, 7.5, 8.4 | +| **RG-T09** | Contrainte croisee `(source, mode_consommation)` sur `commande` : si `source = 'drive'`, alors `mode_consommation = 'drive'` (verification a la creation). Materialisable en CHECK SQL : `CHECK (source != 'drive' OR mode_consommation = 'drive')`. | 2.3, 3.1 | +| **RG-T10** | Toute operation qui modifie `commande.statut` doit aussi inserer une ligne dans `commande_event` dans la meme transaction (event_type aligne sur la transition, from_statut, to_statut, user_id de l'acteur ou NULL si auto, payload JSON optionnel). Append-only : aucun UPDATE / DELETE applicatif. A encapsuler dans un repository pour eviter les oublis. | 2.3, 3.1, 4.2, 4.3, 5.2, 6.1 | + +--- + +## 12. Coherence avec la machine a etats (recap MLT) + +Synthese des transitions de statut `commande` couvertes par le MLT, avec les operations MLT +correspondantes et les protections associees. + +| Transition | Operation MLT | Condition SQL | Protection concurrence | Event audit insere | +|------------|---------------|---------------|------------------------|--------------------| +| `-> pending_payment` (creation) | PASSER_COMMANDE (2.3), SAISIR_COMMANDE_MANUELLE (3.1) | INSERT avec statut `pending_payment` | Transaction atomique | `CREATED` | +| `pending_payment -> paid` (paiement) | PASSER_COMMANDE (2.3), SAISIR_COMMANDE_MANUELLE (3.1) | UPDATE dans la meme transaction | Transaction atomique | `PAID` | +| `paid -> preparing` | MARQUER_EN_PREPARATION (4.2) | `WHERE statut = 'paid'` | AND statut dans WHERE | `PREPARING_STARTED` | +| `preparing -> ready` | MARQUER_PRETE (4.3) | `WHERE statut = 'preparing'` | AND statut dans WHERE | `READY` | +| `ready -> delivered` | DECLARER_LIVREE (5.2) | `WHERE statut = 'ready'` | AND statut dans WHERE | `DELIVERED` | +| `pending_payment/paid/preparing/ready -> cancelled` | ANNULER_COMMANDE (6.1) | `WHERE statut IN ('pending_payment', 'paid', 'preparing', 'ready')` | AND statut dans WHERE | `CANCELLED` | + +Statuts terminaux (aucune transition prevue depuis ce statut) : `delivered`, `cancelled`. + +Note : la transition `pending_payment -> paid` est interne a l'operation de creation et non +observable par les autres acteurs. Le statut `pending_payment` ne sera visible dans aucune file +d'attente metier (preparation, accueil) : ces vues filtrent sur `paid`, `preparing`, `ready`. + +--- + +## 13. Points d'incoherence signales et arbitrages attendus + +Ces points ont ete identifies lors de la construction du MLT. Ils reprennent et completent +les points signales au MCT section 14. + +### 13.1 Colonne `source` vs `mode_consommation` sur `commande` - RESOLU (2026-05-28) + +**Decision actee** : ajout d'une colonne `source ENUM('kiosk','comptoir','drive')` sur `commande`, en plus de `mode_consommation`. Deux dimensions distinctes maintenues : + +- `mode_consommation` (sur_place / a_emporter / drive) : visee fiscale, determine le taux de TVA (10% sur_place, 5,5% a_emporter en restauration rapide FR) +- `source` (kiosk / comptoir / drive) : visee operationnelle, trace le canal de saisie + +**Contrainte croisee** : `source = drive` implique `mode_consommation = drive`. Pour `kiosk` et `comptoir`, les deux dimensions sont independantes. Verifiee dans la regle [RG-T09] ci-dessous (section 11). + +Dictionnaire et MCD amendes (cf. dictionary 3.5 + notes 8/9, MCD 4.2). + +### 13.2 Tracabilite acteur sur `commande` - RESOLU (2026-05-28) + +**Decision actee** : pas de colonnes `created_by_user_id` / `prepared_by_user_id` etc. directes sur `commande`. A la place, **table d'audit dediee `commande_event`** (cf. dictionary 3.7, MCD 4.2.bis, dictionary note 10). Pattern event sourcing simplifie. + +- Append-only : aucun UPDATE / DELETE applicatif sur `commande_event` +- Chaque operation qui modifie `commande.statut` insere une ligne avec event_type, from_statut, to_statut, user_id (NULL si auto), payload (JSON nullable) +- Tracabilite complete sans denormalisation + +Pattern d'ecriture documente dans la regle [RG-T10] (section 11). + +### 13.3 Statut `pending_payment` - RESOLU + +Le statut `pending_payment` est maintenu dans la machine canonique. Il represente la phase +de composition de la commande avant paiement, conformement a la regle metier confirmee +(le client compose sa commande, PUIS il paie). La transition `pending_payment -> paid` est +atomique dans les operations de creation, ce statut est donc non observable par les files +d'attente metier. Il est reserve pour une evolution vers un paiement reel asynchrone sans +migration destructive de l'ENUM. Ce point est clos. + +### 13.4 (Information) `service_day` non persiste en colonne + +PROJECT_CONTEXT documente la logique `service_day` (section 2). Elle n'est pas +materialisee comme colonne dans le dictionnaire. Pour les requetes de stats frequentes, +une colonne calculee (colonne generee MariaDB, syntaxe `AS (expression) VIRTUAL/STORED`) +pourrait etre envisagee au DDL pour eviter de recalculer a chaque requete. Non bloquant pour MVP. diff --git a/docs/uml/sequence-passer-commande.md b/docs/uml/sequence-passer-commande.md new file mode 100644 index 0000000..3b00cfe --- /dev/null +++ b/docs/uml/sequence-passer-commande.md @@ -0,0 +1,193 @@ +# Diagramme de sequence - Passer une commande (borne client) + +**Phase UML** : P1 - Conception, complement UML (apres MCD) +**Statut** : v0.1 +**Date** : 2026-05-21 +**Branche** : `feat/p1-conception` +**Auteur methodologie** : BYAN + +--- + +## 1. Objet du document + +Ce document decrit le **flux temporel** du parcours "passer une commande" cote +**Client sur la borne kiosk** : navigation dans les categories, selection d'un +produit ou composition d'un menu, gestion du panier, validation avec saisie du +numero de retrait, paiement, puis confirmation. + +Le diagramme reste au niveau **conceptuel / logique**. Il nomme les echanges +entre participants sans detailler l'implementation PHP (controllers, models) +ni le SQL exact. Il complete le cas d'utilisation "Passer une commande" de +`docs/uml/use-cases.md` et la machine a etats de `docs/uml/state-commande.md`. + +**Sources** : +- `docs/PROJECT_CONTEXT.md` section 2 (processus metier), section 7 (endpoints API) +- `docs/merise/dictionary.md` (`commande`, `ligne_commande`, `menu`, `produit`) +- `docs/uml/state-commande.md` (transitions `pending_payment -> paid`) + +--- + +## 2. Participants + +| Participant | Role | Couche | +|---|---|---| +| **Client** | Utilisateur final, compose sa commande au doigt | Acteur | +| **Borne** | Interface tactile (front Bloc 1, HTML/CSS/JS vanilla) | Presentation | +| **API** | Back-end REST sous `/api/*` (Bloc 2) | Application | +| **BDD** | Base de donnees MariaDB | Persistance | + +Le panier est gere **cote Borne** (etat local du front) jusqu'a la validation. +Aucune commande n'est creee en base avant la validation finale, pour eviter les +commandes fantomes abandonnees. + +--- + +## 3. Diagramme de sequence + +```mermaid +sequenceDiagram + actor Client + participant Borne + participant API + participant BDD + + Note over Client,BDD: Phase 1 - Navigation du catalogue + + Client->>Borne: ouvrir la borne + Borne->>API: GET /api/categories + API->>BDD: lire les categories actives + BDD-->>API: liste des categories + API-->>Borne: categories (JSON) + Borne-->>Client: afficher les categories + + Client->>Borne: choisir une categorie + Borne->>API: GET /api/products (filtre categorie) + API->>BDD: lire les produits disponibles + BDD-->>API: liste des produits + API-->>Borne: produits (JSON) + Borne-->>Client: afficher les produits + + Note over Client,BDD: Phase 2 - Selection produit ou composition menu + + alt Produit a la carte + Client->>Borne: selectionner un produit + Client->>Borne: regler taille / options + Borne->>Borne: ajouter la ligne au panier local + else Composition d'un menu + Client->>Borne: selectionner un menu + Borne->>API: GET /api/menus (composition du menu) + API->>BDD: lire menu et composition + BDD-->>API: menu + produits par role + API-->>Borne: composition (JSON) + Borne-->>Client: afficher les choix par slot (burger, accompagnement, boisson, sauce) + Client->>Borne: choisir chaque composant + tailles + Borne->>Borne: ajouter la ligne menu au panier local + end + + Note over Client,BDD: Phase 3 - Gestion du panier + + Client->>Borne: consulter le panier + Borne-->>Client: recapitulatif + total provisoire + opt Modifier le panier + Client->>Borne: ajuster quantite / supprimer une ligne + Borne->>Borne: recalculer le total local + Borne-->>Client: panier mis a jour + end + + Note over Client,BDD: Phase 4 - Validation du panier et saisie du numero + + Client->>Borne: valider la commande + Client->>Borne: saisir le numero de retrait + Borne->>Borne: valider le panier (au moins 1 ligne) + Borne->>API: POST /api/orders (lignes + mode_consommation + numero) + + API->>API: recalculer les totaux cote serveur + API->>BDD: creer la commande (statut pending_payment) + API->>BDD: creer les lignes (snapshot libelle + prix) + BDD-->>API: commande persistee {id, numero, statut: pending_payment} + API-->>Borne: 201 Created {id, numero, statut: pending_payment, total} + Borne-->>Client: afficher le total a regler + + Note over Client,BDD: Phase 5 - Paiement (pending_payment -> paid) + + Client->>Borne: payer la commande + Borne->>API: POST /api/orders/{id}/pay + API->>BDD: enregistrer le paiement, passer la commande a paid (paye_a) + BDD-->>API: commande mise a jour {id, numero, statut: paid} + + Note over Client,BDD: Phase 6 - Confirmation + + API-->>Borne: 200 OK {id, numero, statut: paid} + Borne-->>Client: ecran de confirmation avec le numero + + Note over Client,BDD: Cas d'erreur + + alt Panier vide ou donnees invalides + API-->>Borne: 4xx {error: code, message} + Borne-->>Client: message d'erreur, retour au panier + end +``` + +--- + +## 4. Notes de modelisation + +### 4.1 Recalcul des totaux cote serveur + +La Borne affiche un total **provisoire** calcule localement pour l'experience +utilisateur. L'API recalcule les totaux a la reception du `POST /api/orders` a +partir des prix en base, puis fige les snapshots +(`prix_unitaire_ttc_cents_snapshot`, `libelle_snapshot` dans `ligne_commande`, +voir `dictionary.md` 3.6). Le total affiche par le client n'est pas considere +comme la source de verite : ceci limite la falsification du prix cote client. + +### 4.2 Transitions de statut + +Le parcours materialise les transitions T1 et T2 de +`docs/uml/state-commande.md`, en deux phases successives conformes a la regle +metier : + +- `POST /api/orders` cree la commande composee en `pending_payment` (T1). +- `POST /api/orders/{id}/pay` enregistre le paiement et fait passer la commande + a `paid` (T2), avec l'horodatage `paye_a`. + +La separation des deux appels reflete les deux phases du cycle de vie : +composer la commande, puis la payer. + +### 4.3 Panier local jusqu'a la validation + +Aucun appel ecriture vers la BDD n'a lieu pendant les phases 1 a 3. Le panier +vit dans l'etat du front (JavaScript). Ce choix evite de creer en base des +commandes abandonnees et reduit le nombre d'ecritures. Inconvenient connu : un +rafraichissement de la borne peut vider le panier ; un stockage local cote +navigateur peut etre envisage plus tard. + +### 4.4 Fallback JSON (hors flux nominal) + +`PROJECT_CONTEXT.md` section 4 prevoit un mode de repli ou la Borne lit des +fichiers JSON statiques si l'API est indisponible. Ce mode concerne uniquement +les lectures (phases 1 a 2). La validation (phase 4) et le paiement (phase 5) +requierent l'API ; sans elle, la commande n'est ni persistee ni payee. Ce cas +degrade n'est pas detaille dans le diagramme nominal ci-dessus. + +--- + +## 5. Coherence avec les autres livrables + +| Verification | Resultat | +|---|---| +| Endpoints utilises existent dans `PROJECT_CONTEXT.md` section 7 | `GET /api/categories`, `GET /api/products`, `GET /api/menus`, `POST /api/orders` ; `POST /api/orders/{id}/pay` est a confirmer en section 7 du brief | +| Entites manipulees presentes au MCD | Oui : `categorie`, `produit`, `menu`, `menu_produit`, `commande`, `ligne_commande` | +| Statuts utilises coherents avec `state-commande.md` | Oui : `pending_payment` puis `paid` (T1, T2), valeurs ENUM anglaises | +| Format de reponse JSON | Coherent avec `PROJECT_CONTEXT.md` section 7 (`{data, error}`) et la reponse `{id, number, status}` du POST orders | + +--- + +## 6. Arbitrage tranche + +La phase de paiement est integree au flux conformement a la regle metier des +deux phases (composer puis payer). La sequence suit la machine canonique de +`state-commande.md` : creation en `pending_payment` (T1) puis paiement vers +`paid` (T2), avec des valeurs ENUM en anglais. Point a confirmer au MCT : +l'endpoint de paiement (`POST /api/orders/{id}/pay`) doit etre reporte dans la +section 7 du brief s'il n'y figure pas encore. diff --git a/docs/uml/state-commande.md b/docs/uml/state-commande.md new file mode 100644 index 0000000..a99f309 --- /dev/null +++ b/docs/uml/state-commande.md @@ -0,0 +1,144 @@ +# Diagramme d'etats-transitions - Commande + +**Phase UML** : P1 - Conception, complement UML (apres MCD) +**Statut** : v0.1 +**Date** : 2026-05-21 +**Branche** : `feat/p1-conception` +**Auteur methodologie** : BYAN + +--- + +## 1. Objet du document + +Ce document formalise la **machine a etats** de l'attribut `commande.statut`. +Il decrit les etats possibles d'une commande, les transitions autorisees entre +ces etats, les **evenements** qui les declenchent et les **gardes** (conditions) +qui les conditionnent. + +Il complete le MCD (`docs/merise/mcd.md` section 9, qui esquisse le cycle de +vie) et le dictionnaire (`docs/merise/dictionary.md` 3.5, qui declare l'ENUM). + +--- + +## 2. Source de verite et regle metier + +La regle metier confirmee fixe deux phases successives dans le cycle de vie +d'une commande : le client **compose** sa commande, **puis** il **paie**. Une +fois payee, la commande entre en preparation. Le paiement fait partie integrante +du cycle. Les valeurs d'etat sont en anglais et alignees sur l'ENUM du +dictionnaire. + +| Source | Valeurs de statut | +|---|---| +| `dictionary.md` 3.5 (ENUM SQL) | `pending_payment`, `paid`, `preparing`, `ready`, `delivered`, `cancelled` | +| Regle metier confirmee | composer -> payer -> preparer -> pret -> remettre | + +**Machine a etats canonique** : la machine ci-dessous est la seule autorisee. +Elle suit l'ENUM du dictionnaire et la regle metier des deux phases : + +- `pending_payment` : commande composee, en attente de paiement. +- `paid` : paiement effectue ; la commande peut entrer en file de preparation. + +> Le dictionnaire (`dictionary.md` 3.5) et la machine ci-dessous partagent la +> meme ENUM, ce qui maintient la coherence entre le modele de donnees et le +> modele d'etats (cross-validation, mantra #34). + +--- + +## 3. Etats retenus + +| Etat | Valeur ENUM | Signification | Acteur qui declenche l'entree | +|---|---|---|---| +| En attente de paiement | `pending_payment` | Commande composee, panier fige, en attente de paiement. | Client (kiosk) ou Accueil (counter/drive) | +| Payee | `paid` | Paiement effectue ; la commande peut entrer en file de preparation. | Client (paiement) ou Accueil | +| En preparation | `preparing` | Prise en charge par la Preparation, en cuisine. | Preparation | +| Prete | `ready` | Preparation terminee, prete au comptoir. | Preparation | +| Livree | `delivered` | Remise effectuee au client. Etat **final**. | Accueil | +| Annulee | `cancelled` | Commande abandonnee ou annulee. Etat **final**. | Client, Accueil ou Administration | + +--- + +## 4. Diagramme d'etats-transitions + +```mermaid +stateDiagram-v2 + [*] --> pending_payment : creer commande (kiosk / counter / drive) + + pending_payment --> paid : payer\n[panier contient au moins 1 ligne] + pending_payment --> cancelled : abandonner\n[avant paiement] + + paid --> preparing : prendre en charge\n[acteur Preparation, file triee par heure croissante] + paid --> cancelled : annuler\n[Accueil ou Administration] + + preparing --> ready : declarer preparee\n[acteur Preparation] + preparing --> cancelled : annuler\n[rupture produit / decision Administration] + + ready --> delivered : remettre au client\n[acteur Accueil] + ready --> cancelled : annuler\n[client absent / non recuperee] + + delivered --> [*] + cancelled --> [*] +``` + +--- + +## 5. Transitions detaillees + +| # | De | Vers | Evenement declencheur | Garde (condition) | Acteur | +|---|---|---|---|---|---| +| T1 | (initial) | `pending_payment` | Creation de la commande composee | Au moins un item ajoute au panier en cours | Client / Accueil | +| T2 | `pending_payment` | `paid` | Paiement de la commande | La commande contient au moins une `ligne_commande` ; le paiement aboutit | Client / Accueil | +| T3 | `pending_payment` | `cancelled` | Abandon avant paiement | Commande pas encore payee | Client / Accueil | +| T4 | `paid` | `preparing` | Prise en charge en file | La commande est la plus ancienne non traitee (tri par heure de livraison croissante) | Preparation | +| T5 | `paid` | `cancelled` | Annulation avant preparation | Decision operationnelle | Accueil / Administration | +| T6 | `preparing` | `ready` | Declaration "preparee" | Preparation terminee | Preparation | +| T7 | `preparing` | `cancelled` | Annulation pendant preparation | Rupture produit ou decision Administration | Preparation / Administration | +| T8 | `ready` | `delivered` | Remise physique au client | Le client se presente avec le bon numero | Accueil | +| T9 | `ready` | `cancelled` | Annulation apres preparation | Client non present / commande non recuperee | Accueil / Administration | + +### Invariants de la machine a etats + +- `delivered` et `cancelled` sont des etats **finaux** : aucune transition n'en + sort. +- Aucune transition ne revient en arriere (pas de `preparing -> paid`). Une + erreur operationnelle se traite par annulation puis nouvelle commande, pour + preserver l'integrite de l'historique et des snapshots de prix. +- La transition vers `cancelled` est possible depuis tous les etats **sauf** + `delivered` (une commande remise ne s'annule pas dans ce modele). Ceci est + coherent avec `mcd.md` section 9 : "Annuler : transition vers `cancelled` + (depuis tout statut sauf `delivered`)". +- `paye_a` (DATETIME, `dictionary.md` 3.5) est renseigne au moment de la + transition T2 (`pending_payment -> paid`) et reste NULL avant. + +--- + +## 6. Coherence avec les autres livrables + +| Verification | Resultat | +|---|---| +| Tous les etats du diagramme existent dans l'ENUM `dictionary.md` 3.5 | Oui (6 valeurs, toutes utilisees) | +| La regle "annulation possible sauf depuis delivered" de `mcd.md` 9 | Respectee (T5, T7, T9 ; pas de transition depuis `delivered`) | +| Cycle de vie esquisse dans `mcd.md` 9 | Couvert : `pending_payment` -> `paid` (payer), `paid` -> `preparing` (preparer), `preparing` -> `ready` (marquer pret), `ready` -> `delivered` (remettre) | +| Acteurs de `use-cases.md` | Preparation declenche T4/T6/T7 ; Accueil declenche T8/T9 ; Administration peut annuler | + +--- + +## 7. Arbitrage tranche + +La divergence historique entre l'ENUM du dictionnaire et un parcours sans +paiement est resolue par la regle metier confirmee : le cycle de vie comporte +deux phases successives, la composition de la commande puis son paiement. Le +paiement fait partie integrante du cycle. + +La machine canonique retenue est donc : + +``` +pending_payment -> paid -> preparing -> ready -> delivered + (cancelled atteignable depuis pending_payment, paid, preparing) +``` + +Cette machine est la source de verite partagee par `dictionary.md` 3.5, +`use-cases.md` (cas "Payer la commande" cote Client) et +`sequence-passer-commande.md` (etape paiement entre validation du panier et +confirmation). La colonne `paye_a` est renseignee a la transition T2. A +revalider lors du MCT. diff --git a/docs/uml/use-cases.md b/docs/uml/use-cases.md new file mode 100644 index 0000000..c9897be --- /dev/null +++ b/docs/uml/use-cases.md @@ -0,0 +1,222 @@ +# Diagramme de cas d'utilisation - Wakdo + +**Phase UML** : P1 - Conception, complement UML (apres MCD) +**Statut** : v0.1 +**Date** : 2026-05-21 +**Branche** : `feat/p1-conception` +**Auteur methodologie** : BYAN + +--- + +## 1. Objet du document + +Ce document recense les **cas d'utilisation** de Wakdo, c'est-a-dire les +fonctionnalites observables du systeme du point de vue de ses acteurs. Il +complete le MCD (`docs/merise/mcd.md`) et le dictionnaire +(`docs/merise/dictionary.md`) en passant de la vue **donnees** a la vue +**usages**. + +Le diagramme reste au niveau conceptuel. Il ne prejuge pas de l'ecran ou de +l'endpoint qui realisera chaque cas, mais identifie qui fait quoi. + +**Sources** : +- `docs/PROJECT_CONTEXT.md` sections 2 (acteurs, processus), 7 (scope RBAC) +- `docs/merise/dictionary.md` (entites `commande`, `role`, `user`) + +--- + +## 2. Acteurs - perimetre et challenge de pertinence + +Le brief (`PROJECT_CONTEXT.md` section 2 et section 7) definit les acteurs +metier. Avant de les retenir, chaque acteur propose dans la consigne initiale +est confronte au perimetre reel du projet. + +| Acteur candidat | Statut | Justification (perimetre reel) | +|---|---|---| +| **Client (borne kiosk)** | Retenu | Acteur central du Bloc 1. Compose et valide une commande sur la borne tactile autonome (canal `kiosk`). Non authentifie. | +| **Manager / Admin** | Retenu, fusionne en **Administration** | Le brief ne distingue pas "manager" et "admin" comme deux roles. Le role RBAC reel est `admin` (section 7). Il porte le CRUD catalogue, la gestion des utilisateurs/roles et les stats. On nomme l'acteur **Administration** pour coller au vocabulaire du brief. | +| **Cuisine** | Retenu, renomme **Preparation** | Correspond au role RBAC `preparation` (section 7). Voit la file des commandes a preparer triees par heure de livraison croissante et fait avancer leur statut. Le terme "Cuisine" est un synonyme metier ; le role technique est `preparation`. | +| **Caisse** | Ecarte comme acteur distinct | Challenge : il n'existe pas de role RBAC `caisse` (les 3 roles sont `admin`, `preparation`, `accueil`). Le paiement existe dans le cycle (cote Client sur la borne et cote Accueil au comptoir/drive), mais aucun acteur "Caisse" dedie n'est modelise. L'equivalent operationnel le plus proche est l'**Accueil** (role `accueil`) qui saisit les commandes au comptoir/drive et remet les commandes livrees. | +| **Accueil** | Retenu (non liste dans la consigne mais present au brief) | Role RBAC `accueil`. Saisit les commandes au comptoir (canal `counter`) ou au drive (canal `drive`), puis remet les commandes au client (passage a `delivered`). C'est l'acteur qui recouvre le besoin que la consigne attribuait a "Caisse". | + +### Decision sur les acteurs retenus + +Quatre acteurs sont conserves au diagramme : + +1. **Client** (borne, non authentifie) +2. **Administration** (role `admin`) +3. **Preparation** (role `preparation`, ex-"Cuisine") +4. **Accueil** (role `accueil`, recouvre le besoin "Caisse") + +> Decision actee : il n'y a **pas** de parcours employe dedie modelise a part. +> Les cas d'usage des employes (Administration, Preparation, Accueil) sont +> couverts directement ici. Cette decision suit le mantra du Rasoir d'Ockham +> (#37) : on evite une couche de modelisation redondante tant qu'aucun besoin +> ne la justifie. + +--- + +## 3. Diagramme de cas d'utilisation + +Mermaid ne fournit pas de type `usecase` natif. La representation ci-dessous +utilise un `flowchart` : les acteurs sont des noeuds a gauche, les cas +d'utilisation sont des noeuds arrondis regroupes par sous-systeme, et les +fleches portent les relations (`<>`, `<>`) la ou elles +ont du sens. + +```mermaid +flowchart LR + %% Acteurs + Client(("Client
borne kiosk")) + Admin(("Administration
role admin")) + Prep(("Preparation
role preparation")) + Accueil(("Accueil
role accueil")) + + %% Sous-systeme Borne client + subgraph BORNE["Borne client - Bloc 1"] + UC1(["Consulter le catalogue"]) + UC2(["Composer un menu"]) + UC3(["Passer une commande"]) + UC4(["Saisir le numero de retrait"]) + UC5(["Recevoir la confirmation"]) + UC6(["Payer la commande"]) + end + + %% Sous-systeme Back-office + subgraph BACK["Back-office - Bloc 2"] + UC10(["Gerer le catalogue
categories, produits, menus"]) + UC11(["Gerer les utilisateurs et roles"]) + UC12(["Consulter les statistiques"]) + UC20(["Consulter la file de preparation"]) + UC21(["Faire avancer une commande"]) + UC30(["Saisir une commande
comptoir ou drive"]) + UC31(["Remettre la commande au client"]) + UC40(["S'authentifier"]) + end + + %% Relations Client + Client --> UC1 + Client --> UC2 + Client --> UC3 + Client --> UC6 + Client --> UC5 + + %% include / extend cote borne + UC3 -. include .-> UC4 + UC3 -. include .-> UC6 + UC2 -. include .-> UC1 + UC3 -. extend .-> UC2 + + %% Relations Administration + Admin --> UC40 + Admin --> UC10 + Admin --> UC11 + Admin --> UC12 + + %% Relations Preparation + Prep --> UC40 + Prep --> UC20 + Prep --> UC21 + + %% Relations Accueil + Accueil --> UC40 + Accueil --> UC30 + Accueil --> UC31 + UC30 -. include .-> UC1 + + %% Authentification mutualisee + UC10 -. include .-> UC40 + UC11 -. include .-> UC40 + UC20 -. include .-> UC40 + UC30 -. include .-> UC40 +``` + +--- + +## 4. Description des cas d'utilisation + +### 4.1 Acteur Client (borne kiosk) + +| Cas | Description | Entites manipulees | +|---|---|---| +| Consulter le catalogue | Parcourir les categories, produits et menus disponibles. Charges via `GET /api/categories`, `/api/products`, `/api/menus` (ou JSON fallback). | `categorie`, `produit`, `menu` | +| Composer un menu | Choisir burger + accompagnement + boisson + sauce, regler les options de taille (normale / grande) et de personnalisation. Etend "Passer une commande" car un menu compose est une variante d'item au panier. | `menu`, `menu_produit`, `produit` | +| Passer une commande | Valider le panier, declencher la creation de la commande composee. Inclut la saisie du numero de retrait et le paiement. | `commande`, `ligne_commande` | +| Saisir le numero de retrait | Renseigner le numero qui identifie le client au comptoir. Cas inclus par "Passer une commande". | `commande.numero` | +| Payer la commande | Regler la commande une fois le panier compose et valide. Materialise la transition `pending_payment -> paid` de `state-commande.md`. Cas inclus par "Passer une commande". | `commande.statut`, `commande.paye_a` | +| Recevoir la confirmation | Afficher l'ecran de confirmation avec le numero, apres paiement. | `commande` | + +> Note de coherence : le cycle de vie comporte deux phases successives, la +> composition de la commande puis son paiement (regle metier confirmee). Le cas +> "Payer la commande" est retenu cote Client et materialise la transition +> `pending_payment -> paid` de l'ENUM `statut` +> (`dictionary.md` 3.5, `state-commande.md`). + +### 4.2 Acteur Administration (role admin) + +| Cas | Description | Entites manipulees | +|---|---|---| +| Gerer le catalogue | CRUD sur categories, produits et menus (libelles, prix, images, disponibilite, composition de menu). | `categorie`, `produit`, `menu`, `menu_produit` | +| Gerer les utilisateurs et roles | CRUD sur les comptes back-office et leurs roles ; consultation de la matrice de permissions. | `user`, `role`, `permission`, `role_permission` | +| Consulter les statistiques | Voir les commandes du jour de service, le chiffre d'affaires, les produits les plus commandes. | `commande`, `ligne_commande` | + +### 4.3 Acteur Preparation (role preparation, ex-Cuisine) + +| Cas | Description | Entites manipulees | +|---|---|---| +| Consulter la file de preparation | Afficher les commandes a preparer triees par heure de livraison croissante, tous canaux confondus. | `commande`, `ligne_commande` | +| Faire avancer une commande | Declarer une commande "preparee", ce qui declenche une transition de statut (voir `state-commande.md`). | `commande.statut` | + +### 4.4 Acteur Accueil (role accueil, recouvre Caisse) + +| Cas | Description | Entites manipulees | +|---|---|---| +| Saisir une commande | Creer une commande pour un client au comptoir (`counter`) ou au drive (`drive`), en consultant le catalogue. | `commande`, `ligne_commande`, `produit`, `menu` | +| Remettre la commande au client | Declarer une commande "livree" au moment de la remise physique. | `commande.statut` | + +### 4.5 Cas transverse - S'authentifier + +Tous les acteurs du back-office (Administration, Preparation, Accueil) passent +par "S'authentifier" avant d'acceder a leurs cas. Modelise comme cas inclus +(`<>`) par chaque cas back-office pour eviter de surcharger le +diagramme. Le Client de la borne n'est pas authentifie (canal `kiosk` public). + +--- + +## 5. Relations include / extend retenues + +| Relation | Type | Justification | +|---|---|---| +| Passer une commande -> Saisir le numero de retrait | include | La saisie du numero fait partie integrante de toute validation de commande. | +| Passer une commande -> Payer la commande | include | Le paiement suit la composition du panier et fait partie integrante du parcours (phase 2 du cycle de vie). | +| Composer un menu -> Consulter le catalogue | include | Composer un menu suppose de parcourir les produits eligibles a chaque slot. | +| Passer une commande -> Composer un menu | extend | Le menu est un cas optionnel : une commande peut ne contenir que des produits a la carte. La composition etend le parcours seulement si le client choisit un menu. | +| Saisir une commande (Accueil) -> Consulter le catalogue | include | L'equipier consulte le catalogue pour saisir au comptoir / drive. | +| Cas back-office -> S'authentifier | include | Acces conditionne a une session authentifiee. | + +--- + +## 6. Incoherences remontees vers les autres livrables + +Ces ecarts entre les sources sont signales pour arbitrage de l'auteur (la +modelisation finale releve de sa decision, mantra de validation humaine). + +1. **ENUM `statut` et phase de paiement (tranche)** + Le dictionnaire (`dictionary.md` 3.5) definit + `statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled')` + avec un paiement explicite. La regle metier confirmee fixe deux phases + successives, la composition de la commande puis son paiement. Le cas + "Payer la commande" est donc retenu cote Client et materialise la transition + `pending_payment -> paid`. Cet ecart est tranche : la machine canonique de + `state-commande.md` fait foi. + +2. **Acteur "Caisse" absent du RBAC** + Aucun role `caisse` n'existe (`PROJECT_CONTEXT.md` section 7 : `admin`, + `preparation`, `accueil`). La fonction d'encaissement de la consigne a ete + rattachee a l'acteur **Accueil**. A confirmer. + +3. **"Manager" vs "Admin"** + La consigne parle de "Manager/Admin" ; le brief ne connait que `admin`. Les + deux ont ete fusionnes en un acteur **Administration**. A confirmer si un + role manager intermediaire est souhaite (le dictionnaire 3.8 mentionne un + role `manager` extensible, non present dans le scope section 7). From de355da54c4fd8cff70cc193efa51707365821a3 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 4 Jun 2026 10:19:25 +0000 Subject: [PATCH 3/8] docs: journal entry for 2026-06-04 prod-like conception decisions Records the alignment review of all project docs and the point-by-point decision session: drop commande_event, English naming convention, VAT carried by product (after BOFiP fact-check), real menu customization, full ingredient configurator, allergen modal. Lists open points D4-D8. --- ...026-06-04--conception-prodlike-revision.md | 121 ++++++++++++++++++ docs/journal/README.md | 1 + 2 files changed, 122 insertions(+) create mode 100644 docs/journal/2026-06-04--conception-prodlike-revision.md diff --git a/docs/journal/2026-06-04--conception-prodlike-revision.md b/docs/journal/2026-06-04--conception-prodlike-revision.md new file mode 100644 index 0000000..6a3e33c --- /dev/null +++ b/docs/journal/2026-06-04--conception-prodlike-revision.md @@ -0,0 +1,121 @@ +# Conception P1 — revue d'alignement + revision prod-like du modele de donnees + +**Date** : 2026-06-04 +**Branche** : `feat/p1-conception` +**PR** : a venir (apres reecriture des docs Merise) +**Duree estimee** : session de decision (point par point) + +--- + +## Ce qui a ete fait + +1. **Revue d'alignement complete** de tous les `.md` du projet (PROJECT_CONTEXT, dictionary, mcd, mct, mlt, mld, UML, les 13 notes de `docs/notes/`, le journal) pour verifier que la conception P1 ne derive pas du cadrage. Synthese dans `docs/notes/revue-alignement-p1.md` (non versionne). +2. **Session de decision point par point** sur le modele de donnees. Les decisions ci-dessous remplacent ou precisent plusieurs choix du dictionnaire / MCD / MLD v0.1. +3. **Principe directeur acte** : le produit vise est **prod-like, pas MVP**. Tout ce qui est decide est implemente dans le livrable final. + +Aucune reecriture des docs Merise n'a encore ete faite : cette session fige les decisions, la propagation dans les 5 docs Merise + PROJECT_CONTEXT se fera en une passe une fois les points D4-D8 tranches. + +--- + +## Pourquoi — decisions et alternatives + +### Decision 1 — Suppression de `commande_event`, traçabilite par timestamps de phase + +- **Decision** : abandon de la table d'audit append-only `commande_event`. La traçabilite passe par `commande.status` (etat courant) + une colonne `DATETIME` par phase (`paid_at`, `preparing_at`, `ready_at`, `delivered_at`), plus `created_at`. +- **Alternatives** : (A) garder `commande_event` ; (B) colonnes `created_by_user_id` denormalisees ; (C) retrait total de la traçabilite. +- **Raison** : en restaurant, le compte back-office est **partage par poste** (cuisine, accueil), pas individuel. L'attribution par personne n'a donc pas de valeur. Le besoin reel est : compter par canal et mesurer les **durees entre phases**. Les timestamps par phase couvrent durees + heures de la journee (stats `service_day`, KPI), sans la complexite d'un journal d'evenements. Mantra #37 (Ockham). + +### Decision 2 — Convention de nommage anglaise, par couche + +- **Decision** : tout en anglais. BDD en `snake_case`, classes PHP en `PascalCase`, methodes/proprietes PHP et JS en `camelCase`, JSON/API en `camelCase`. +- **Alternatives** : conserver le francais (dictionnaire v0.1) ; `camelCase` jusque dans les noms de tables SQL. +- **Raison** : le `snake_case` reste la convention courante en SQL et evite les pieges de sensibilite a la casse des noms de tables sous Linux/Docker (deja rencontres en Session 4 sur les noms d'images). Le `camelCase` reste la ou il est standard (code PHP/JS). Resout le point D3 (valeur `source` : `comptoir` -> `counter`). + +### Decision 3 — Machine a etats avec phase de paiement + +- **Decision** : conservation de la machine deux phases `pending_payment -> paid -> preparing -> ready -> delivered` (+ `cancelled`). Transition `pending_payment -> paid` atomique a la creation dans le cadre RNCP (saisie du numero = substitut de paiement). +- **Raison** : une borne fast-food reelle encaisse a la borne ; modeliser la phase paiement reflete le metier et laisse la porte ouverte a un vrai paiement sans migration destructive. Cout d'une valeur d'ENUM. + +### Decision 4 — TVA portee par le produit, pas par le mode de consommation (apres fact-check) + +- **Decision** : la TVA devient un attribut du produit (`vat_rate`), 10 % par defaut, 5,5 % sur les items en contenant conservable (eau, jus en bouteille). La TVA de la commande se calcule ligne par ligne, taux snapshote sur `order_item`. `mode_consommation` est renomme `service_mode` et conserve **uniquement** pour les stats/KPI (sur place / a emporter / drive), sans role fiscal. +- **Alternative ecartee** : la regle initiale du dictionnaire « 10 % sur place / 5,5 % a emporter ». +- **Raison** : fact-check de la regle initiale contre la doctrine fiscale officielle. Resultat : le taux depend de la **nature consommation immediate vs differee**, pas du mode sur place / a emporter. Voir bloc FACT-CHECK ci-dessous. + +``` +FACT-CHECK +Claim audite : "TVA 10% sur place / 5,5% a emporter" (dictionnaire note 9, mlt RG-2) +Domaine : compliance (fiscal) +Verdict : le claim initial est INEXACT +Source : BOFiP BOI-ANNX-000495 + BOI-TVA-LIQ-30-10-10 (doctrine officielle impots.gouv.fr) +Regle reelle : 10% pour la consommation immediate (sur place OU a emporter) ; + 5,5% pour les produits en contenant conservable (bouteille, canette) / consommation differee +Confiance : 95% (L1, texte officiel) +``` + +### Decision 5 (D1) — Personnalisation reelle des menus + +- **Decision** : un menu est bati autour d'un **burger fixe** ; le client choisit accompagnement, boisson, sauce. Format **Normal / Maxi** au niveau du menu, qui fait basculer accompagnement + boisson en grande taille et change le prix (deux prix par menu : `price_normal_cents`, `price_maxi_cents`). A la carte, la taille existe la ou la donnee la porte (frites, potatoes). Modele relationnel : table `menu_slot` (emplacements a choix) + `order_item_selection` (choix du client). +- **Alternative ecartee** : stocker les choix en bloc JSON ; menu combo fige. +- **Raison** : une borne sans choix reel ne reflete pas le metier. Le relationnel est interrogeable (stats KPI : boisson la plus prise, % grandes tailles) et plus defendable au jury Bloc 2 que du JSON opaque. +- **Calibrage prix Maxi** : le supplement Maxi est derive de la donnee (Grande Frite 3,50 − Moyenne Frite 2,75 = 0,75) plus un upsize boisson comparable, soit ~1,50 €. Cross-check marche reel (McDonald's France, ecart Best Of -> Maxi Best Of ~1,50-2 € en 2026) : coherent. Wakdo etant un pastiche fictif, on derive de la donnee plutot que copier les prix reels. + +### Decision 6 (D2) — Configurateur d'ingredients complet + +- **Decision** : personnalisation au niveau ingredient (retirer = gratuit, ajouter = supplement) sur **tous les sandwichs composes** (burgers, wraps, cheeseburger), aussi bien a la carte que dans un menu. Tables : `ingredient`, `product_ingredient` (composition par defaut + retirable + ajoutable + supplement), `order_item_modifier` (modifications a la commande). +- **Alternatives** : note texte libre ; jeu d'options legeres ; report post-MVP. +- **Raison** : choix prod-like assume par l'auteur. Les compositions reelles seront saisies en seed (recuperees publiquement, coherent avec un catalogue deja calque sur des produits connus). + +### Decision 7 — Modal allergenes derivee des ingredients + +- **Decision** : table `allergen` + `ingredient_allergen`. Les allergenes d'un produit sont **calcules** par jointure sur sa composition, sans ressaisie manuelle. Affichage en modal sur la borne pour chaque produit. +- **Raison** : reutilise la donnee ingredient (Decision 6) sans duplication ; coherence garantie. Aligne avec le reglement INCO (UE) 1169/2011 (declaration des 14 allergenes ; liste officielle a confirmer au seed). Nourrit l'accessibilite du Bloc 1. + +--- + +## Comment — points techniques cles + +- **Taille / format unifies** : une notion `normal` / `maxi`. A la carte, la taille existe via des produits distincts (la donnee ecole modelise « Petite/Moyenne/Grande Frite » comme 3 produits). En menu, le format est un attribut de la ligne menu qui cascade sur les composants (pas de prix individuel, compris dans le prix combo). +- **Snapshots** : prix unitaire ET taux TVA sont snapshotes sur `order_item` au moment de la commande (integrite historique, meme logique que le snapshot de libelle deja prevu). +- **Personnalisation du burger dans un menu** : les modifications (`order_item_modifier`) doivent pouvoir s'attacher au burger qu'il soit pris seul ou comme burger fixe d'un menu. Materialisation a preciser au DDL. +- **Couleurs KDS back-office** : calculees a l'affichage (`maintenant − paid_at` vs seuil SLA global ~10 min en config), aucune donnee supplementaire a stocker. + +--- + +## Criteres RNCP couverts + +- **Bloc 2 - Cr 3.a / 3.b** : analyse et modelisation des donnees (dictionnaire, MCD, MLD), passage relationnel, contraintes referentielles, polymorphisme, snapshots. +- **Bloc 2 - Cr 3.d** : la TVA correcte et le calcul ligne par ligne demontrent la rigueur sur la donnee. +- **Bloc 1 - Cr 1.c** : la modal allergenes renforce l'accessibilite / l'information utilisateur. +- **Compliance / fact-check** : la regle TVA est sourcee L1 (BOFiP), conforme au protocole `.claude/rules/fact-check.md`. + +--- + +## Questions anticipees du jury + +- **Q** : "Pourquoi avoir abandonne le journal d'evenements de commande ?" + **R** : Le compte back-office est partage par poste, donc l'attribution individuelle d'une transition n'a pas de valeur metier. Le besoin reel (durees entre phases, heures) est couvert par des timestamps par phase sur la commande, sans la complexite d'un event store. + +- **Q** : "Vous appliquez 5,5 % a l'emporter ?" + **R** : Non. Apres verification du BOFiP, le taux depend de la consommation immediate ou differee, pas du mode sur place / a emporter. En fast-food, ce qui est chaud / en gobelet est a 10 % dans les deux cas ; le 5,5 % concerne les contenants conservables (bouteille, canette). La TVA est donc portee par le produit. + +- **Q** : "Comment gerez-vous les allergenes sans les ressaisir pour chaque produit ?" + **R** : Ils sont modelises au niveau ingredient. Les allergenes d'un produit sont calcules par jointure sur sa composition. Modifier un ingredient met a jour tous les produits concernes. + +--- + +## Points d'amelioration conscients + +- **Scope volontairement etendu** : le modele passe de ~11 a ~16 entites (configurateur d'ingredients + allergenes + selections de menu). Choix prod-like assume. Consequence : `PROJECT_CONTEXT` §7 (scope, mot « MVP » a retirer, items a deplacer en IN scope) et §11 (planning / budget heures) sont a rechiffrer pour rester honnetes. +- **Docs Merise a reecrire** : dictionary, mcd, mct, mlt, mld doivent etre repris en une passe (anglais, 16 entites, prod-like) une fois les decisions restantes tranchees. Reecriture differee volontairement pour ne pas toucher ces docs deux fois. +- **Decisions encore ouvertes** (a trancher avant la reecriture) : D4 (liste des roles unifiee), D5 (vocabulaire des permissions), D6 (correction de la formule `service_day` — coupure a 10h, pas 4h30), D7 (subnet Docker : doc vs realite), D8 (prefixe du numero de commande). +- **Diagrammes** : MCD et MLD a regenerer pour refleter le modele a 16 entites. + +--- + +## Liens vers artefacts + +- Revue d'alignement : `docs/notes/revue-alignement-p1.md` (non versionne) +- Docs impactes a venir : `docs/merise/{dictionary,mcd,mct,mlt,mld}.md`, `docs/PROJECT_CONTEXT.md` +- Sources fact-check TVA : BOFiP BOI-ANNX-000495, BOI-TVA-LIQ-30-10-10 (impots.gouv.fr) +- Reference prix Maxi : mcdonalds.fr (menus Best Of / Maxi Best Of), cross-check de magnitude uniquement diff --git a/docs/journal/README.md b/docs/journal/README.md index 45d5053..0b8b5f8 100644 --- a/docs/journal/README.md +++ b/docs/journal/README.md @@ -30,6 +30,7 @@ Les fichiers sont ordonnes chronologiquement par leur nom. | 2026-04-23 | [cadrage-projet](2026-04-23--cadrage-projet.md) | Analyse brief RNCP, decisions d'architecture, bootstrap Git | `main` (commit initial) | | 2026-04-24 | [infra-docker](2026-04-24--infra-docker.md) | Stack Docker complete (compose + 4 services), referentiel RNCP integre, cross-check mappings Cr 4.f | `feat/infra-docker` | | 2026-04-30 | [smoke-test-infra](2026-04-30--smoke-test-infra.md) | Smoke test bout-en-bout sur serveur reel : fusion .env, switch FQDN sur stark.a3n.fr, subnet explicite RFC 1918, fix init cron + healthz | `feat/infra-docker` | +| 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` | *Mis a jour a chaque nouvelle entree.* From 6ceebf7fb17b02bca71bf61efc3aed7c38b1fa46 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 4 Jun 2026 15:17:33 +0000 Subject: [PATCH 4/8] docs(merise): rewrite data dictionary to prod-like v0.2 (19 entities, English) Drop commande_event and menu_produit. Add ingredient configurator (ingredient, product_ingredient, allergen, ingredient_allergen), numeric stock (stock_movement), customizable menus (menu_slot, menu_slot_option, order_item_selection, order_item_modifier), RBAC role attributes (default_route, order_source) and role_visible_source. VAT carried by product (vat_rate), 4-state order machine, English snake_case naming. Decisions D1-D8 + stock. --- docs/merise/dictionary.md | 1051 +++++++++++++++++++++++-------------- 1 file changed, 664 insertions(+), 387 deletions(-) diff --git a/docs/merise/dictionary.md b/docs/merise/dictionary.md index 326876e..0dcbf80 100644 --- a/docs/merise/dictionary.md +++ b/docs/merise/dictionary.md @@ -1,508 +1,785 @@ -# Dictionnaire de donnees - Wakdo +# Data Dictionary — Wakdo -**Phase Merise** : P1 - Conception, etape 1 (data dictionary first, mantra #33) -**Statut** : v0.1 (squelette MCD a venir, mantra "Incremental Design") -**Date** : 2026-04-30 -**Branche** : `feat/p1-stubs-and-dictionary` +**Merise phase** : P1 - Conception, step 1 (data dictionary first, mantra #33) +**Version** : v0.2 — prod-like, 19 entities +**Date** : 2026-06-04 +**Branch** : `feat/p1-conception` +**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7) +**Author** : BYAN (methodology layer) --- -## 1. Objet du document +## 1. Purpose -Ce dictionnaire liste **toutes les entites de donnees** identifiees pour Wakdo, avec -leurs attributs, types, contraintes et sources. Il sert de base au MCD (entites + relations), -puis au MLD (passage relationnel), puis au DDL (SQL CREATE TABLE). +This dictionary lists **all data entities** identified for Wakdo, with their attributes, +types, constraints, and sources. It serves as the basis for the MCD (entities + relations), +then the MLD (relational mapping), then the DDL (SQL CREATE TABLE). -**Methodologie** : derivation bottom-up depuis les sources disponibles : -- **Source ecole** : `docs/merise/_sources/categories.json` + `produits.json` (66 produits, 9 categories) -- **Brief metier** : `docs/PROJECT_CONTEXT.md` (composition de menu, parcours commande, RBAC, - modes de consommation) -- **Maquette** : `docs/design/maquette-borne.pdf` (UX kiosk, ecrans visibles) +**Methodology**: bottom-up derivation from available sources: +- **School source**: `docs/merise/_sources/categories.json` + `produits.json` + (66 products, 9 categories) +- **Business brief**: `docs/PROJECT_CONTEXT.md` (menu composition, order flow, RBAC, + service modes) +- **Mockup**: `docs/design/maquette-borne.pdf` (kiosk UX, visible screens) -Tout ecart entre la source ecole et le modele final est documente dans la section "Notes -de modelisation" en bas de ce document. +All deviations between school source and final model are documented in the +"Modeling notes" section at the bottom of this document. + +For the entity-relationship diagram and cardinality justifications, see [`mcd.md`](mcd.md). +This dictionary does not duplicate that view to avoid diverging sources of truth. --- -## 2. Conventions generales +## 2. General conventions ### Naming -- **Tables** : `snake_case` au singulier (ex : `categorie`, `produit`, `menu_produit`). - Le singulier reflete la perspective "1 ligne = 1 instance de l'entite" (convention courante - dans les ecoles francaises de gestion). Le code applicatif (PHP, JS) utilisera ces noms - tels quels. -- **Colonnes** : `snake_case`. Suffixes typiques : `_id` (FK), `_at` (timestamp), `_cents` - (montant monetaire en centimes), `_path` (chemin de fichier), `_taux` (pourcentage ou - fraction). -- **Cles primaires** : colonne `id` (INT UNSIGNED AUTO_INCREMENT). Pas de cle composite en - PK, sauf sur les tables de jointure pure. -- **Cles etrangeres** : `_id` (ex : `categorie_id` dans `produit`). +- **Tables**: `snake_case`, singular (e.g., `category`, `product`, `customer_order`). + Singular reflects the perspective "1 row = 1 instance of the entity" (standard relational + convention). Application code (PHP, JS) uses these names as-is via ORM mapping. +- **Columns**: `snake_case`. Typical suffixes: `_id` (FK), `_at` (timestamp), + `_cents` (monetary amount in integer cents), `_path` (file path), `_rate` (rate or + fraction stored as per-mille integer). +- **Primary keys**: column `id` (INT UNSIGNED AUTO_INCREMENT). No composite PK except + on pure join tables. +- **Foreign keys**: `_id` (e.g., `category_id` in `product`). +- **ENUM values**: English, snake_case (e.g., `pending_payment`, `dine_in`, `kiosk`). +- **Code-facing strings** (ENUM, permission codes, role codes): English only, consistent + across DB, PHP, and JSON API. -### Types par defaut +### Default types -| Categorie | Type MariaDB | Justification | +| Category | MariaDB type | Justification | |---|---|---| -| Identifiants | `INT UNSIGNED AUTO_INCREMENT` | 4 milliards d'ids = largement suffisant pour ce projet | -| Libelles courts | `VARCHAR(120)` | Couvre la plupart des noms produits (ex : `"Signature Beef BBQ Burger (2 viandes)"` = 41 chars) | -| Descriptions | `TEXT` | Longueur variable, pas de limite stricte | -| Montants monetaires | `INT UNSIGNED` (centimes) | Evite les bugs d'arrondi des FLOAT (cf. note 1 en bas) | -| Booleens | `TINYINT(1)` | Convention MariaDB pour `BOOLEAN` (alias) | -| Timestamps | `DATETIME` | Lisible humainement, gere les timezones via app | -| Enumerations | `ENUM('a','b','c')` | Contrainte SGBD, lisible (cf. note 2) | -| Chemins de fichiers | `VARCHAR(255)` | Limite POSIX courante pour un chemin simple | +| Identifiers | `INT UNSIGNED AUTO_INCREMENT` | 4 billion ids — sufficient for this project | +| Short labels | `VARCHAR(120)` | Covers most product names (max observed: 41 chars in school source) | +| Descriptions | `TEXT` | Variable length, no strict limit | +| Monetary amounts | `INT UNSIGNED` (cents) | Avoids FLOAT rounding bugs (see note 1) | +| Booleans | `TINYINT(1)` | MariaDB convention for `BOOLEAN` (alias) | +| Timestamps | `DATETIME` | Human-readable, timezone handled at app layer | +| Enumerations | `ENUM('a','b','c')` | DBMS-level constraint, readable (see note 2) | +| File paths | `VARCHAR(255)` | Standard POSIX path length limit | -### Charset et collation +### Charset and collation -- **Charset** : `utf8mb4` (RFC 3629 - UTF-8 reel sur 4 octets, supporte les emoji et caracteres - asiatiques). MariaDB gere `utf8mb4` en natif. -- **Collation** : `utf8mb4_unicode_ci` (insensible a la casse, comparaison conforme Unicode). +- **Charset**: `utf8mb4` (RFC 3629 — real 4-byte UTF-8, supports emoji and Asian characters). + MariaDB handles `utf8mb4` natively. +- **Collation**: `utf8mb4_unicode_ci` (case-insensitive, Unicode-compliant comparison). -### Champs d'audit (presents sur toutes les tables metier sauf jointures pures) +### Audit fields (present on all business tables except pure join tables) -| Colonne | Type | Defaut | Role | +| Column | Type | Default | Role | |---|---|---|---| -| `created_at` | `DATETIME` | `CURRENT_TIMESTAMP` | Date de creation, non modifiee par la suite (ecriture unique a l'insertion) | -| `updated_at` | `DATETIME` | `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` | Date de derniere modification, mise a jour automatique | +| `created_at` | `DATETIME` | `CURRENT_TIMESTAMP` | Creation timestamp, written once at insert | +| `updated_at` | `DATETIME` | `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` | Last modification timestamp, auto-updated | ### Soft delete -Pas de soft delete generalise pour MVP. Les entites qui peuvent etre desactivees temporairement -ont une colonne `est_actif` ou `est_disponible` (boolean). La suppression dure (`DELETE`) -reste possible mais reservee a des operations admin avec sauvegarde prealable. +No generalized soft delete. Entities that can be temporarily deactivated carry an +`is_active` or `is_available` boolean column. Hard `DELETE` remains possible but is +reserved for admin operations with prior backup. --- -## 3. Entites +## 3. Entities -### 3.1 `categorie` +### 3.1 `category` -Regroupement metier des produits et menus pour l'affichage sur la borne. +Business grouping of products and menus for display on the kiosk. -| Attribut | Type | NULL | Defaut | Contrainte | Source ecole | Notes | +| Attribute | Type | NULL | Default | Constraint | School source | Notes | |---|---|---|---|---|---|---| -| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | `id` (1-9) | identique source | -| `libelle` | VARCHAR(60) | NO | - | UNIQUE | `title` | renomme depuis `title` (semantique francaise) | -| `slug` | VARCHAR(60) | NO | - | UNIQUE | derive de `title` (kebab-case lowercase) | utile pour URL `/api/categories/burgers` | -| `image_path` | VARCHAR(255) | YES | NULL | - | `image` | normalisation post-import (kebab-case lowercase) | -| `ordre` | SMALLINT UNSIGNED | NO | 0 | - | (enrichi) | ordre d'affichage sur la borne, ajustable depuis admin | -| `est_actif` | TINYINT(1) | NO | 1 | - | (enrichi) | permet de desactiver une categorie sans la supprimer | -| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | audit | -| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | - | audit | +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | `id` (1-9) | same as source | +| `name` | VARCHAR(60) | NO | — | UNIQUE | `title` | renamed from `title` | +| `slug` | VARCHAR(60) | NO | — | UNIQUE | derived from `title` (kebab-case lowercase) | used for URL `/api/categories/burgers` | +| `image_path` | VARCHAR(255) | YES | NULL | — | `image` | relative path, see note 8 | +| `display_order` | SMALLINT UNSIGNED | NO | 0 | — | (added) | display order on kiosk, adjustable from admin | +| `is_active` | TINYINT(1) | NO | 1 | — | (added) | deactivate without deleting | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | — | audit | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | — | audit | -**Exemples** : `menus`, `boissons`, `burgers`, `frites`, `encas`, `wraps`, `salades`, -`desserts`, `sauces`. Volume : 9 lignes a l'init (seed depuis `categories.json`). +**Examples**: `menus`, `drinks`, `burgers`, `fries`, `snacks`, `wraps`, `salads`, +`desserts`, `sauces`. Volume: 9 rows at init (seed from `categories.json`). --- -### 3.2 `produit` +### 3.2 `product` -Article unitaire vendable a la carte ou comme composant d'un menu. +A single sellable item, available a la carte or as a component in a menu slot. -| Attribut | Type | NULL | Defaut | Contrainte | Source ecole | Notes | +| Attribute | Type | NULL | Default | Constraint | School source | Notes | |---|---|---|---|---|---|---| -| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | `id` (14-66 selon categorie) | identique source | -| `categorie_id` | INT UNSIGNED | NO | - | FK -> `categorie(id)`, ON DELETE RESTRICT | (enrichi : derive de la cle d'objet du JSON) | source absente, deduit de la position dans `produits.json` | -| `libelle` | VARCHAR(120) | NO | - | INDEX | `nom` | renomme depuis `nom` (coherence francaise) | -| `description` | TEXT | YES | NULL | - | (enrichi) | absente de la source ecole, alimente plus tard via admin | -| `prix_ttc_cents` | INT UNSIGNED | NO | - | CHECK > 0 | `prix` (FLOAT) | conversion FLOAT -> INT centimes au seed (cf. note 1) | -| `image_path` | VARCHAR(255) | YES | NULL | - | `image` | normalisation post-import | -| `est_disponible` | TINYINT(1) | NO | 1 | - | (enrichi) | rupture manuelle depuis admin (= booleen, pas de gestion stock numerique en MVP) | -| `ordre` | SMALLINT UNSIGNED | NO | 0 | - | (enrichi) | ordre dans la categorie | -| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | audit | -| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | - | audit | +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | `id` | same as source | +| `category_id` | INT UNSIGNED | NO | — | FK -> `category(id)`, ON DELETE RESTRICT | (derived from JSON object key) | | +| `name` | VARCHAR(120) | NO | — | INDEX | `nom` | renamed from `nom` | +| `description` | TEXT | YES | NULL | — | (added) | populated later via admin | +| `price_cents` | INT UNSIGNED | NO | — | CHECK > 0 | `prix` (FLOAT) | FLOAT -> INT cents conversion at seed (see note 1) | +| `vat_rate` | SMALLINT UNSIGNED | NO | 100 | CHECK IN (55, 100) | (added) | VAT rate in per-mille: 100 = 10%, 55 = 5.5%. Default 10%. See note 9 | +| `image_path` | VARCHAR(255) | YES | NULL | — | `image` | relative path, see note 8 | +| `is_available` | TINYINT(1) | NO | 1 | — | (added) | manual availability toggle from admin | +| `display_order` | SMALLINT UNSIGNED | NO | 0 | — | (added) | display order within category | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | — | audit | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | — | audit | -**Volume** : 53 lignes a l'init (66 lignes dans `produits.json` moins les 13 menus qui vont dans `menu`). Cf. note 3 pour la separation produit/menu. +**Volume**: ~53 rows at init (66 rows in `produits.json` minus 13 menus moved to `menu`). --- ### 3.3 `menu` -Combo prix fixe = burger + accompagnement + boisson + sauce (composition modelisee dans -`menu_produit`). +Fixed-price combo built around a specific burger, with customer-selectable slots +(drink, side, sauce). Two price tiers: Normal and Maxi. -| Attribut | Type | NULL | Defaut | Contrainte | Source ecole | Notes | +| Attribute | Type | NULL | Default | Constraint | School source | Notes | |---|---|---|---|---|---|---| -| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | `id` (1-13 dans categorie `menus`) | | -| `categorie_id` | INT UNSIGNED | NO | - | FK -> `categorie(id)`, ON DELETE RESTRICT | implicite (categorie `menus`) | | -| `libelle` | VARCHAR(120) | NO | - | INDEX | `nom` | ex : "Menu Le 280", "Menu Big Mac" | -| `description` | TEXT | YES | NULL | - | (enrichi) | | -| `prix_ttc_cents` | INT UNSIGNED | NO | - | CHECK > 0 | `prix` | | -| `image_path` | VARCHAR(255) | YES | NULL | - | `image` | reutilise typiquement l'image du burger dominant | -| `est_disponible` | TINYINT(1) | NO | 1 | - | (enrichi) | | -| `ordre` | SMALLINT UNSIGNED | NO | 0 | - | (enrichi) | | -| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | audit | -| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | - | audit | +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | `id` (1-13 in `menus` category) | | +| `category_id` | INT UNSIGNED | NO | — | FK -> `category(id)`, ON DELETE RESTRICT | implicit (category `menus`) | | +| `burger_product_id` | INT UNSIGNED | NO | — | FK -> `product(id)`, ON DELETE RESTRICT | (added) | the fixed burger that anchors this menu; drives ingredient customization | +| `name` | VARCHAR(120) | NO | — | INDEX | `nom` | e.g., "Menu Le 280" | +| `description` | TEXT | YES | NULL | — | (added) | | +| `price_normal_cents` | INT UNSIGNED | NO | — | CHECK > 0 | `prix` | Normal format price. Replaces single `prix_ttc_cents`. | +| `price_maxi_cents` | INT UNSIGNED | NO | — | CHECK > 0 | (added) | Maxi format price (~+150 cents vs normal; see note 7) | +| `image_path` | VARCHAR(255) | YES | NULL | — | `image` | typically reuses the burger image | +| `is_available` | TINYINT(1) | NO | 1 | — | (added) | | +| `display_order` | SMALLINT UNSIGNED | NO | 0 | — | (added) | | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | — | audit | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | — | audit | -**Volume** : 13 lignes a l'init. +**Volume**: 13 rows at init. Replaces the old fixed-composition `menu_produit` model. --- -### 3.4 `menu_produit` (jointure) +### 3.4 `menu_slot` -Composition d'un menu : pour chaque menu, la liste des produits avec leur role. +A selectable slot within a menu (e.g., "drink slot", "side slot", "sauce slot"). +Each slot constrains which products the customer can choose from, expressed via +the join table `menu_slot_option`. -| Attribut | Type | NULL | Defaut | Contrainte | Notes | -|---|---|---|---|---|---| -| `menu_id` | INT UNSIGNED | NO | - | FK -> `menu(id)`, ON DELETE CASCADE | | -| `produit_id` | INT UNSIGNED | NO | - | FK -> `produit(id)`, ON DELETE RESTRICT | RESTRICT pour eviter qu'un produit retire ne casse silencieusement les menus existants | -| `role` | ENUM('burger','accompagnement','boisson','sauce','dessert') | NO | - | - | role metier du produit dans le menu | -| `position` | SMALLINT UNSIGNED | NO | 0 | - | ordre d'affichage dans le menu (ex : burger en 1, frites en 2, etc.) | - -**Cle primaire** : composite `(menu_id, produit_id)`. - -**Volume estime** : 13 menus x 3-4 produits chacun = 40-50 lignes a l'init. - -**Decision YAGNI** : pas de colonne `quantite` (cf. discussion Session 5). Si un menu duo -arrivait, il serait modelise comme un nouveau menu distinct, ou la colonne serait ajoutee -via `ALTER TABLE` avec backfill. - ---- - -### 3.5 `commande` - -Transaction client : 1 commande = 1 panier valide a un instant donne. - -| Attribut | Type | NULL | Defaut | Contrainte | Notes | +| Attribute | Type | NULL | Default | Constraint | Notes | |---|---|---|---|---|---| | `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | -| `numero` | VARCHAR(20) | NO | - | UNIQUE | format humain ex : `K-2026-04-30-001`, genere a la creation | -| `source` | ENUM('kiosk','comptoir','drive') | NO | - | INDEX | canal de saisie de la commande (cf. note 8) | -| `mode_consommation` | ENUM('sur_place','a_emporter','drive') | NO | - | - | mode de consommation fiscal et operationnel (impacte la TVA, cf. note 9) | -| `statut` | ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') | NO | 'pending_payment' | INDEX | machine a etats (cf. MCT) | -| `total_ht_cents` | INT UNSIGNED | NO | - | CHECK >= 0 | snapshot calcule a la validation | -| `total_tva_cents` | INT UNSIGNED | NO | - | CHECK >= 0 | snapshot | -| `total_ttc_cents` | INT UNSIGNED | NO | - | CHECK > 0 | snapshot, doit valoir total_ht_cents + total_tva_cents (verification au MLT) | -| `tva_taux_pourmille` | SMALLINT UNSIGNED | NO | - | - | TVA en pour mille (ex : 100 pour 10%, 55 pour 5,5%). Stocke en INT pour eviter les arrondis FLOAT | -| `paye_a` | DATETIME | YES | NULL | - | timestamp du passage en `paid` (NULL avant) | -| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | INDEX | utilise pour les agregations stats live | -| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | audit | +| `menu_id` | INT UNSIGNED | NO | — | FK -> `menu(id)`, ON DELETE CASCADE | a slot belongs to exactly one menu | +| `name` | VARCHAR(80) | NO | — | — | e.g., "Drink", "Side", "Sauce" | +| `slot_type` | ENUM('drink','side','sauce','dessert','extra') | NO | — | — | semantic role of this slot | +| `is_required` | TINYINT(1) | NO | 1 | — | whether the customer must fill this slot | +| `display_order` | SMALLINT UNSIGNED | NO | 0 | — | order of display in the menu builder | -**Volume estime** : ~100-300 commandes/jour en pic, sur 6 mois de demo = ~10k lignes max. - -**TVA en restauration France** (cf. service-public.fr article F31407, 2024) : -- 10% sur la consommation immediate (sur place ou plats chauds a emporter) -- 5,5% sur les produits a emporter destines a la consommation differee - -Le taux est snapshote au moment de la commande pour preserver l'integrite historique -si la legislation evolue. +**No audit fields**: a slot is part of menu definition; created and updated with the menu. +**Composite index**: `(menu_id, display_order)`. --- -### 3.6 `ligne_commande` +### 3.5 `menu_slot_option` -Detail d'une commande : produits unitaires OU menus, avec snapshot prix et libelle au moment -de la transaction. +Eligible products for a given menu slot. Pure join table. -| Attribut | Type | NULL | Defaut | Contrainte | Notes | +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `menu_slot_id` | INT UNSIGNED | NO | — | FK -> `menu_slot(id)`, ON DELETE CASCADE | | +| `product_id` | INT UNSIGNED | NO | — | FK -> `product(id)`, ON DELETE RESTRICT | RESTRICT: removing a product must not silently break menus | + +**Primary key**: composite `(menu_slot_id, product_id)`. + +**Volume**: ~3-5 options per slot, ~3 slots per menu, 13 menus = ~120-200 rows at init. + +--- + +### 3.6 `ingredient` + +Elementary ingredient used in product composition. Carries stock data. + +| Attribute | Type | NULL | Default | Constraint | Notes | |---|---|---|---|---|---| | `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | -| `commande_id` | INT UNSIGNED | NO | - | FK -> `commande(id)`, ON DELETE CASCADE | si la commande disparait, ses lignes aussi | -| `type_item` | ENUM('produit','menu') | NO | - | - | discriminateur | -| `produit_id` | INT UNSIGNED | YES | NULL | FK -> `produit(id)`, ON DELETE RESTRICT | non-null SI type_item = 'produit' | -| `menu_id` | INT UNSIGNED | YES | NULL | FK -> `menu(id)`, ON DELETE RESTRICT | non-null SI type_item = 'menu' | -| `libelle_snapshot` | VARCHAR(120) | NO | - | - | copie du libelle au moment de la commande (preserve si on renomme) | -| `prix_unitaire_ttc_cents_snapshot` | INT UNSIGNED | NO | - | CHECK > 0 | copie du prix au moment de la commande | -| `quantite` | SMALLINT UNSIGNED | NO | 1 | CHECK > 0 | si le client commande 3 cocas, 1 ligne avec `quantite=3` | -| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | +| `name` | VARCHAR(120) | NO | — | UNIQUE | e.g., "Sesame Bun", "Cheddar Slice", "Ketchup Portion" | +| `unit` | VARCHAR(40) | NO | — | — | packaging unit label: piece / portion / sachet 1kg / pot / bottle (free-form label, not an ENUM — units vary per ingredient) | +| `stock_quantity` | INT | NO | 0 | CHECK >= 0 | current stock in units. Signed INT to allow negative detection (alert), but business rule enforces >= 0 | +| `pack_size` | SMALLINT UNSIGNED | NO | 1 | CHECK > 0 | units per restocking pack (e.g., 100 for a bag of 100 portions) | +| `pack_label` | VARCHAR(80) | YES | NULL | — | human label of the pack (e.g., "Sac 100 portions") | +| `low_stock_threshold` | SMALLINT UNSIGNED | NO | 0 | CHECK >= 0 | alert threshold: stock_quantity <= this value triggers low-stock indicator | +| `is_active` | TINYINT(1) | NO | 1 | — | deactivate obsolete ingredients without deleting | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | audit | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | audit | -**Contrainte CHECK applicative ou triggers** : -`(type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL) OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL)`. Cette contrainte est verifiable cote MariaDB -via CHECK (depuis 10.2) ou cote PHP au moment de l'insertion. - -**Volume** : ~3-5 lignes par commande -> 30k-50k lignes sur 6 mois. - -**Snapshots** : `libelle_snapshot` et `prix_unitaire_ttc_cents_snapshot` permettent de retrouver -la facturation exacte d'une commande historique meme si le produit a ete renomme/repricaye depuis. -Argumentaire jury : integrite des donnees comptables. +**Stock decrement rule**: at the `paid` transition, each ingredient is decremented by +`product_ingredient.quantity_normal` or `quantity_maxi` (selected by `order_item.format`) +multiplied by `order_item.quantity`, then adjusted by `order_item_modifier` rows. See note 7. +**Restocking rule**: `stock_quantity += N * pack_size` (restocked in full packs). +**Cancellation rule**: stock is re-credited when a `paid` order is cancelled. +**Low-stock alert**: computed at display time (`stock_quantity <= low_stock_threshold`); +no additional stored column. --- -### 3.7 `commande_event` +### 3.7 `product_ingredient` -Journal d'audit append-only : 1 ligne par changement d'etat d'une commande. Pattern -event sourcing simplifie (cf. note 10). Trace **qui** a fait **quoi**, **quand**, sur quelle -commande, avec quel contexte. Aucun update / delete autorise (immuable). +Default composition of a product (burger, wrap, etc.) in terms of ingredients. +Carries customization metadata for the ingredient configurator. -| Attribut | Type | NULL | Defaut | Contrainte | Notes | +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `product_id` | INT UNSIGNED | NO | — | FK -> `product(id)`, ON DELETE CASCADE | | +| `ingredient_id` | INT UNSIGNED | NO | — | FK -> `ingredient(id)`, ON DELETE RESTRICT | RESTRICT: cannot remove an ingredient still referenced in a product recipe | +| `quantity_normal` | SMALLINT UNSIGNED | NO | 1 | CHECK > 0 | units consumed in Normal format (e.g., 2 for double cheese) | +| `quantity_maxi` | SMALLINT UNSIGNED | NO | 1 | CHECK > 0 | units consumed in Maxi format. Equals `quantity_normal` for format-invariant ingredients (burger, sauce); higher for side and drink ingredients (Maxi enlarges side + drink only). See note 7. | +| `is_removable` | TINYINT(1) | NO | 1 | — | customer can remove this ingredient at no cost | +| `is_addable` | TINYINT(1) | NO | 0 | — | customer can add an extra unit of this ingredient | +| `extra_price_cents` | INT UNSIGNED | NO | 0 | CHECK >= 0 | surcharge in cents when `is_addable=1` and customer adds it (0 = free extra) | + +**Primary key**: composite `(product_id, ingredient_id)`. + +**Volume**: ~5-10 ingredients per product, ~53 products = ~300-500 rows at seed. + +--- + +### 3.8 `allergen` + +Catalogue of the 14 regulated allergens (INCO Regulation (EU) 1169/2011). + +| Attribute | Type | NULL | Default | Constraint | Notes | |---|---|---|---|---|---| | `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | -| `commande_id` | INT UNSIGNED | NO | - | FK -> `commande(id)`, ON DELETE CASCADE | si la commande disparait, son journal aussi | -| `event_type` | ENUM('CREATED','PAID','PREPARING_STARTED','READY','DELIVERED','CANCELLED') | NO | - | INDEX | type d'evenement, aligne sur la machine a etats | -| `from_statut` | ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') | YES | NULL | - | statut avant transition (NULL pour CREATED) | -| `to_statut` | ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') | NO | - | - | statut apres transition | -| `user_id` | INT UNSIGNED | YES | NULL | FK -> `user(id)`, ON DELETE SET NULL | NULL si auto-validation kiosk ou system event ; sinon = equipier qui a declenche | -| `payload` | JSON | YES | NULL | - | contexte additionnel : raison annulation, methode paiement, montant rembourse, etc. | -| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | INDEX | timestamp immuable de l'evenement | +| `code` | VARCHAR(30) | NO | — | UNIQUE | machine-readable code, e.g., `gluten`, `milk`, `nuts` | +| `name` | VARCHAR(80) | NO | — | — | display name, e.g., "Gluten", "Lait", "Fruits a coque" | +| `description` | TEXT | YES | NULL | — | optional guidance for staff | -**Cle primaire** : `id`. - -**Index supplementaires** : -- `(commande_id, created_at)` pour requete "historique d'une commande" -- `(user_id, created_at)` pour requete "actions d'un equipier sur une periode" - -**Volume** : ~5-8 events par commande (1 CREATED + 1 PAID + 1 PREPARING + 1 READY + 1 DELIVERED, plus eventuels CANCELLED). Sur 6 mois, ~50k-80k lignes. - -**ON DELETE SET NULL sur `user_id`** : si un user est supprime (rare, cf. soft delete), les events restent (audit preserve) mais l'attribution est perdue. Le brief peut imposer `ON DELETE RESTRICT` si l'integrite de l'audit est critique. +**Volume**: 14 rows at seed (fixed by EU regulation 1169/2011, list confirmed at seed time). +Allergens for a product are **computed** by joining `product_ingredient` -> +`ingredient_allergen` -> `allergen`; no manual re-entry per product. --- -### 3.8 `user` +### 3.9 `ingredient_allergen` -Utilisateur du back-office (admin, manager, equipier) - **pas** les clients de la borne, qui -ne sont pas authentifies. +Maps which allergens each ingredient contains. Pure join table. -| Attribut | Type | NULL | Defaut | Contrainte | Notes | +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `ingredient_id` | INT UNSIGNED | NO | — | FK -> `ingredient(id)`, ON DELETE CASCADE | | +| `allergen_id` | INT UNSIGNED | NO | — | FK -> `allergen(id)`, ON DELETE RESTRICT | | + +**Primary key**: composite `(ingredient_id, allergen_id)`. + +--- + +### 3.10 `customer_order` + +Customer transaction: 1 order = 1 validated cart at a point in time. +(Table name rationale: see modeling note 3.) + +| Attribute | Type | NULL | Default | Constraint | Notes | |---|---|---|---|---|---| | `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | -| `email` | VARCHAR(254) | NO | - | UNIQUE | longueur max RFC 5321 | -| `password_hash` | VARCHAR(255) | NO | - | - | hash argon2id (cf. `PASSWORD_ALGO` dans `.env`), longueur 96 chars typique mais marge 255 | -| `nom` | VARCHAR(60) | NO | - | - | | -| `prenom` | VARCHAR(60) | NO | - | - | | -| `role_id` | INT UNSIGNED | NO | - | FK -> `role(id)`, ON DELETE RESTRICT | un user ne peut pas exister sans role | -| `est_actif` | TINYINT(1) | NO | 1 | - | desactivation sans suppression | -| `last_login_at` | DATETIME | YES | NULL | - | utile pour audit et detection comptes dormants | -| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | -| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | - | +| `order_number` | VARCHAR(20) | NO | — | UNIQUE | human-readable format: `K`/`C`/`D`-YYYY-MM-DD-NNN. Prefix by channel: K=kiosk, C=counter, D=drive. See note 4. | +| `source` | ENUM('kiosk','counter','drive') | NO | — | INDEX | input channel (who entered the order). Values in English, see note 5. | +| `service_mode` | ENUM('dine_in','takeaway','drive') | NO | — | — | consumption mode, retained for stats/KPI only. No fiscal role (see note 9). `drive` source implies `drive` service_mode (cross-constraint enforced at app layer). | +| `status` | ENUM('pending_payment','paid','delivered','cancelled') | NO | 'pending_payment' | INDEX | 4-state machine: `pending_payment -> paid -> delivered` (+ `cancelled`). See note 6. | +| `total_ht_cents` | INT UNSIGNED | NO | — | CHECK >= 0 | ex-VAT total, snapshot at order validation | +| `total_vat_cents` | INT UNSIGNED | NO | — | CHECK >= 0 | VAT amount, snapshot | +| `total_ttc_cents` | INT UNSIGNED | NO | — | CHECK > 0 | incl.-VAT total; must equal total_ht_cents + total_vat_cents (verified at MLT layer) | +| `paid_at` | DATETIME | YES | NULL | — | timestamp of transition to `paid` (NULL before payment) | +| `delivered_at` | DATETIME | YES | NULL | — | timestamp of transition to `delivered` (NULL before delivery) | +| `cancelled_at` | DATETIME | YES | NULL | — | timestamp of cancellation (NULL if not cancelled) | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | INDEX | used for live stats aggregations; also serves as `service_day` base | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | audit | -**Volume** : 5-20 lignes (equipe restaurant + 1-2 admins). +**Dropped from v0.1**: `tva_taux_pourmille` (moved to line level — `order_item.vat_rate_snapshot`), +`paye_a` (renamed `paid_at`). Machine states `preparing` and `ready` dropped (see note 6). -**Reference RFC 5321 sur la longueur email** : la limite locale-part = 64, domaine = 255, -total = 254 (incluant le `@`). VARCHAR(254) est la valeur conforme spec. +**`service_day` computation** (KPI grouping): +``` +CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END +``` +Computed at query time, not stored as a column (the generated-column formula with `INTERVAL 4 HOUR +30 MINUTE` in v0.1 MLD was incorrect and is dropped). Cutoff: 10:00. + +**Volume**: ~100-300 orders/day at peak, ~10k rows over a 6-month demo. --- -### 3.9 `role` +### 3.11 `order_item` -Roles utilisables dans le back-office (RBAC). Creables / modifiables / desactivables depuis -l'UI admin (les permissions sont statiques, declarees en migration). +Line of an order: a single product or a menu, with price, label, and VAT rate +snapshotted at transaction time. -| Attribut | Type | NULL | Defaut | Contrainte | Notes | +| Attribute | Type | NULL | Default | Constraint | Notes | |---|---|---|---|---|---| | `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | -| `code` | VARCHAR(40) | NO | - | UNIQUE | identifiant code (ex : `admin`, `manager`, `equipier`) | -| `libelle` | VARCHAR(80) | NO | - | - | nom affichable (ex : `Administrateur`) | -| `description` | TEXT | YES | NULL | - | | -| `est_actif` | TINYINT(1) | NO | 1 | - | desactivation sans suppression (preserve l'historique des users qui avaient ce role) | -| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | -| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | audit | +| `order_id` | INT UNSIGNED | NO | — | FK -> `customer_order(id)`, ON DELETE CASCADE | | +| `item_type` | ENUM('product','menu') | NO | — | — | discriminator | +| `product_id` | INT UNSIGNED | YES | NULL | FK -> `product(id)`, ON DELETE RESTRICT | non-null if `item_type = 'product'` | +| `menu_id` | INT UNSIGNED | YES | NULL | FK -> `menu(id)`, ON DELETE RESTRICT | non-null if `item_type = 'menu'` | +| `format` | ENUM('normal','maxi') | NO | 'normal' | — | applies to menu items (Normal / Maxi). For standalone products, value is `normal` (no individual upsizing in this model). See note 7. | +| `label_snapshot` | VARCHAR(120) | NO | — | — | label at time of order (preserved if product is renamed) | +| `unit_price_cents_snapshot` | INT UNSIGNED | NO | — | CHECK > 0 | unit price incl. VAT at time of order | +| `vat_rate_snapshot` | SMALLINT UNSIGNED | NO | — | CHECK IN (55, 100) | VAT rate in per-mille at time of order (snapshotted from `product.vat_rate`) | +| `quantity` | SMALLINT UNSIGNED | NO | 1 | CHECK > 0 | quantity ordered (e.g., 3 Cocas = 1 line with quantity=3) | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | audit | -**Volume** : 3-5 lignes (admin, manager, equipier-comptoir, equipier-drive). Extensible -via UI admin sans deploiement. +**CHECK constraint** (applicative or MariaDB CHECK >= 10.2): +`(item_type='product' AND product_id IS NOT NULL AND menu_id IS NULL) +OR (item_type='menu' AND menu_id IS NOT NULL AND product_id IS NULL)` + +**Volume**: ~3-5 lines per order -> 30k-50k rows over 6 months. --- -### 3.10 `permission` +### 3.12 `order_item_selection` -Permissions granulaires assignables aux roles (ex : `produit.create`, `commande.read`). +The actual choices made by the customer for each slot of a menu line. +1 row = 1 slot filled for 1 order_item of type `menu`. -| Attribut | Type | NULL | Defaut | Contrainte | Notes | +| Attribute | Type | NULL | Default | Constraint | Notes | |---|---|---|---|---|---| | `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | -| `code` | VARCHAR(60) | NO | - | UNIQUE | format `.` (ex : `produit.update`) | -| `libelle` | VARCHAR(120) | NO | - | - | nom affichable | -| `description` | TEXT | YES | NULL | - | | -| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | +| `order_item_id` | INT UNSIGNED | NO | — | FK -> `order_item(id)`, ON DELETE CASCADE | must reference an order_item with item_type='menu' | +| `menu_slot_id` | INT UNSIGNED | NO | — | FK -> `menu_slot(id)`, ON DELETE RESTRICT | which slot was filled | +| `product_id` | INT UNSIGNED | NO | — | FK -> `product(id)`, ON DELETE RESTRICT | product chosen by the customer for this slot | +| `label_snapshot` | VARCHAR(120) | NO | — | — | product label at time of order | -**Volume** : ~20-40 lignes selon granularite (CRUD sur produit, menu, categorie, user, role, -commande, stats). +**Volume**: ~2-3 selections per menu line. +**KPI use**: enables analysis of which drink/side combinations are most chosen. --- -### 3.11 `role_permission` (jointure) +### 3.13 `order_item_modifier` -Mapping N-N entre roles et permissions. +Ingredient-level modifications applied by the customer to a product or to the fixed +burger of a menu: removal (free) or addition (with optional surcharge). -| Attribut | Type | NULL | Defaut | Contrainte | -|---|---|---|---|---| -| `role_id` | INT UNSIGNED | NO | - | FK -> `role(id)`, ON DELETE CASCADE | -| `permission_id` | INT UNSIGNED | NO | - | FK -> `permission(id)`, ON DELETE CASCADE | +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | +| `order_item_id` | INT UNSIGNED | NO | — | FK -> `order_item(id)`, ON DELETE CASCADE | the order line being modified (product or menu) | +| `ingredient_id` | INT UNSIGNED | NO | — | FK -> `ingredient(id)`, ON DELETE RESTRICT | the ingredient being modified | +| `action` | ENUM('remove','add') | NO | — | — | `remove` = free removal; `add` = extra unit (may have surcharge) | +| `extra_price_cents` | INT UNSIGNED | NO | 0 | CHECK >= 0 | snapshot of `product_ingredient.extra_price_cents` at time of order (0 for removals) | -**Cle primaire** : composite `(role_id, permission_id)`. +**Modifier attachment rule** (see modeling note 10): +- For a standalone product (`item_type='product'`): the modifier targets the product + directly via `order_item_id`. +- For a menu (`item_type='menu'`): the modifier targets the menu line's fixed burger + via the same `order_item_id`. The burger is identified by `menu.burger_product_id`, + allowing the kitchen display to resolve which ingredients are modified without ambiguity. + No additional FK is needed: given `order_item_id`, the burger is + `order_item.menu_id -> menu.burger_product_id`. -**Volume** : ~50-100 lignes selon les attributions (admin couvre potentiellement toutes les -permissions, les autres roles un sous-ensemble). +**Stock impact**: each modifier affects ingredient stock at `paid` transition +(`remove` -> no decrement for that ingredient; `add` -> extra decrement). --- -## 4. Notes de modelisation +### 3.14 `user` -> Le diagramme entites-relations et les justifications de cardinalites sont documentes dans [`mcd.md`](mcd.md) (diagrammes drawio des 4 sous-domaines + recapitulatif global). Le dictionnaire ne dedouble pas cette vue pour eviter d'avoir deux sources de verite divergeantes. +Back-office user (admin, manager, kitchen staff, counter, drive). Kiosk customers +are not authenticated and have no row here. -### Note 1 - Pourquoi `INT UNSIGNED` en centimes pour les prix +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | +| `email` | VARCHAR(254) | NO | — | UNIQUE | max length per RFC 5321 | +| `password_hash` | VARCHAR(255) | NO | — | — | argon2id hash (see `PASSWORD_ALGO` in `.env`); typical length 96 chars, margin to 255 | +| `first_name` | VARCHAR(60) | NO | — | — | | +| `last_name` | VARCHAR(60) | NO | — | — | | +| `role_id` | INT UNSIGNED | NO | — | FK -> `role(id)`, ON DELETE RESTRICT | a user cannot exist without a role | +| `is_active` | TINYINT(1) | NO | 1 | — | deactivation without deletion | +| `last_login_at` | DATETIME | YES | NULL | — | useful for audit and dormant account detection | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | audit | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | audit | -Stocker un prix en `FLOAT` ou `DECIMAL(10,2)` est techniquement valide mais introduit deux -risques : +**Volume**: 5-20 rows (restaurant team + 1-2 admins). -1. **Arrondi FLOAT** : `0.1 + 0.2 = 0.30000000000000004` en flottants IEEE 754. Sommer 100 - lignes de commande peut produire des ecarts de centimes vs la realite metier. -2. **Conversion FLOAT -> string** : differents drivers PHP/MariaDB peuvent serialiser les - floats avec une precision variable. +RFC 5321 email length: local-part <= 64, domain <= 255, total <= 254 (including `@`). +VARCHAR(254) is the spec-compliant value. -Stocker en `INT UNSIGNED` (centimes : 880 pour 8,80 EUR) elimine ces risques. La conversion -en EUR pour l'affichage se fait cote PHP a la sortie : `number_format($cents / 100, 2)`. +--- -Reference : David Goldberg, *What Every Computer Scientist Should Know About Floating-Point -Arithmetic*, ACM Computing Surveys, 1991. (Le sujet est devenu un classique de la litterature -informatique.) +### 3.15 `role` -### Note 2 - Pourquoi `ENUM` plutot que table de reference +Back-office roles (RBAC). Creatable / modifiable / deactivatable from admin UI. +Seed provides 5 roles; custom roles (e.g., "chef-patissier") can be added without deployment. -Les ENUM (`mode_consommation`, `statut`, `role` dans `menu_produit`, `type_item`) auraient pu -etre des tables de reference (ex : `mode_consommation_referentiel`). Choix retenu : ENUM. +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | +| `code` | VARCHAR(40) | NO | — | UNIQUE | machine code, e.g., `admin`, `manager`, `kitchen`, `counter`, `drive` | +| `label` | VARCHAR(80) | NO | — | — | display name, e.g., `Administrator`, `Kitchen Staff` | +| `description` | TEXT | YES | NULL | — | | +| `default_route` | VARCHAR(120) | YES | NULL | — | landing screen for this role (e.g., `/admin/orders`, `/kitchen/display`). Makes routing dynamic — no hardcoded role names in front-end routing. | +| `order_source` | ENUM('kiosk','counter','drive') | YES | NULL | — | auto-tagged `source` when this role creates an order (NULL for admin/manager who can create on behalf of any channel) | +| `is_active` | TINYINT(1) | NO | 1 | — | deactivation preserves history of users who held this role | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | audit | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | audit | -Avantages ENUM dans ce contexte : -- Valeurs stables et limitees (3-7 valeurs max), peu probables d'evoluer -- Contrainte SGBD au lieu de FK runtime, requetes plus simples -- Lisibilite directe en SQL : `WHERE mode_consommation = 'sur_place'` - -Cout d'un changement futur : un `ALTER TABLE ... MODIFY COLUMN ... ENUM(...)` pour ajouter une -valeur. Acceptable car les changements sont attendus rarement. - -Si plus tard ces ENUMs prennent des libelles ou descriptions multilingues, on les passera en -tables. Pas pour MVP. - -### Note 3 - Pourquoi `produit` ET `menu` separes (pas une table unique avec STI) - -Option consideree : Single Table Inheritance avec une colonne `type ENUM('produit','menu')` -sur une seule table. Cout : NULLs fantomes sur les colonnes specifiques (un produit n'a pas -de composition). - -Option retenue : 2 tables separees (`produit`, `menu`). Avantages : -- Semantique claire (un menu n'est pas un "produit avec composition", c'est une autre nature) -- Contraintes specifiques possibles (ex : un menu doit avoir au moins 1 entree dans - `menu_produit`, contrainte applicative) -- Pas de NULL sur les colonnes specifiques - -Cout : la table `ligne_commande` doit gerer 2 FKs (produit_id OU menu_id) avec une regle -d'exclusivite. Acceptable et courant en e-commerce. - -### Note 4 - Pas de gestion stock numerique - -Choix MVP : un boolean `est_disponible` suffit. La rupture est geree manuellement par -l'equipier-comptoir depuis le back-office. Si une feature `quantite_stock` est ajoutee -plus tard, ce sera une nouvelle colonne avec sa propre logique de decrement/realimentation. - -### Note 5 - Audit fields uniformes - -Les tables metier portent `created_at` et `updated_at`. Cette uniformite permet : -- Diagnostic ("quand cette donnee a-t-elle ete modifiee ?") -- Tri par recence dans le back-office sans table dediee -- Synchronisation eventuelle avec un cache - -Les tables de jointure pure (`menu_produit`, `role_permission`) n'ont pas de `updated_at` : -les jointures sont supprimees+recreees au lieu d'etre modifiees. - -### Note 6 - Polymorphisme `ligne_commande` -> (`produit` ou `menu`) - -Pattern utilise : 2 colonnes nullables avec un discriminateur `type_item`. Avantages : -- FKs reelles vers les tables ciblees (integrite referentielle) -- Lisible en SQL (`JOIN produit ON l.produit_id = p.id` selon `type_item`) - -Alternative consideree : une colonne `item_id` + `item_type` sans FK reelle (Rails-style -polymorphic association). Inconvenient : pas d'integrite referentielle SGBD. - -Choix retenu : 2 colonnes + 2 FKs + contrainte CHECK. Cout : 1 colonne supplementaire -(`menu_id` souvent NULL, `produit_id` parfois NULL), gain : integrite forte. - -### Note 7 - Limites RFC pour les emails et libelles - -- `email` : VARCHAR(254) (RFC 5321) -- `libelle` produit/menu : VARCHAR(120) - couvre la quasi-totalite des libelles observes dans - la source ecole (max observe : 41 chars). Marge 3x. -- `slug` : VARCHAR(60) - coherent avec les conventions URL kebab-case courantes. - -### Note 8 - `source` vs `mode_consommation` (separation canal / fiscalite) - -Deux dimensions distinctes que la modelisation Wakdo separe explicitement : - -| | `source` | `mode_consommation` | +**Seed roles**: +| Code | `default_route` | `order_source` | |---|---|---| -| Nature | canal de saisie de la commande (input) | mode de consommation (output) | -| Valeurs | kiosk, comptoir, drive | sur_place, a_emporter, drive | -| Decision metier | qui a saisi la commande, authentification, analytics | TVA applicable, gestion capacite salle | +| `admin` | `/admin/dashboard` | NULL | +| `manager` | `/admin/stats` | NULL | +| `kitchen` | `/kitchen/display` | NULL | +| `counter` | `/counter/orders` | `counter` | +| `drive` | `/drive/orders` | `drive` | -Les deux dimensions sont independantes pour `kiosk` et `comptoir` (un client a la borne peut choisir sur_place OU a_emporter ; idem au comptoir). Le `drive` est le seul cas ou les deux dimensions sont identiques : `source=drive` implique `mode_consommation=drive`. - -Cette contrainte croisee est verifiee a l'ecriture (MLT - precondition de l'operation `creer_commande`). En SQL elle pourrait etre exprimee par un CHECK : `CHECK (source != 'drive' OR mode_consommation = 'drive')`. - -### Note 9 - TVA en restauration rapide chez Wakdo - -Wakdo est un fast-food, pas un restaurant a service a table : quel que soit le `mode_consommation`, tout est servi en emballages papier (sur plateau pour `sur_place`, en sac pour `a_emporter` et `drive`). La distinction `sur_place` vs `a_emporter` ne porte donc pas sur le service mais sur : - -- **TVA applicable** : 10% pour la consommation immediate sur place, 5,5% pour les produits a emporter destines a la consommation differee (cf. service-public.fr article F31407, 2024) -- **Occupation salle** : le client `sur_place` consomme une place assise (utile si une feature capacite est ajoutee plus tard) - -Le taux de TVA est snapshote dans `commande.tva_taux_pourmille` au moment de la transaction pour preserver l'integrite historique si la legislation evolue. - -### Note 10 - Pattern event sourcing simplifie via `commande_event` - -Plutot que d'ajouter des colonnes `saisi_par_id`, `valide_par_id`, `prepare_par_id`, `livre_par_id` sur `commande` (denormalisation lourde, 4 FKs), Wakdo retient une table d'audit dediee `commande_event` (cf. entite 3.7). - -**Principe** : `commande` porte uniquement l'**etat courant** (`statut`). Chaque transition d'etat insere une ligne dans `commande_event` (append-only, immuable). Pour reconstituer l'historique d'une commande : `SELECT * FROM commande_event WHERE commande_id = ? ORDER BY created_at`. - -**Avantages** : -- Tracabilite complete sans charger `commande` de colonnes peu remplies -- Extensible : ajouter un nouveau type d'evenement (REFUNDED, RECLAIMED, ...) = ajouter une valeur a l'ENUM `event_type`, sans migration intrusive -- Compatible avec analytics fines : "temps moyen entre PAID et READY par equipier" via JOIN sur `(user_id, event_type)` - -**Couts assumes** : -- Pattern d'ecriture systematique a respecter : chaque service qui modifie `commande.statut` doit aussi inserer dans `commande_event`. A encapsuler dans un repository pour eviter les oublis. -- Volume table x5-x8 par rapport a `commande` -- Requete "qui a saisi cette commande" demande un join (pas de denormalisation `saisi_par_id` directe) - -Si le cout SQL devient penible plus tard, on pourra dupliquer `saisi_par_id` sur `commande` comme colonne denormalisee, sans changer le pattern event. - -**Defendable a l'oral** comme "audit log applicatif" ou "event sourcing simplifie", aligne sur les pratiques de tracabilite des SI en production. - -### Note 11 - Stockage des images : path en VARCHAR vs BLOB en DB - -Les colonnes `image_path` (entites `categorie`, `produit`, `menu`) stockent un **chemin relatif** au public root (ex : `/uploads/produits/burger-classique.jpg`), pas un chemin absolu serveur. Le PHP resout via un prefixe configure dans `.env` (`UPLOAD_DIR=public/uploads`). - -#### Pourquoi pas un BLOB en BDD ? - -L'alternative consistant a stocker les images en LONGBLOB dans MariaDB a ete consideree puis ecartee : - -| Critere | `image_path` VARCHAR (retenu) | BLOB en DB | -|---|---|---| -| Performance kiosk | Apache sert le fichier en ms (cache OS) | PHP lit la DB + streame, latence multipliee | -| Cache HTTP | ETag, Last-Modified, cache browser, CDN natifs | A reimplementer cote PHP | -| Backup BDD | Quelques Mo (paths uniquement) | Croissance Go (66 produits x ~200 Ko + variantes responsive) | -| Replication / dump | Rapide | Lente, ralentit les ACK | -| Pipeline image | `convert`, `webp`, optimisation = outils filesystem standards | A reinventer en PHP | -| Cout cloud (si migration) | Storage S3-like cheap | BDD storage cher | - -Pour un MVP fast-food avec borne tactile reactive, le filesystem est le choix par defaut documente dans la litterature web (cf. references). Le BLOB en DB se justifie pour des cas specifiques (fichiers sensibles avec acces controle par ligne, garantie ACID sur le contenu) qui ne s'appliquent pas a un catalogue produit public. - -#### Le "leak" de path n'en est pas un - -Argument souvent entendu : "stocker un chemin en DB expose la structure du serveur". Analyse : - -- `image_path` contient un chemin **relatif** (`/uploads/produits/...`), pas absolu. -- Cette URL est par definition **publique** : la borne kiosk affiche `` que n'importe quel visiteur voit dans le HTML. -- Pour acceder a la colonne `image_path` en DB, un attaquant doit deja avoir une breche DB (SQLi, credentials voles). A ce stade il a deja toutes les donnees metier (commandes, password_hash, etc.) ; connaitre `/uploads/produits/` est l'info la moins critique de la DB. - -#### Les vrais risques securite filesystem (traites par ailleurs) - -1. **Path traversal a l'upload** : valider que le nom de fichier upload passe par `basename()` + regex `^[a-z0-9_-]+\.(jpg|png|webp)$` cote service admin. -2. **MIME type spoof** : verifier le vrai MIME via `finfo_file()` (extension `.jpg` ne suffit pas). Desactiver l'execution PHP dans `/uploads/` via Apache (`php_flag engine off` + `FilesMatch .(php|phtml|phar)$ deny`). -3. **Stockage hors-webroot pour les fichiers sensibles** : pas applicable au catalogue public, mais regle de principe pour PDF de facturation, exports stats, etc. -4. **Validation taille** : `UPLOAD_MAX_SIZE_MB` dans `.env` + verification PHP cote upload. -5. **Nom non-predictible pour fichiers sensibles** : UUID au lieu du nom metier si l'image contient des donnees sensibles. Pas applicable a un catalogue public. - -#### Sources - -- OWASP File Upload Cheat Sheet (section "Filesystem storage") -- MariaDB Knowledge Base - LONGBLOB performance considerations -- Apache HTTP Server documentation - `mod_xsendfile` et serving static content +**RBAC architecture rule (P2)**: application code tests permissions, not role names. +Adding a new role with the right permissions requires no code change (permission-driven, +not role-name-driven — per Sandhu/NIST RBAC model). --- -## 5. A faire au prochain sprint (MCD) +### 3.16 `role_visible_source` -- Tracer le MCD avec les cardinalites precises (entites + associations + roles + cardinalites - min/max) -- Cross-validation MCD <-> MCT (mantra #34) : verifier que chaque traitement metier identifie - manipule des entites existantes et que chaque entite participe a au moins un traitement -- Decider du nommage final des associations (`compose`, `passe_commande`, `contient`, etc.) -- Eventuellement normaliser plus loin (3NF) si une derive est detectee +Defines which order sources are visible on the preparation dashboard for a given role. +Pure join table. + +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `role_id` | INT UNSIGNED | NO | — | FK -> `role(id)`, ON DELETE CASCADE | | +| `source` | ENUM('kiosk','counter','drive') | NO | — | — | source visible to this role on the kitchen/counter/drive display | + +**Primary key**: composite `(role_id, source)`. + +**Seed data**: +| Role | Visible sources | +|---|---| +| `kitchen` | kiosk, counter, drive (all) | +| `counter` | kiosk, counter | +| `drive` | drive | + +--- + +### 3.17 `permission` + +Granular permissions assignable to roles. Catalogue is fixed at seed (no UI creation). + +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | +| `code` | VARCHAR(60) | NO | — | UNIQUE | format `.` | +| `label` | VARCHAR(120) | NO | — | — | display name | +| `description` | TEXT | YES | NULL | — | | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | audit | + +**Fixed permission catalogue** (23 codes — frozen before DDL): + +| Code | Granted to (seed default) | +|---|---| +| `product.create` | admin, manager | +| `product.read` | admin, manager, kitchen, counter, drive | +| `product.update` | admin, manager | +| `product.delete` | admin | +| `menu.create` | admin, manager | +| `menu.read` | admin, manager, kitchen, counter, drive | +| `menu.update` | admin, manager | +| `menu.delete` | admin | +| `category.manage` | admin, manager | +| `ingredient.manage` | admin, manager | +| `stock.read` | admin, manager, kitchen, counter, drive | +| `stock.count` | admin, manager, kitchen, counter, drive | +| `stock.manage` | admin, manager | +| `order.read` | admin, manager, kitchen, counter, drive | +| `order.create` | admin, counter, drive | +| `order.deliver` | admin, counter, drive | +| `order.cancel` | admin, counter, drive | +| `user.create` | admin | +| `user.read` | admin, manager | +| `user.update` | admin | +| `user.deactivate` | admin | +| `role.manage` | admin | +| `stats.read` | admin, manager | + +**Volume**: 23 rows at seed. + +--- + +### 3.18 `role_permission` + +N-N mapping between roles and permissions. Pure join table. + +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `role_id` | INT UNSIGNED | NO | — | FK -> `role(id)`, ON DELETE CASCADE | | +| `permission_id` | INT UNSIGNED | NO | — | FK -> `permission(id)`, ON DELETE CASCADE | | + +**Primary key**: composite `(role_id, permission_id)`. + +**Volume**: ~50-100 rows at seed (admin covers all; others cover a subset). + +--- + +### 3.19 `stock_movement` + +Append-only audit log of all stock changes per ingredient. +1 row per movement (sale, cancellation, restock, inventory correction). + +| Attribute | Type | NULL | Default | Constraint | Notes | +|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | +| `ingredient_id` | INT UNSIGNED | NO | — | FK -> `ingredient(id)`, ON DELETE RESTRICT | ingredient affected | +| `movement_type` | ENUM('sale','cancellation','restock','inventory_correction') | NO | — | INDEX | nature of the movement | +| `delta` | INT | NO | — | — | signed change: negative for consumption (sale), positive for restock/cancellation/correction | +| `order_id` | INT UNSIGNED | YES | NULL | FK -> `customer_order(id)`, ON DELETE SET NULL | linked order for `sale` and `cancellation` movements; NULL for restock/correction | +| `user_id` | INT UNSIGNED | YES | NULL | FK -> `user(id)`, ON DELETE SET NULL | user who triggered the movement (NULL for automated sale decrements) | +| `note` | VARCHAR(255) | YES | NULL | — | optional human note (e.g., reason for correction, pack reference) | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | INDEX | immutable timestamp | + +**Immutability**: no UPDATE or DELETE on this table. Corrections are new rows with +`movement_type='inventory_correction'` and a signed delta. + +**Automatic movements** (triggered at status transitions): +- `paid` transition: 1 `sale` row per ingredient unit consumed (accounting for modifiers). +- `cancelled` (from `paid`): 1 `cancellation` row per ingredient unit re-credited. + +**Manual movements**: +- `restock`: manager or admin records a delivery (`+= N * pack_size`). +- `inventory_correction`: morning/evening physical count; system records the discrepancy + (delta = actual - theoretical). + +**Volume**: ~5-15 movements per order across all ingredients; index on +`(ingredient_id, created_at)` is recommended for per-ingredient history queries. + +--- + +## 4. Modeling notes + +### Note 1 — Why `INT UNSIGNED` in cents for prices + +Storing a price as `FLOAT` or `DECIMAL(10,2)` is technically valid but introduces two risks: + +1. **FLOAT rounding**: `0.1 + 0.2 = 0.30000000000000004` in IEEE 754 floating-point. + Summing 100 order lines can produce cent-level discrepancies vs business reality. +2. **FLOAT-to-string conversion**: different PHP/MariaDB driver versions may serialize floats + with variable precision. + +Storing as `INT UNSIGNED` (cents: 880 for EUR 8.80) eliminates these risks. Conversion to EUR +for display is done in PHP at output: `number_format($cents / 100, 2)`. + +Reference: David Goldberg, *What Every Computer Scientist Should Know About Floating-Point +Arithmetic*, ACM Computing Surveys, 1991. + +### Note 2 — Why `ENUM` rather than a reference table + +ENUMs (`service_mode`, `status`, `item_type`, `action`, `slot_type`) could have been reference +tables. Choice retained: ENUM. + +Advantages in this context: +- Values are stable and limited (3-7 values max), unlikely to evolve frequently. +- DBMS-level constraint instead of runtime FK; simpler queries. +- Directly readable in SQL: `WHERE status = 'paid'`. + +Cost of a future change: `ALTER TABLE ... MODIFY COLUMN ... ENUM(...)` to add a value. +Acceptable given changes are expected to be rare. + +If these ENUMs later require multilingual labels or descriptions, they will be migrated to +reference tables. Not in scope for this iteration. + +### Note 3 — Why `customer_order` and not `order` + +`ORDER` is an SQL reserved word (used in `ORDER BY`). Three approaches exist: +- Quote the name everywhere: `` `order` `` — requires quoting in every SQL statement, + error-prone and non-portable across DBMS dialects. +- Use an alias at ORM level: possible but adds a mapping layer. +- Rename: `customer_order` (chosen) — unambiguous, self-documenting, no quoting required. + +Alternative considered and rejected: `purchase` (less domain-specific), +`transaction` (also reserved or ambiguous). `customer_order` matches the domain language +and avoids all conflicts. + +`order_item` is retained as the line table name: `item` is not reserved, and the +`order_` prefix makes the parent relationship clear. + +### Note 4 — Order number prefix by channel + +Format: `K`/`C`/`D`-YYYY-MM-DD-NNN (kiosk / counter / drive). + +Rationale: the prefix encodes the channel, which is useful for rapid visual identification +by kitchen and counter staff without querying the `source` column. The sequential counter NNN +restarts daily per channel. Collision-free within a day given expected volume. + +Alternative rejected: neutral prefix `W-` for all channels (simpler, but loses channel +readability for staff). + +### Note 5 — `source` vs `service_mode` (channel vs consumption mode) + +Two distinct dimensions, kept separate: + +| | `source` | `service_mode` | +|---|---|---| +| Nature | input channel (who entered the order) | consumption mode (where the customer eats) | +| Values | kiosk, counter, drive | dine_in, takeaway, drive | +| Used for | authentication, analytics, permission filtering | KPI, capacity (no fiscal role) | + +The two dimensions are independent for `kiosk` and `counter` (a kiosk customer can choose +`dine_in` or `takeaway`). `drive` is the only case where both dimensions align: +`source=drive` implies `service_mode=drive`. This cross-constraint is verified at app layer. + +### Note 6 — Reduced 4-state machine + +v0.1 had 6 states (`pending_payment`, `paid`, `preparing`, `ready`, `delivered`, `cancelled`). +v0.2 reduces to 4 states: `pending_payment -> paid -> delivered` (+ `cancelled`). + +Rationale (Decision 4 from `revue-alignement-p1.md` §7): in a fast-food context, the kitchen +display (KDS) is a visual system — staff see the ticket and act. `preparing` and `ready` were +intermediate states that added complexity without proportional business value. The single +kitchen action is `deliver` (counter/drive staff hands over the order), collapsing +`preparing + ready + delivered` into one gesture. KPI is total time: `delivered_at - paid_at` +(SLA ~10 min). KDS color coding is computed from `now - paid_at`, no extra stored state. + +**Dropped states and timestamps**: `preparing_at`, `ready_at` are not stored. + +### Note 7 — Normal / Maxi format cascade + +The Maxi format enlarges the side and the drink only. The burger is unchanged and the sauce +portion is unchanged (a sauce pot is the same in both formats). This scope is explicit so the +stock model stays faithful. + +**Price side** — not modeled at individual component price level: +- `menu` carries two prices: `price_normal_cents` and `price_maxi_cents`. +- `order_item.format` records which format the customer chose (`normal` or `maxi`). +- `order_item.unit_price_cents_snapshot` captures the actual price paid (Normal or Maxi). +- No individual price per slot component is stored; the price differential is a menu-level + attribute, consistent with how fast-food menus tend to be priced in practice. + +**Stock side** — modeled via a format multiplier on the recipe: +- `product_ingredient` carries `quantity_normal` and `quantity_maxi`. +- At the `paid` transition, the decrement uses `quantity_maxi` when `order_item.format='maxi'`, + otherwise `quantity_normal`. +- For burger and sauce ingredients, `quantity_maxi = quantity_normal` (format-invariant). +- For side and drink ingredients, `quantity_maxi > quantity_normal` (Maxi consumes more). +- The format cascades from the menu line (`order_item.format`) to its slot selections; a + standalone product line defaults to `normal`. +- Single product per choice (e.g., one `Fries` product), not separate medium/large products. + +Calibration: the Maxi surcharge is in the ~1.50 EUR range for this model (derived from +internal data; cross-checked against market magnitude for plausibility. Wakdo is a fictional +pastiche so exact prices are not copied from a real chain). + +Calibration: the Maxi surcharge is in the ~1.50 EUR range for this model (derived from +internal data; cross-checked against market magnitude for plausibility. Wakdo is a fictional +pastiche so exact prices are not copied from a real chain). + +### Note 8 — Image storage: path in VARCHAR vs BLOB in DB + +`image_path` columns (`category`, `product`, `menu`) store a **relative path** from the +public root (e.g., `/uploads/products/classic-burger.jpg`), not an absolute server path. +PHP resolves via a prefix from `.env` (`UPLOAD_DIR=public/uploads`). + +BLOB storage was considered and rejected: + +| Criterion | `image_path` VARCHAR (chosen) | BLOB in DB | +|---|---|---| +| Kiosk performance | Apache serves files in ms (OS cache) | PHP reads DB + streams, multiplied latency | +| HTTP caching | ETag, Last-Modified, browser cache, CDN native | must be reimplemented in PHP | +| DB backup size | Megabytes (paths only) | Gigabytes (66 products x ~200 KB + responsive variants) | +| Image pipeline | `convert`, `webp`, optimization = standard filesystem tools | must be reinvented in PHP | + +Sources: OWASP File Upload Cheat Sheet; MariaDB Knowledge Base — LONGBLOB performance; +Apache HTTP Server documentation — serving static content. + +### Note 9 — VAT rule in French fast-food (fact-checked) + +``` +FACT-CHECK +Claim audited : "TVA 10% sur place / 5,5% a emporter" (dictionary v0.1 note 9) +Domain : compliance (fiscal) +Verdict : CLAIM INEXACT — superseded +Source : BOFiP BOI-ANNX-000495 + BOI-TVA-LIQ-30-10-10 (official doctrine impots.gouv.fr) +Actual rule : 10% for immediate consumption (dine-in OR hot takeaway); + 5.5% for products in resealable containers (bottle, can) / deferred consumption +Confidence : 95% (L1, official text) +``` + +**Model consequence**: VAT rate is an attribute of the `product` (`vat_rate` in per-mille: +100 = 10%, 55 = 5.5%), not of the order or the service mode. Default: 100 (10%). +The 5.5% rate applies to products in resealable containers (bottled water, juice bottles). +VAT is computed line by line; the rate is snapshotted on `order_item.vat_rate_snapshot` +at transaction time to preserve historical integrity if legislation changes. + +`service_mode` is retained on `customer_order` for stats and KPI only (capacity planning, +per-mode revenue breakdown). It has no fiscal computation role. + +### Note 10 — Ingredient configurator and modifier attachment + +`order_item_modifier` attaches to an `order_item` row via `order_item_id`, regardless of +whether the line is a standalone product or a menu. + +For a **standalone product** (`item_type='product'`): `order_item_id` directly identifies +the product being modified. + +For a **menu** (`item_type='menu'`): the modifiable product is the fixed burger, identified +via `order_item.menu_id -> menu.burger_product_id`. The kitchen display resolves: +`modifier.order_item_id -> order_item -> menu -> menu.burger_product_id -> product.name`. +No additional FK column is needed on `order_item_modifier`. This keeps the modifier table +simple and avoids a nullable `target_product_id` column that would only be populated for +menu lines. + +Constraint enforced at app layer: `order_item_modifier` rows for a menu line reference +only ingredients belonging to `menu.burger_product_id` via `product_ingredient`. + +### Note 11 — `menu_slot` eligibility: category filter vs explicit product list + +Two options were considered: +- **Category filter**: `menu_slot.category_id` points to a category; all products in that + category are eligible. Simple, but a category may contain products not offered in this slot + (e.g., a premium drink added to the "drinks" category should not automatically appear in + all menu slots). +- **Explicit product list** `menu_slot_option(menu_slot_id, product_id)` (chosen): each + eligible product is listed explicitly per slot. More verbose at seed time but precise — + no accidental eligibility when the catalogue grows. Enables per-slot pricing overrides + in the future without structural change. + +The explicit list adds one entity (`menu_slot_option`, entity 3.5) but eliminates a class +of correctness bugs. Consistent with the prod-like ambition of this model. + +### Note 12 — `commande_event` dropped + +v0.1 carried a `commande_event` append-only audit table (event sourcing pattern). +Dropped in v0.2 (Decision 1, `revue-alignement-p1.md` §7). + +Rationale: in a restaurant context, the back-office account is shared per workstation, not +individual. Per-person attribution of a state transition has no business value. The actual +need (phase durations, time-of-day stats) is covered by phase timestamps on `customer_order` +(`paid_at`, `delivered_at`, `cancelled_at`) without the complexity of an event store. + +The 4-state machine combined with 3 phase timestamps provides all KPI data needed: +- Time-to-deliver: `delivered_at - paid_at` +- Cancellation rate and timing: `cancelled_at - created_at` +- Volume by hour: `HOUR(created_at)` / `service_day` computation + +For stock audit, `stock_movement` (entity 3.19) provides the append-only audit trail +where it is genuinely needed (inventory reconciliation). + +--- + +## 5. Entity count summary + +| # | Entity | Type | Replaces / new | +|---|---|---|---| +| 1 | `category` | business | v0.1 `categorie` (renamed + translated) | +| 2 | `product` | business | v0.1 `produit` (+ `vat_rate`) | +| 3 | `menu` | business | v0.1 `menu` (+ burger FK, 2 prices) | +| 4 | `menu_slot` | business | new — replaces `menu_produit` fixed composition | +| 5 | `menu_slot_option` | join | new — eligibility list per slot | +| 6 | `ingredient` | business | new — ingredient configurator + stock | +| 7 | `product_ingredient` | join | new — recipe + customization metadata | +| 8 | `allergen` | reference | new — INCO 1169/2011 | +| 9 | `ingredient_allergen` | join | new — maps allergens to ingredients | +| 10 | `customer_order` | business | v0.1 `commande` (renamed, 4-state machine, phase timestamps) | +| 11 | `order_item` | business | v0.1 `ligne_commande` (+ format, vat_rate_snapshot) | +| 12 | `order_item_selection` | business | new — customer menu slot choices | +| 13 | `order_item_modifier` | business | new — ingredient-level modifications | +| 14 | `user` | business | v0.1 `user` (translated field names) | +| 15 | `role` | business | v0.1 `role` (+ default_route, order_source) | +| 16 | `role_visible_source` | join | new — per-role dashboard filter | +| 17 | `permission` | reference | v0.1 `permission` (translated, catalogue frozen) | +| 18 | `role_permission` | join | v0.1 `role_permission` (unchanged) | +| 19 | `stock_movement` | audit | new — append-only stock audit log | + +**Dropped from v0.1**: `commande_event` (replaced by phase timestamps on `customer_order`), +`menu_produit` (replaced by `menu_slot` + `menu_slot_option` model). + +**Total: 19 entities.** + +--- + +*For the ER diagram and cardinality justifications, see [`mcd.md`](mcd.md) — the diagram is +the single source of truth for graphical representation.* From 6c1cede3f0c0da59a66dc635c44141c9121fba3e Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 4 Jun 2026 15:17:33 +0000 Subject: [PATCH 5/8] docs(merise): rewrite MCD to prod-like v0.2 (19 entities across 4 subdomains) Catalogue / Ingredients and Stock / Order / RBAC subdomains, Mermaid erDiagram inline, Merise (min,max) cardinality tables, cross-validation 19/19. --- docs/merise/mcd.md | 686 ++++++++++++++++++++++++++++++--------------- 1 file changed, 455 insertions(+), 231 deletions(-) diff --git a/docs/merise/mcd.md b/docs/merise/mcd.md index 1bdbc8e..80fcbf9 100644 --- a/docs/merise/mcd.md +++ b/docs/merise/mcd.md @@ -1,309 +1,533 @@ -# Modele Conceptuel des Donnees (MCD) - Wakdo +# Conceptual Data Model (MCD) — Wakdo -**Phase Merise** : P1 - Conception, etape 2 (apres dictionnaire de donnees) -**Statut** : v0.1 -**Date** : 2026-04-30 -**Branche** : `feat/p1-conception` +**Merise phase** : P1 - Conception, step 2 (data dictionary first, mantra #33) +**Version** : v0.2 — prod-like, 19 entities +**Date** : 2026-06-04 +**Branch** : `feat/p1-conception` +**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7) +**Author** : BYAN (methodology layer) --- -## 1. Objet du document +## 1. Purpose of this document -Le MCD (Modele Conceptuel des Donnees) formalise les **entites** du domaine -Wakdo, leurs **associations** et les **cardinalites** qui regissent ces -associations. Il est la traduction normalisee du dictionnaire de donnees, et -sert de base au MLD (Modele Logique des Donnees) qui produira le schema -relationnel. +The MCD (Modele Conceptuel des Donnees) formalises the **entities** of the Wakdo domain, +their **associations**, and the **cardinalities** governing those associations. +It is the normalised translation of the data dictionary, and serves as the basis for the +MLD (relational mapping). -A la difference du dictionnaire (qui detaille les attributs et types), le MCD -focalise sur la structure relationnelle : combien de X pour un Y, est-ce -obligatoire, peut-il y avoir des relations sans participants ? +Unlike the dictionary (which details attributes and types), the MCD focuses on relational +structure: how many X per Y, whether participation is mandatory, whether associations carry +their own attributes. -**Sources** : -- `docs/merise/dictionary.md` (entites + attributs identifies) -- `docs/PROJECT_CONTEXT.md` (regles metier : composition menu, parcours commande, RBAC) -- `docs/merise/_sources/` (donnees ecole : 9 categories + 53 produits + 13 menus) +**Sources**: +- `docs/merise/dictionary.md` (v0.2 — 19 entities, source of truth for all names, types, ENUMs) +- `docs/notes/revue-alignement-p1.md` §7 (decision table D1-D8 + stock) +- `docs/PROJECT_CONTEXT.md` (business rules: menu composition, order flow, RBAC, service modes) +- `docs/merise/_sources/` (school data: 9 categories, 53 products, 13 menus) --- -## 2. Notation Merise utilisee +## 2. Merise notation used -### Cardinalites au pied de l'association (style francais) +### Cardinalities at the association foot (French Merise style) -A chaque extremite d'une association, la cardinalite `(min, max)` precise -combien de fois une instance de l'entite participe a l'association. +At each end of an association, the cardinality `(min,max)` states how many times an +instance of the entity participates in the association. ``` -ENTITE_A (min,max) ----[ ASSOCIATION ]---- (min,max) ENTITE_B +ENTITY_A (min,max) ----[ ASSOCIATION ]---- (min,max) ENTITY_B ``` -| Notation | Lecture | Exemple | +| Notation | Reading | Example | |---|---|---| -| `(0,1)` | Optionnel, au plus 1 | Un user a (0,1) avatar | -| `(1,1)` | Obligatoire, exactement 1 | Un produit appartient a (1,1) categorie | -| `(0,N)` | Optionnel, illimite | Une categorie regroupe (0,N) produits | -| `(1,N)` | Obligatoire au moins 1, illimite | Une commande contient (1,N) lignes | +| `(0,1)` | Optional, at most 1 | A stock_movement links to (0,1) customer_order | +| `(1,1)` | Mandatory, exactly 1 | A product belongs to (1,1) category | +| `(0,N)` | Optional, unbounded | A category groups (0,N) products | +| `(1,N)` | At least 1, unbounded | An order contains (1,N) order_items | -Lecture francaise : "une instance de l'entite-source participe au moins MIN -fois et au plus MAX fois a l'association". +Reading: "one instance of the source entity participates at least MIN times and at most +MAX times in the association". -### Convention nommage des associations +### Association naming convention -Verbe a l'infinitif au sens metier, ex : `regroupe`, `compose`, `contient`, -`refere`, `a_pour_role`, `possede`. +Active verb in business terms, e.g.: `groups`, `anchors`, `defines_slot`, `contains`, +`references_product`, `references_menu`, `fills_slot`, `modifies_ingredient`, `logs`, +`holds`, `grants`, `filters_source`, `decrements`. -Les associations qui portent des attributs (= relations N-N enrichies) -deviennent des **entites associatives** au MLD (table de jointure avec -colonnes propres). +N-N associations that carry their own attributes become **associative entities** in the MLD +(join table with own columns). --- -## 3. Vue d'ensemble (diagramme global) +## 3. Decomposition by sub-domain -Diagramme entites-relations dessine dans draw.io, exporte en SVG. Les -sources `.drawio` editables sont dans `_diagrams/`. Cardinalites Merise -`(min,max)` notees directement sur les associations. Recapitulatif des -cardinalites en section 5. +The 19-entity model is split into 4 sub-domains for readability. Beyond approximately +5 entities, a single flat diagram becomes difficult to read; decomposition is the standard +Merise practice for models of this size. -![MCD - Diagramme global](_diagrams/mcd-global.svg) +| Sub-domain | Entities | Count | +|---|---|---| +| Catalogue | category, product, menu, menu_slot, menu_slot_option | 5 | +| Ingredients & Stock | ingredient, product_ingredient, allergen, ingredient_allergen, stock_movement | 5 | +| Order | customer_order, order_item, order_item_selection, order_item_modifier | 4 | +| RBAC | user, role, role_visible_source, permission, role_permission | 5 | -*Source : [`_diagrams/mcd-global.drawio`](_diagrams/mcd-global.drawio)* - -> **A regenerer** : le diagramme global doit etre mis a jour pour inclure l'entite `COMMANDE_EVENT` (11 entites au total) et l'attribut `source` sur `COMMANDE`. Le SVG actuel reflete l'etat anterieur a ces decisions. - -### Lecture rapide - -- Une `CATEGORIE` regroupe `(0,N)` `PRODUIT` ou `MENU` ; un `PRODUIT` ou un - `MENU` appartient a `(1,1)` categorie (chacun cote sa categorie de - rattachement). -- Un `MENU` est compose de `(1,N)` produits (un menu sans composition n'a pas - de sens metier) ; un `PRODUIT` peut faire partie de `(0,N)` menus - (independance). -- Une `COMMANDE` contient `(1,N)` `LIGNE_COMMANDE` (commande vide impossible) ; - une ligne appartient a `(1,1)` commande. -- Une `LIGNE_COMMANDE` refere `(0,1)` `PRODUIT` OU `(0,1)` `MENU` selon le - discriminateur `type_item` (polymorphisme). -- Une `COMMANDE` est journalisee par `(1,N)` `COMMANDE_EVENT` (au moins 1 event `CREATED`, append-only). -- Un `USER` declenche `(0,N)` `COMMANDE_EVENT` (NULL possible si event auto-kiosk). -- Un `USER` a `(1,1)` `ROLE` (un user sans role ne peut pas se connecter) ; - un `ROLE` peut etre porte par `(0,N)` users. -- Un `ROLE` possede `(0,N)` `PERMISSION` via `ROLE_PERMISSION` (matrice N-N). +**Note on the absence of a global diagram**: a single 19-entity ER diagram would be +unreadable and unmaintainable. The sub-domain decomposition below is the intentional +structural choice. The `.drawio` source files will be regenerated from this document as the +single reference once the MCD is stabilised (regeneration tracked in `docs/notes/`). --- -## 4. Decomposition par sous-domaine +## 4. Sub-domain: Catalogue -Le modele est segmente en 3 sous-domaines pour faciliter la lecture et -l'analyse : +### 4.1 Mermaid entity-relationship diagram -1. **Catalogue** : produits, menus, categories, composition -2. **Commande** : commande, lignes, references polymorphiques -3. **RBAC** : users, roles, permissions, mapping +```mermaid +erDiagram + category { + int id PK + varchar name + varchar slug + varchar image_path + smallint display_order + tinyint is_active + } + product { + int id PK + int category_id FK + varchar name + text description + int price_cents + smallint vat_rate + varchar image_path + tinyint is_available + smallint display_order + } + menu { + int id PK + int category_id FK + int burger_product_id FK + varchar name + text description + int price_normal_cents + int price_maxi_cents + varchar image_path + tinyint is_available + smallint display_order + } + menu_slot { + int id PK + int menu_id FK + varchar name + enum slot_type + tinyint is_required + smallint display_order + } + menu_slot_option { + int menu_slot_id FK + int product_id FK + } -### 4.1 Sous-domaine Catalogue + category ||--o{ product : "groups" + category ||--o{ menu : "groups" + menu ||--|| product : "anchors (burger_product_id)" + menu ||--o{ menu_slot : "defines_slot" + menu_slot ||--o{ menu_slot_option : "lists" + product ||--o{ menu_slot_option : "is_eligible_for" +``` -![MCD - Catalogue](_diagrams/mcd-catalogue.svg) +### 4.2 Association cardinalities -*Source : [`_diagrams/mcd-catalogue.drawio`](_diagrams/mcd-catalogue.drawio)* +| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification | +|---|---|---|---|---|---|---| +| C1 | groups (product) | category | (0,N) | product | (1,1) | A category can exist with no products yet (created empty). A product must belong to exactly one category to appear on the kiosk. | +| C2 | groups (menu) | category | (0,N) | menu | (1,1) | Same rationale as C1 for menus. All 13 menus belong to the `menus` category. | +| C3 | anchors | menu | (1,1) | product | (0,N) | Each menu is built around exactly one fixed burger product (`burger_product_id`). A product may anchor 0 or more menus (a burger not used in a menu yet; or a popular burger anchoring several formats). | +| C4 | defines_slot | menu | (1,N) | menu_slot | (1,1) | A menu must define at least one slot (drink, side, sauce) to have customisable composition. A slot belongs to exactly one menu. | +| C5 | lists | menu_slot | (1,N) | menu_slot_option | (1,1) | A slot must list at least one eligible product (otherwise the customer cannot fill it). Each option row belongs to exactly one slot. | +| C6 | is_eligible_for | product | (0,N) | menu_slot_option | (1,1) | A product may be eligible for any number of slots across all menus, or none if it is only sold a la carte. Each option row references exactly one product. | -#### Justification des cardinalites +### 4.3 Notes on the Catalogue sub-domain -| Cote | Cardinalite | Justification | -|---|---|---| -| Categorie -> Produit | `(0,N)` cote Categorie | Une categorie peut etre creee a vide (ex : "petit dejeuner" ajoutee sans produit initial). Maximum illimite (au moins 53 produits dans la source actuelle, repartis sur 9 categories). | -| Categorie -> Produit | `(1,1)` cote Produit | Tout produit doit etre rattache a une categorie pour etre affiche sur la borne. Pas de produit "orphelin". | -| Categorie -> Menu | `(0,N)` cote Categorie | Toutes les categories sauf "menus" ont 0 menu. La categorie "menus" en a 13. | -| Categorie -> Menu | `(1,1)` cote Menu | Tout menu est rattache a la categorie "menus" (par construction du modele). Un menu sans categorie ne s'affiche pas sur la borne. | -| Menu -> MenuProduit | `(1,N)` cote Menu | Un menu doit avoir au moins 1 produit dans sa composition. Sans composition, le menu est invendable. | -| Produit -> MenuProduit | `(0,N)` cote Produit | Un produit peut exister independamment des menus (vente a la carte uniquement). Inversement, un produit peut entrer dans plusieurs menus differents (ex : frites moyennes presentes dans plusieurs combos). | +**`menu_slot` vs category filter**: the explicit eligibility list `menu_slot_option(menu_slot_id, product_id)` was chosen over a category-based filter (`menu_slot.category_id`). Rationale: a product added to the `drinks` category should not automatically appear in every drink slot of every menu. The explicit list avoids accidental eligibility when the catalogue grows (see dictionary note 11). -#### Note sur l'entite associative `MENU_PRODUIT` +**`menu.burger_product_id` as anchor**: the menu references a specific burger product, not a generic slot. This allows the ingredient configurator (sub-domain Ingredients & Stock) to resolve which ingredients are modifiable for a menu line, via `menu -> burger_product_id -> product_ingredient`. -`MENU_PRODUIT` est une entite associative : la relation N-N entre `MENU` et -`PRODUIT` porte deux attributs metier (`role` et `position`). Au MLD, elle -deviendra une table de jointure avec PK composite `(menu_id, produit_id)`. +**Normal / Maxi format**: two prices (`price_normal_cents`, `price_maxi_cents`) on `menu`; format recorded at `order_item.format`. No individual slot-level price differential is stored (see dictionary note 7). -### 4.2 Sous-domaine Commande +--- -![MCD - Commande](_diagrams/mcd-commande.svg) +## 5. Sub-domain: Ingredients & Stock -*Source : [`_diagrams/mcd-commande.drawio`](_diagrams/mcd-commande.drawio)* +### 5.1 Mermaid entity-relationship diagram -> **A regenerer** : le diagramme `mcd-commande.drawio` doit etre mis a jour pour inclure l'entite `COMMANDE_EVENT` (cf. section 4.2.bis ci-dessous) et l'attribut `source` sur `COMMANDE`. Le SVG actuel reflete l'etat anterieur a ces decisions. +```mermaid +erDiagram + product { + int id PK + varchar name + } + ingredient { + int id PK + varchar name + varchar unit + int stock_quantity + smallint pack_size + varchar pack_label + smallint low_stock_threshold + tinyint is_active + } + product_ingredient { + int product_id FK + int ingredient_id FK + smallint quantity + tinyint is_removable + tinyint is_addable + int extra_price_cents + } + allergen { + int id PK + varchar code + varchar name + text description + } + ingredient_allergen { + int ingredient_id FK + int allergen_id FK + } + customer_order { + int id PK + varchar order_number + } + user { + int id PK + varchar email + } + stock_movement { + int id PK + int ingredient_id FK + enum movement_type + int delta + int order_id FK + int user_id FK + varchar note + } -#### Justification des cardinalites + product ||--o{ product_ingredient : "is_composed_of" + ingredient ||--o{ product_ingredient : "appears_in" + ingredient ||--o{ ingredient_allergen : "contains" + allergen ||--o{ ingredient_allergen : "is_present_in" + ingredient ||--o{ stock_movement : "decrements" + customer_order |o--o{ stock_movement : "triggers" + user |o--o{ stock_movement : "logs" +``` -| Cote | Cardinalite | Justification | -|---|---|---| -| Commande -> LigneCommande | `(1,N)` cote Commande | Une commande sans aucune ligne n'a pas de sens metier. Au moment de la validation (passage de `pending_payment` a `paid`), au moins 1 ligne est presente. | -| Commande -> LigneCommande | `(1,1)` cote LigneCommande | Toute ligne appartient a exactement une commande. ON DELETE CASCADE (si commande supprimee, lignes aussi). | -| LigneCommande -> Produit | `(0,N)` cote Produit | Un produit peut etre commande des centaines de fois. Maximum non borne. | -| LigneCommande -> Produit | `(0,1)` cote LigneCommande | Selon `type_item` : si `'produit'` -> 1 produit reference ; si `'menu'` -> 0 (la colonne `produit_id` est NULL). | -| LigneCommande -> Menu | `(0,N)` cote Menu | Symetrique de Produit. | -| LigneCommande -> Menu | `(0,1)` cote LigneCommande | Selon `type_item` : si `'menu'` -> 1 menu reference ; si `'produit'` -> 0. | -| Commande -> CommandeEvent | `(1,N)` cote Commande | Toute commande a au moins 1 event (CREATED) ; en pratique 5-8 events sur tout son cycle de vie. | -| Commande -> CommandeEvent | `(1,1)` cote CommandeEvent | Chaque event appartient a exactement une commande. ON DELETE CASCADE. | -| User -> CommandeEvent | `(0,N)` cote User | Un equipier peut declencher 0 a N events (un nouveau user n'a encore rien fait). | -| User -> CommandeEvent | `(0,1)` cote CommandeEvent | NULL si event auto (kiosk paye via CB sans equipier) ou si le user a ete supprime (ON DELETE SET NULL preserve l'audit). | +### 5.2 Association cardinalities -#### Note sur le polymorphisme +| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification | +|---|---|---|---|---|---|---| +| I1 | is_composed_of | product | (0,N) | product_ingredient | (1,1) | A product may have no ingredients entered in the system yet (catalogue row exists before recipe is entered). A recipe row belongs to exactly one product. | +| I2 | appears_in | ingredient | (1,N) | product_ingredient | (1,1) | An ingredient in active use appears in at least one product recipe. Each recipe row references exactly one ingredient. Newly created ingredients with no recipe row yet are modelled as (0,N) from a pure structural standpoint; the business rule of (1,N) applies to ingredients in production use. | +| I3 | contains (allergens) | ingredient | (0,N) | ingredient_allergen | (1,1) | An ingredient may contain no regulated allergens (e.g., pure salt). Each allergen-link row belongs to one ingredient. | +| I4 | is_present_in | allergen | (0,N) | ingredient_allergen | (1,1) | An allergen may initially have no linked ingredients (seed: allergen catalogue is complete before recipe data is entered). Each link row references one allergen. | +| I5 | decrements | ingredient | (0,N) | stock_movement | (1,1) | All movements affect exactly one ingredient. An ingredient may have no stock movement rows yet if it was recently created and no orders have been placed. Each movement row references exactly one ingredient. | +| I6 | triggers | customer_order | (0,1) | stock_movement | (0,N) | A `sale` or `cancellation` movement references the originating order. A `restock` or `inventory_correction` has no order (NULL). A given order triggers movements across all its ingredients; an order still `pending_payment` has triggered no movement yet. | +| I7 | logs | user | (0,1) | stock_movement | (0,N) | Automated sale decrements have no user (NULL). Manual restocks and corrections are attributed to a user. A user may log any number of movements. | -La cardinalite `(0,1)` cote LigneCommande pour les deux associations -`refere_si_type_produit` et `refere_si_type_menu` reflete le polymorphisme : -**exactement une** des deux references est non-nulle, l'autre est nulle. -Cette regle d'exclusivite est a renforcer au MLD via une contrainte CHECK -SQL ou une regle applicative : +### 5.3 Notes on the Ingredients & Stock sub-domain +**`product_ingredient` as an associative entity**: the N-N association between `product` and `ingredient` carries four attributes (`quantity`, `is_removable`, `is_addable`, `extra_price_cents`). It becomes a join table in the MLD with composite PK `(product_id, ingredient_id)`. + +**`ingredient_allergen` as a pure join table**: no own attributes. The allergen set for a product is computed at query time by joining `product_ingredient -> ingredient_allergen -> allergen`; no manual per-product entry is needed. + +**`stock_movement` immutability**: this table is append-only. No UPDATE or DELETE is permitted at application layer. Corrections are new rows with `movement_type = 'inventory_correction'` and a signed `delta`. + +**Low-stock alert**: computed at display time (`stock_quantity <= low_stock_threshold`); no additional stored column. + +--- + +## 6. Sub-domain: Order + +### 6.1 Mermaid entity-relationship diagram + +```mermaid +erDiagram + customer_order { + int id PK + varchar order_number + enum source + enum service_mode + enum status + int total_ht_cents + int total_vat_cents + int total_ttc_cents + datetime paid_at + datetime delivered_at + datetime cancelled_at + } + order_item { + int id PK + int order_id FK + enum item_type + int product_id FK + int menu_id FK + enum format + varchar label_snapshot + int unit_price_cents_snapshot + smallint vat_rate_snapshot + smallint quantity + } + order_item_selection { + int id PK + int order_item_id FK + int menu_slot_id FK + int product_id FK + varchar label_snapshot + } + order_item_modifier { + int id PK + int order_item_id FK + int ingredient_id FK + enum action + int extra_price_cents + } + product { + int id PK + varchar name + } + menu { + int id PK + varchar name + } + menu_slot { + int id PK + varchar name + } + ingredient { + int id PK + varchar name + } + + customer_order ||--o{ order_item : "contains" + order_item }o--o| product : "references_product" + order_item }o--o| menu : "references_menu" + order_item ||--o{ order_item_selection : "fills_slot" + order_item ||--o{ order_item_modifier : "modifies_ingredient" + menu_slot ||--o{ order_item_selection : "slot_filled_by" + product ||--o{ order_item_selection : "chosen_for_slot" + ingredient ||--o{ order_item_modifier : "modified_by" +``` + +### 6.2 Association cardinalities + +| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification | +|---|---|---|---|---|---|---| +| O1 | contains | customer_order | (1,N) | order_item | (1,1) | An order without at least one line has no business meaning. A line belongs to exactly one order. ON DELETE CASCADE: if the order is purged, its lines go with it. | +| O2 | references_product | order_item | (0,1) | product | (0,N) | When `item_type = 'product'`, `product_id` is non-null (1 product referenced). When `item_type = 'menu'`, `product_id` is NULL (0). A product may appear in any number of order lines across history. | +| O3 | references_menu | order_item | (0,1) | menu | (0,N) | Symmetric to O2 for the menu discriminator branch. Exactly one of O2/O3 is active per line (CHECK constraint in MLD). | +| O4 | fills_slot | order_item | (0,N) | order_item_selection | (1,1) | A `menu`-type order line has one selection per slot (typically 2-3). A `product`-type line has no selections (0). Each selection row belongs to exactly one order line. | +| O5 | slot_filled_by | menu_slot | (0,N) | order_item_selection | (1,1) | A slot definition may have been chosen many times across historical orders (0,N). Each selection row references exactly one slot. ON DELETE RESTRICT: preserves historical records if the slot definition is later changed. | +| O6 | chosen_for_slot | product | (0,N) | order_item_selection | (1,1) | A product may have been selected for many slot choices across history. Each selection references one product. | +| O7 | modifies_ingredient | order_item | (0,N) | order_item_modifier | (1,1) | An order line may have any number of ingredient modifications (remove onion, add cheese). Each modifier row belongs to one order line. | +| O8 | modified_by | ingredient | (0,N) | order_item_modifier | (1,1) | An ingredient may have been modified in many order lines across history. Each modifier references one ingredient. | + +### 6.3 Notes on the Order sub-domain + +**Polymorphism on `order_item`**: each line references either a `product` or a `menu` (not both, not neither). The discriminator `item_type` ENUM drives which FK is populated. The mutual exclusivity is enforced by a CHECK constraint in the MLD. This pattern (2 nullable FKs + discriminator + CHECK) is a standard relational approach to single-table inheritance without a separate table per type. + +**`order_item_selection` (menu slot choices)**: captures which product the customer chose for each slot of a menu line. One row per slot filled. Used for KPI analysis (most popular drink/side combinations). The `label_snapshot` preserves the product name at transaction time. + +**`order_item_modifier` (ingredient modifications)**: attaches to an `order_item` regardless of whether the line is a standalone product or a menu. For a menu line, the modifiable product is the fixed burger, resolved via `order_item.menu_id -> menu.burger_product_id` (see dictionary note 10). No additional FK column is needed on `order_item_modifier`. + +**Price snapshots**: `label_snapshot`, `unit_price_cents_snapshot`, and `vat_rate_snapshot` on `order_item` preserve the state at transaction time. If a product is later renamed or repriced, historical order data remains consistent. ON DELETE RESTRICT on `product_id` and `menu_id` is a secondary safeguard. + +**`service_day` computation** (KPI grouping): not stored as a column. Computed at query time: ```sql -CHECK ( - (type_item = 'produit' AND produit_id IS NOT NULL AND menu_id IS NULL) - OR (type_item = 'menu' AND menu_id IS NOT NULL AND produit_id IS NULL) -) +CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END +``` +Cutoff: 10:00. The generated-column formula with `INTERVAL 4 HOUR 30 MINUTE` from the v0.1 MLD +was incorrect and is dropped (decision D6, `revue-alignement-p1.md` §7). + +**`source = 'drive' => service_mode = 'drive'`**: cross-constraint. A drive-channel order can +only have `service_mode = 'drive'`. Enforced at application layer (and optionally as a CHECK in +the MLD). + +**4-state machine** (`pending_payment -> paid -> delivered` + `cancelled`): +`preparing` and `ready` are dropped (decision D4, `revue-alignement-p1.md` §7). KPI timing is +`delivered_at - paid_at`; KDS colour coding is computed from `NOW() - paid_at`. + +--- + +## 7. Sub-domain: RBAC + +### 7.1 Mermaid entity-relationship diagram + +```mermaid +erDiagram + user { + int id PK + varchar email + varchar password_hash + varchar first_name + varchar last_name + int role_id FK + tinyint is_active + datetime last_login_at + } + role { + int id PK + varchar code + varchar label + text description + varchar default_route + enum order_source + tinyint is_active + } + role_visible_source { + int role_id FK + enum source + } + permission { + int id PK + varchar code + varchar label + text description + } + role_permission { + int role_id FK + int permission_id FK + } + + user }o--|| role : "holds" + role ||--o{ role_visible_source : "sees_source" + role ||--o{ role_permission : "grants" + permission ||--o{ role_permission : "granted_to" ``` -Voir `docs/notes/polymorphic-fk-snapshots.md` pour le detail du choix de -modelisation polymorphique. +### 7.2 Association cardinalities -#### 4.2.bis Journal d'audit `COMMANDE_EVENT` (event sourcing simplifie) +| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification | +|---|---|---|---|---|---|---| +| R1 | holds | user | (1,1) | role | (0,N) | A user must have exactly one role to access the back-office. A role may have no current users (created but not yet assigned). ON DELETE RESTRICT on `role_id`: a role cannot be deleted while users hold it. | +| R2 | sees_source | role | (0,N) | role_visible_source | (1,1) | A role may see 0 or more order sources on the preparation dashboard (admin/manager use a global view with no source filter). Each visibility row belongs to exactly one role. | +| R3 | grants | role | (0,N) | role_permission | (1,1) | A role may have no permissions (a newly created role before assignment) or many. Each mapping row belongs to one role. | +| R4 | granted_to | permission | (0,N) | role_permission | (1,1) | A permission may be granted to no roles yet (declared at seed, not yet distributed) or to several. Each mapping row references one permission. | -`COMMANDE_EVENT` est une entite append-only qui journalise chaque changement d'etat d'une commande, avec l'auteur de la transition (un `USER` ou NULL si auto). Pattern event sourcing simplifie (cf. note 10 du dictionnaire). +### 7.3 Notes on the RBAC sub-domain -Trois proprietes invariantes : +**RBAC architecture**: roles are dynamic (creatable and modifiable via admin UI). Permissions are static (declared in migration, tied to application code). Application code tests permissions, not role names: adding a new role with the right permissions requires no code change (permission-driven, per Sandhu/NIST RBAC model — decision D4, `revue-alignement-p1.md` §7). -1. **Append-only** : aucun UPDATE / DELETE applicatif sur `commande_event`. Garantie d'integrite de l'audit. -2. **Lien fort a la commande** : `ON DELETE CASCADE` cote `commande_id` (si la commande disparait, son journal aussi). -3. **Lien faible a l'user** : `ON DELETE SET NULL` cote `user_id` (si un equipier est supprime, les events restent avec `user_id = NULL` ; l'audit reste consultable, seule l'attribution individuelle est perdue). +**`role.order_source`**: when a counter or drive staff member creates an order, the `source` column on `customer_order` is automatically populated from their role's `order_source`. NULL for admin and manager (they can create on behalf of any channel). -La contrainte croisee `(source, mode_consommation)` introduite par l'attribut `source` sur `COMMANDE` (cf. dictionnaire note 8) est verifiee au MLT lors de la creation de la commande, pas au MCD. +**`role.default_route`**: the landing screen for each role, stored in the database. Front-end routing reads this value at login; no role name is hardcoded in routing logic. -### 4.3 Sous-domaine RBAC +**`role_visible_source`**: a pure join table linking a role to the set of order sources visible on the preparation dashboard. A `kitchen` role sees all three sources; a `counter` role sees `kiosk` and `counter`; a `drive` role sees only `drive`. -![MCD - RBAC](_diagrams/mcd-rbac.svg) +**`role_permission`** and **`role_visible_source`** both use composite PKs. ON DELETE CASCADE on both FKs of `role_permission` (deleting a role or a permission removes its mappings). ON DELETE CASCADE on `role_id` of `role_visible_source`. -*Source : [`_diagrams/mcd-rbac.drawio`](_diagrams/mcd-rbac.drawio)* - -#### Justification des cardinalites - -| Cote | Cardinalite | Justification | -|---|---|---| -| User -> Role | `(1,1)` cote User | Un user doit avoir un role pour acceder au back-office. Pas de connexion sans role. | -| User -> Role | `(0,N)` cote Role | Un role peut exister sans aucun user (ex : nouveau role cree dans l'UI admin avant d'etre assigne). | -| Role -> Permission (via ROLE_PERMISSION) | `(0,N)` cote Role | Un role peut avoir 0 permission (role "vide" ou "en construction") jusqu'a toutes les permissions (admin). | -| Role -> Permission (via ROLE_PERMISSION) | `(0,N)` cote Permission | Une permission peut etre assignee a 0 role (permission declaree mais pas encore distribuee) ou a plusieurs roles. | - -#### Note sur le modele RBAC - -Le RBAC retenu est **dynamique cote roles** (creables/modifiables via UI -admin) et **statique cote permissions** (declarees en migration, liees au -code applicatif). Voir `docs/notes/rbac-roles-permissions.md` pour le detail -du choix. +**Seed roles** (5 roles, frozen at DDL; extendable without code change): +`admin`, `manager`, `kitchen`, `counter`, `drive`. --- -## 5. Recapitulatif global des cardinalites +## 8. Cross-validation MCD <-> dictionary -| # | Association | Cote A | Cardinalite A | Cote B | Cardinalite B | -|---|---|---|---|---|---| -| 1 | regroupe (Categorie -> Produit) | Categorie | (0,N) | Produit | (1,1) | -| 2 | regroupe (Categorie -> Menu) | Categorie | (0,N) | Menu | (1,1) | -| 3 | compose (Menu <-> Produit via MenuProduit) | Menu | (1,N) | Produit | (0,N) | -| 4 | contient (Commande -> LigneCommande) | Commande | (1,N) | LigneCommande | (1,1) | -| 5 | refere_si_type_produit (LigneCommande -> Produit) | LigneCommande | (0,1) | Produit | (0,N) | -| 6 | refere_si_type_menu (LigneCommande -> Menu) | LigneCommande | (0,1) | Menu | (0,N) | -| 7 | journalise (Commande -> CommandeEvent) | Commande | (1,N) | CommandeEvent | (1,1) | -| 8 | declenche (User -> CommandeEvent) | User | (0,N) | CommandeEvent | (0,1) | -| 9 | a_pour_role (User -> Role) | User | (1,1) | Role | (0,N) | -| 10 | possede (Role <-> Permission via RolePermission) | Role | (0,N) | Permission | (0,N) | +Verification that all 19 dictionary entities appear in the MCD and vice versa. + +| # | Dictionary entity (section 3) | Sub-domain in MCD | Present | +|---|---|---|---| +| 1 | `category` (3.1) | Catalogue | Yes | +| 2 | `product` (3.2) | Catalogue + Ingredients + Order | Yes | +| 3 | `menu` (3.3) | Catalogue + Order | Yes | +| 4 | `menu_slot` (3.4) | Catalogue + Order | Yes | +| 5 | `menu_slot_option` (3.5) | Catalogue | Yes | +| 6 | `ingredient` (3.6) | Ingredients + Order | Yes | +| 7 | `product_ingredient` (3.7) | Ingredients | Yes | +| 8 | `allergen` (3.8) | Ingredients | Yes | +| 9 | `ingredient_allergen` (3.9) | Ingredients | Yes | +| 10 | `customer_order` (3.10) | Order | Yes | +| 11 | `order_item` (3.11) | Order | Yes | +| 12 | `order_item_selection` (3.12) | Order | Yes | +| 13 | `order_item_modifier` (3.13) | Order | Yes | +| 14 | `user` (3.14) | RBAC | Yes | +| 15 | `role` (3.15) | RBAC | Yes | +| 16 | `role_visible_source` (3.16) | RBAC | Yes | +| 17 | `permission` (3.17) | RBAC | Yes | +| 18 | `role_permission` (3.18) | RBAC | Yes | +| 19 | `stock_movement` (3.19) | Ingredients & Stock | Yes | + +**Result**: 19/19 entities traced. No entity from the dictionary is absent from the MCD. +No entity in the MCD falls outside the dictionary. + +**Entities appearing in multiple sub-domains** (cross-domain shared entities): +- `product`: Catalogue (sold item, slot eligibility) + Ingredients (recipe) + Order (line reference, slot choice) +- `menu`: Catalogue (definition, slots) + Order (line reference) +- `menu_slot`: Catalogue (slot definition) + Order (slot choices via `order_item_selection`) +- `ingredient`: Ingredients (recipe, stock) + Order (modifiers) +- `customer_order`: Order (order lifecycle) + Ingredients (stock movement trigger) +- `user`: RBAC (authentication) + Ingredients (stock movement author) + +This is expected in a normalised model. The sub-domain split is for readability; the actual +relational schema is a unified graph. --- -## 6. Cross-validation avec le dictionnaire de donnees +## 9. Decisions deferred to the MLD -Verification que chaque entite du dictionnaire est presente dans le MCD et -inversement. +The MCD remains at the conceptual level. The following decisions are deferred to the MLD: -| Entite dictionnaire (section 3) | Presente dans MCD ? | Sous-domaine | -|---|---|---| -| `categorie` (3.1) | Oui | Catalogue | -| `produit` (3.2) | Oui | Catalogue | -| `menu` (3.3) | Oui | Catalogue | -| `menu_produit` (3.4) | Oui (entite associative) | Catalogue | -| `commande` (3.5) | Oui | Commande | -| `ligne_commande` (3.6) | Oui | Commande | -| `commande_event` (3.7) | Oui (journal d'audit) | Commande | -| `user` (3.8) | Oui | RBAC | -| `role` (3.9) | Oui | RBAC | -| `permission` (3.10) | Oui | RBAC | -| `role_permission` (3.11) | Oui (entite associative) | RBAC | - -Coherence : 100%. Aucune entite du dictionnaire n'est absente du MCD, aucune -entite du MCD n'est en plus du dictionnaire. +1. **Resolution of associative entities into tables**: `product_ingredient`, `menu_slot_option`, + `ingredient_allergen`, `role_visible_source`, `role_permission` become join tables with + composite PKs. +2. **Technical PK vs business identifier**: `id INT UNSIGNED AUTO_INCREMENT` on all main entities. + `customer_order` additionally carries `order_number VARCHAR(20) UNIQUE` (human-readable, + format `K/C/D-YYYY-MM-DD-NNN` per channel). +3. **ON DELETE rules**: CASCADE vs RESTRICT vs SET NULL. Detailed in the MLD. +4. **CHECK constraints**: polymorphism exclusivity on `order_item`, cross-constraint + `source/service_mode` on `customer_order`, arithmetic invariant on totals. +5. **Indexes**: not discussed at MCD level. Defined in the MLD for frequent query patterns. +6. **`service_day` formula**: applicative CASE expression, not a stored generated column. + Documented in the MLD. --- -## 7. Decisions reportees au MLD +## 10. MCD <-> MCT coherence (mantra #34) -Le MCD reste au niveau conceptuel. Les decisions suivantes sont reportees au -MLD (modele logique des donnees, etape suivante) : +Pre-validation: each entity participates in at least one treatment. -1. **Resolution des entites associatives en tables** : `MENU_PRODUIT` et - `ROLE_PERMISSION` deviendront des tables de jointure avec PK composite. -2. **Choix des PK techniques vs metier** : pour les entites principales, PK - technique `id INT UNSIGNED AUTO_INCREMENT`. Pour `commande`, garder - un identifiant metier `numero` UNIQUE en plus de l'id technique. -3. **Index techniques** : non discutes au MCD (couche logique). Au MLD : - index sur les FK, sur les colonnes `est_actif`/`est_disponible`, sur les - colonnes utilisees dans les `WHERE`/`ORDER BY` frequents (`created_at`, - `statut`). -4. **Contraintes CHECK SQL** : la regle d'exclusivite polymorphique sur - `LIGNE_COMMANDE` sera materialisee via CHECK en MariaDB 10.2+, ou via - trigger sur versions anteriures. -5. **Triggers / vues** : non identifies au MCD. A evaluer au MLD pour les - denormalisations utiles (vue `commande_avec_total` ?). - ---- - -## 8. Coherence avec le MCT - -Le MCT (Modele Conceptuel des Traitements) decrira les operations metier qui -manipulent ces entites : - -- **Composer panier** (kiosk) : creation de COMMANDE statut `pending_payment` + insertion COMMANDE_EVENT `CREATED` -- **Valider payment** : transition COMMANDE statut `pending_payment` -> `paid` + insertion COMMANDE_EVENT `PAID` -- **Preparer commande** (cuisine) : transition `paid` -> `preparing` + insertion COMMANDE_EVENT `PREPARING_STARTED` -- **Marquer pret** : transition `preparing` -> `ready` + insertion COMMANDE_EVENT `READY` -- **Remettre client** : transition `ready` -> `delivered` + insertion COMMANDE_EVENT `DELIVERED` -- **Annuler** : transition vers `cancelled` (depuis tout statut sauf `delivered`) + insertion COMMANDE_EVENT `CANCELLED` -- **CRUD admin** : operations sur PRODUIT, MENU, CATEGORIE, USER, ROLE - -Cross-validation MCD <-> MCT (mantra #34) : verifier au MCT que chaque -entite du MCD participe a au moins un traitement, et que chaque traitement -manipule des entites existantes du MCD. - -Pre-validation rapide (intuitive, a re-valider au MCT) : - -| Entite MCD | Au moins 1 traitement attendu ? | +| Entity | Expected treatment(s) | |---|---| -| Categorie | Oui (CRUD admin) | -| Produit | Oui (CRUD admin + ajout panier) | -| Menu | Oui (CRUD admin + ajout panier) | -| MenuProduit | Oui (composition menu admin) | -| Commande | Oui (cycle de vie complet) | -| LigneCommande | Oui (creation panier) | -| CommandeEvent | Oui (insere a chaque transition de statut) | -| User | Oui (CRUD admin + login + declenchement events) | -| Role | Oui (CRUD admin + assignation) | -| Permission | Oui (consultation + assignation matrice) | -| RolePermission | Oui (matrice admin) | +| `category` | Admin CRUD | +| `product` | Admin CRUD + kiosk cart add | +| `menu` | Admin CRUD + kiosk cart add | +| `menu_slot` | Admin CRUD (menu composition) | +| `menu_slot_option` | Admin CRUD (slot eligibility management) | +| `ingredient` | Admin CRUD + stock movements | +| `product_ingredient` | Admin recipe management | +| `allergen` | Admin CRUD (seed: read-only catalogue) | +| `ingredient_allergen` | Admin allergen mapping | +| `customer_order` | Full order lifecycle (create -> pay -> deliver / cancel) | +| `order_item` | Cart building, line creation at validation | +| `order_item_selection` | Menu slot selection during cart building | +| `order_item_modifier` | Ingredient modification during cart building | +| `user` | Admin CRUD + login | +| `role` | Admin CRUD + user assignment | +| `role_visible_source` | Admin role configuration | +| `permission` | Admin permission matrix management | +| `role_permission` | Admin permission matrix management | +| `stock_movement` | Automatic at `paid` transition; manual restock and inventory correction | + +Cross-validation MCD <-> MCT (mantra #34) to be completed exhaustively in `mct.md` +once the MCT is updated to the 4-state machine and 19-entity model. --- -## 9. A faire au prochain sprint (MCT) +## 11. Note on .drawio diagram regeneration -- Lister exhaustivement les operations metier (acteurs, evenements - declencheurs, regles de declenchement) -- Modeliser les flux entre acteurs (client kiosk, equipier comptoir, equipier - cuisine, equipier drive, manager, admin) -- Identifier les synchronisations (ex : passage de `paid` a `preparing` peut - etre manuel cuisine ou automatique selon volume) -- Cross-valider MCD <-> MCT exhaustivement +The `.drawio` XML sources in `docs/merise/_diagrams/` reflect the v0.1 model (11 entities, +French naming). They are scheduled for regeneration from this v0.2 MCD as a separate task. +Until regenerated, this Markdown document is the authoritative conceptual model. The Mermaid +`erDiagram` blocks in sections 4-7 render natively on GitHub and serve as the interim +graphical reference. From 36332b42842f4ffb6f69b910eee93e130a001b31 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 4 Jun 2026 15:17:33 +0000 Subject: [PATCH 6/8] docs(merise): rewrite MLD to prod-like v0.2 (19 tables) Polymorphic order_item (exclusivity CHECK), composite-PK join tables, service_day as query-time CASE (10h cutoff, generated column dropped), line-by-line VAT, ON DELETE rules, recommended indexes. --- docs/merise/mld.md | 1139 +++++++++++++++++++++++++++++--------------- 1 file changed, 760 insertions(+), 379 deletions(-) diff --git a/docs/merise/mld.md b/docs/merise/mld.md index 8e49cd4..a48d6e2 100644 --- a/docs/merise/mld.md +++ b/docs/merise/mld.md @@ -1,525 +1,906 @@ -# Modele Logique des Donnees (MLD) - Wakdo +# Logical Data Model (MLD) — Wakdo -**Phase Merise** : P1 - Conception, etape 5 (apres MCD, MCT, MLT) -**Statut** : v0.1 -**Date** : 2026-05-28 -**Branche** : `feat/p1-conception` -**Auteur methodologie** : BYAN +**Merise phase** : P1 - Conception, step 5 (after MCD, MCT, MLT) +**Version** : v0.2 — prod-like, 19 tables +**Date** : 2026-06-04 +**Branch** : `feat/p1-conception` +**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7) +**Author** : BYAN (methodology layer) --- -## 1. Objet du document +## 1. Purpose of this document -Le MLD transcrit le MCD en schema relationnel formel : 1 entite -> 1 table, chaque association traduite selon sa cardinalite, contraintes referentielles materialisees, index dimensionnes pour les acces frequents. +The MLD transcribes the MCD into a formal relational schema: 1 entity -> 1 table, each +association translated according to its cardinality, referential constraints materialised, +indexes sized for frequent access patterns. -C'est l'etape qui transforme la modelisation conceptuelle en specification implementable. Le DDL SQL (`db/migrations/0001_init_schema.sql`) sera derive directement de ce document a P2. +This is the step that transforms conceptual modelling into an implementable specification. +The DDL SQL (`db/migrations/0001_init_schema.sql`) will be derived directly from this +document at P2. -**Source** : `dictionary.md` (types et contraintes par attribut), `mcd.md` (entites + cardinalites + decisions reportees), `mct.md` (operations + entites manipulees), `mlt.md` (regles de gestion + transitions + protection concurrence). +**Sources**: +- `docs/merise/dictionary.md` (v0.2 — types and constraints per attribute, source of truth) +- `docs/merise/mcd.md` (v0.2 — entities + cardinalities + deferred decisions) +- `docs/notes/revue-alignement-p1.md` §7 (decision table D1-D8 + stock) -**Cibles** : +**Target platform**: - MariaDB 11.4 LTS (cf. `docker-compose.yml` service `wakdo-db`) -- Engine InnoDB (ACID, FKs, row-level locking, CHECK depuis 10.2.1) -- Charset `utf8mb4` collation `utf8mb4_unicode_ci` +- Engine InnoDB (ACID, FK support, row-level locking, CHECK from 10.2.1) +- Charset `utf8mb4`, collation `utf8mb4_unicode_ci` --- -## 2. Conventions de notation +## 2. Notation conventions -### Notation relationnelle +### Relational notation ``` -TABLE_NAME (col1, col2, #col_fk, [col_optionnelle]) +table_name (col1, col2, #col_fk, [col_nullable]) - PK : col1 - UK : col2 - FK : col_fk -> AUTRE_TABLE(id) + PK : col1 + UK : col2 + FK : col_fk -> other_table(id) ON DELETE + IDX : (col_a, col_b) + CHK : ``` -| Symbole | Signification | +| Symbol | Meaning | |---|---| -| `col` | Colonne NOT NULL | -| `[col]` | Colonne nullable | -| `#col` | Colonne FK (sans le diese : non-FK) | +| `col` | NOT NULL column | +| `[col]` | Nullable column | +| `#col` | FK column | -Cette notation reste proche de l'usage Merise francais (UNIRIS, ouvrages Nanci/Espinasse) : la cle primaire est soulignee dans les documents classiques, ici on prefixe par `PK` pour la portabilite ASCII. +Notation follows Merise French usage (Nanci/Espinasse convention adapted for ASCII). -### Types +### Type summary -Les types SQL exacts sont definis dans `dictionary.md` section 2 (Conventions generales) et reprecises dans chaque section de cette MLD. Conventions retenues : - -- `INT UNSIGNED AUTO_INCREMENT` pour toutes les PK techniques -- `INT UNSIGNED` pour tous les montants en centimes (anti-FLOAT cf. dictionary note 1) -- `VARCHAR()` avec longueur calibree selon dictionary note 7 -- `ENUM(...)` pour les valeurs metier stables (cf. dictionary note 2) -- `DATETIME` pour les timestamps (pas TIMESTAMP qui ferait du fuseau auto-implicite) +All exact types are defined in `dictionary.md` section 2. Conventions retained: +- `INT UNSIGNED AUTO_INCREMENT` for all technical PKs +- `INT UNSIGNED` for all monetary amounts in cents (anti-FLOAT, see dictionary note 1) +- `SMALLINT UNSIGNED` for `vat_rate` per-mille values (55 or 100) +- `ENUM(...)` for stable business values (see dictionary note 2) +- `DATETIME` for timestamps (not TIMESTAMP, which implicitly converts to UTC in MariaDB) --- -## 3. Regles de traduction MCD -> MLD +## 3. MCD -> MLD translation rules applied -Les regles classiques de passage MCD -> MLD appliquees : +### 3.1 Entity -> Table -### 3.1 Entite -> Table +Each MCD entity becomes one table. The conceptual identifier `id` becomes PK +`INT UNSIGNED AUTO_INCREMENT`. Attributes retain their names and types. -Chaque entite du MCD devient une table. L'identifiant conceptuel `id` devient PK technique `INT UNSIGNED AUTO_INCREMENT`. Les attributs gardent leurs noms et types. +### 3.2 `(1,1) - (1,N)` association -> simple FK -### 3.2 Association `(1,1) - (1,N)` -> FK simple +The entity on the `(1,1)` side carries the FK toward the `(0,N)` or `(1,N)` entity. -L'entite cote `(1,1)` porte la FK vers l'entite cote `(0,N)` ou `(1,N)`. Exemple : +### 3.3 `(0,N) - (0,N)` or `(1,N) - (1,N)` association -> join table -``` -CATEGORIE (1,1) <--regroupe--> (0,N) PRODUIT +The association becomes its own table with a composite PK of the two FKs. Applied to: +`product_ingredient`, `menu_slot_option`, `ingredient_allergen`, +`role_visible_source`, `role_permission`. -devient +### 3.4 Associative entity with own attributes -> join table with columns -CATEGORIE (id, libelle, ...) -- pas de FK -PRODUIT (id, #categorie_id, ...) -- FK vers CATEGORIE -``` +When an N-N association carries its own attributes, it becomes a table with those attributes +in addition to the composite FK PK. Applied to `product_ingredient`. -### 3.3 Association `(0,N) - (0,N)` ou `(1,N) - (1,N)` -> Table de jointure +### 3.5 Polymorphism -> 2 nullable FKs + discriminator + CHECK -L'association devient sa propre table avec PK composite des deux FKs. Exemple : - -``` -MENU (1,N) <--compose--> (0,N) PRODUIT (via MENU_PRODUIT) - -devient - -MENU_PRODUIT (#menu_id, #produit_id, role, position) - PK composite : (menu_id, produit_id) -``` - -### 3.4 Association porteuse d'attributs -> Table associative - -Si une association MCD porte des attributs propres (`role`, `position` sur `compose`), elle devient table meme si elle pourrait theoriquement etre une FK. Cas applique a `MENU_PRODUIT` et `ROLE_PERMISSION`. - -### 3.5 Polymorphisme -> 2 FKs nullables + discriminateur - -`LIGNE_COMMANDE` -> (`PRODUIT` ou `MENU`) traduit en 2 colonnes FK nullable + 1 colonne discriminateur : - -``` -LIGNE_COMMANDE (id, #commande_id, type_item, [#produit_id], [#menu_id], ...) - CHECK ((type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL) - OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL)) -``` - -Cf. `docs/notes/polymorphic-fk-snapshots.md` pour la justification. - -### 3.6 Audit (event sourcing) -> Table dediee - -`COMMANDE_EVENT` est une table append-only, traduction directe de l'entite MCD 3.7. Aucune denormalisation `user_id` sur `commande` (cf. dictionary note 10). +`order_item` references either `product` or `menu`. Translated as 2 nullable FK columns + +1 discriminator ENUM + 1 CHECK constraint enforcing mutual exclusivity. --- -## 4. Schema relationnel formel +## 4. Relational schema (19 tables) -Les 11 tables qui composent le schema Wakdo, ordonnees par dependance (les tables sans FK d'abord, puis les tables qui dependent d'elles). +Tables are ordered by dependency (no-FK tables first, then tables that depend on them). -### 4.1 `categorie` +--- + +### 4.1 `category` ``` -categorie (id, libelle, slug, image_path, ordre, est_actif, created_at, updated_at) +category (id, name, slug, [image_path], display_order, is_active, created_at, updated_at) - PK : id - UK : libelle - UK : slug + PK : id + UK : name + UK : slug ``` -Types : -- `id INT UNSIGNED AUTO_INCREMENT` -- `libelle VARCHAR(80) NOT NULL` -- `slug VARCHAR(60) NOT NULL` -- `image_path VARCHAR(255) NULL` (cf. dictionary note 11) -- `ordre SMALLINT UNSIGNED NOT NULL DEFAULT 0` -- `est_actif TINYINT(1) NOT NULL DEFAULT 1` -- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` -- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `name` | VARCHAR(60) | NO | Unique display name (see dict 3.1) | +| `slug` | VARCHAR(60) | NO | URL slug, e.g. `burgers` | +| `image_path` | VARCHAR(255) | YES | Relative path from public root | +| `display_order` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Kiosk display order | +| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Soft deactivation | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit | +| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit | -Aucune FK. Table racine du sous-domaine Catalogue. +No FK. Root table for the Catalogue sub-domain. -### 4.2 `produit` +--- + +### 4.2 `product` ``` -produit (id, #categorie_id, libelle, [description], prix_ttc_cents, [image_path], - est_disponible, ordre, created_at, updated_at) +product (id, #category_id, name, [description], price_cents, vat_rate, + [image_path], is_available, display_order, created_at, updated_at) - PK : id - FK : categorie_id -> categorie(id) ON DELETE RESTRICT - IDX : (categorie_id, est_disponible, ordre) + PK : id + FK : category_id -> category(id) ON DELETE RESTRICT + IDX : (category_id, is_available, display_order) + CHK : price_cents > 0 + CHK : vat_rate IN (55, 100) ``` -Types : -- `id INT UNSIGNED AUTO_INCREMENT` -- `categorie_id INT UNSIGNED NOT NULL` -- `libelle VARCHAR(120) NOT NULL` -- `description TEXT NULL` -- `prix_ttc_cents INT UNSIGNED NOT NULL CHECK (prix_ttc_cents > 0)` -- `image_path VARCHAR(255) NULL` -- `est_disponible TINYINT(1) NOT NULL DEFAULT 1` -- `ordre SMALLINT UNSIGNED NOT NULL DEFAULT 0` -- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` -- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `category_id` | INT UNSIGNED | NO | FK -> category | +| `name` | VARCHAR(120) | NO | Product label | +| `description` | TEXT | YES | Optional long description | +| `price_cents` | INT UNSIGNED | NO | A la carte price, incl. VAT, in cents | +| `vat_rate` | SMALLINT UNSIGNED | NO | Per-mille: 100 = 10%, 55 = 5.5% | +| `image_path` | VARCHAR(255) | YES | Relative path from public root | +| `is_available` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Manual availability toggle | +| `display_order` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Within-category display order | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit | +| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit | -**ON DELETE RESTRICT** sur `categorie_id` : impossible de supprimer une categorie qui contient des produits (protection metier, evite les orphelins). +**ON DELETE RESTRICT** on `category_id`: a category with products cannot be deleted. Prevents +orphaned products. + +--- ### 4.3 `menu` ``` -menu (id, #categorie_id, libelle, [description], prix_ttc_cents, [image_path], - est_disponible, ordre, created_at, updated_at) +menu (id, #category_id, #burger_product_id, name, [description], + price_normal_cents, price_maxi_cents, [image_path], + is_available, display_order, created_at, updated_at) - PK : id - FK : categorie_id -> categorie(id) ON DELETE RESTRICT - IDX : (categorie_id, est_disponible, ordre) + PK : id + FK : category_id -> category(id) ON DELETE RESTRICT + FK : burger_product_id -> product(id) ON DELETE RESTRICT + IDX : (category_id, is_available, display_order) + CHK : price_normal_cents > 0 + CHK : price_maxi_cents > 0 ``` -Types : identiques a `produit` (meme structure, semantique distincte cf. dictionary note 3). +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `category_id` | INT UNSIGNED | NO | FK -> category (typically the `menus` category) | +| `burger_product_id` | INT UNSIGNED | NO | FK -> product — the fixed burger that anchors this menu | +| `name` | VARCHAR(120) | NO | e.g. "Menu Le 280" | +| `description` | TEXT | YES | Optional | +| `price_normal_cents` | INT UNSIGNED | NO | Normal format price in cents | +| `price_maxi_cents` | INT UNSIGNED | NO | Maxi format price in cents (~+150 cents) | +| `image_path` | VARCHAR(255) | YES | Typically reuses the burger image | +| `is_available` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Availability toggle | +| `display_order` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Display order | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit | +| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit | -### 4.4 `menu_produit` (table associative) +**ON DELETE RESTRICT** on both FKs: prevents deletion of a category or burger product that +is still referenced by a menu definition. + +--- + +### 4.4 `menu_slot` ``` -menu_produit (#menu_id, #produit_id, role, position) +menu_slot (id, #menu_id, name, slot_type, is_required, display_order) - PK : (menu_id, produit_id) - FK : menu_id -> menu(id) ON DELETE CASCADE - FK : produit_id -> produit(id) ON DELETE RESTRICT - IDX : (menu_id, position) + PK : id + FK : menu_id -> menu(id) ON DELETE CASCADE + IDX : (menu_id, display_order) ``` -Types : -- `menu_id INT UNSIGNED NOT NULL` -- `produit_id INT UNSIGNED NOT NULL` -- `role ENUM('burger','accompagnement','boisson','sauce','dessert') NOT NULL` -- `position SMALLINT UNSIGNED NOT NULL DEFAULT 0` +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `menu_id` | INT UNSIGNED | NO | FK -> menu | +| `name` | VARCHAR(80) | NO | e.g. "Drink", "Side", "Sauce" | +| `slot_type` | ENUM('drink','side','sauce','dessert','extra') | NO | Semantic role | +| `is_required` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Whether the customer must fill this slot | +| `display_order` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Display order within menu builder | -**ON DELETE CASCADE** sur `menu_id` : si un menu est supprime, ses compositions le sont aussi. -**ON DELETE RESTRICT** sur `produit_id` : impossible de supprimer un produit utilise dans un menu (protection integrite menu). +**No audit fields**: a slot is part of menu definition; created and updated together with +the menu. -Pas d'`updated_at` (table de jointure, cf. dictionary note 5 : les jointures sont supprimees+recreees, pas modifiees). +**ON DELETE CASCADE** on `menu_id`: if a menu is deleted, its slots are deleted with it. -### 4.5 `commande` +--- + +### 4.5 `menu_slot_option` + +Pure join table. Composite PK. ``` -commande (id, numero, source, mode_consommation, statut, - total_ht_cents, total_tva_cents, total_ttc_cents, tva_taux_pourmille, - [paye_a], created_at, updated_at) +menu_slot_option (#menu_slot_id, #product_id) - PK : id - UK : numero - IDX : (source, created_at) - IDX : (statut, created_at) - IDX : created_at - CHECK : source != 'drive' OR mode_consommation = 'drive' - CHECK : total_ttc_cents = total_ht_cents + total_tva_cents + PK : (menu_slot_id, product_id) + FK : menu_slot_id -> menu_slot(id) ON DELETE CASCADE + FK : product_id -> product(id) ON DELETE RESTRICT ``` -Types : -- `id INT UNSIGNED AUTO_INCREMENT` -- `numero VARCHAR(20) NOT NULL` -- `source ENUM('kiosk','comptoir','drive') NOT NULL` -- `mode_consommation ENUM('sur_place','a_emporter','drive') NOT NULL` -- `statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') NOT NULL DEFAULT 'pending_payment'` -- `total_ht_cents INT UNSIGNED NOT NULL CHECK (total_ht_cents >= 0)` -- `total_tva_cents INT UNSIGNED NOT NULL CHECK (total_tva_cents >= 0)` -- `total_ttc_cents INT UNSIGNED NOT NULL CHECK (total_ttc_cents > 0)` -- `tva_taux_pourmille SMALLINT UNSIGNED NOT NULL` -- `paye_a DATETIME NULL` (NULL avant paiement, timestamp du passage en `paid`) -- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` -- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` +| Column | Type | NULL | Notes | +|---|---|---|---| +| `menu_slot_id` | INT UNSIGNED | NO | FK -> menu_slot | +| `product_id` | INT UNSIGNED | NO | FK -> product | -**CHECK croise** `source/mode_consommation` (cf. dictionary note 8) : empeche les combinaisons invalides au niveau SGBD plutot que de se reposer uniquement sur le code applicatif. +**ON DELETE CASCADE** on `menu_slot_id`: if a slot is deleted, its eligibility list goes with it. +**ON DELETE RESTRICT** on `product_id`: a product listed as eligible in a slot cannot be +deleted without first removing it from the slot options. Prevents silent breakage of menus. -**CHECK montants** : invariant `TTC = HT + TVA` verifie en base (defense-in-depth contre les bugs de calcul applicatif). +No timestamps. Pure join table. -Aucune FK directe vers `user` : la tracabilite passe par `commande_event` (cf. 4.7). +--- -### 4.6 `ligne_commande` +### 4.6 `ingredient` ``` -ligne_commande (id, #commande_id, type_item, [#produit_id], [#menu_id], - libelle_snapshot, prix_unitaire_ttc_cents_snapshot, quantite, created_at) +ingredient (id, name, unit, stock_quantity, pack_size, [pack_label], + low_stock_threshold, is_active, created_at, updated_at) - PK : id - FK : commande_id -> commande(id) ON DELETE CASCADE - FK : produit_id -> produit(id) ON DELETE RESTRICT - FK : menu_id -> menu(id) ON DELETE RESTRICT - IDX : commande_id - CHECK : (type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL) - OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL) + PK : id + UK : name + CHK : stock_quantity >= 0 + CHK : pack_size > 0 + CHK : low_stock_threshold >= 0 ``` -Types : -- `id INT UNSIGNED AUTO_INCREMENT` -- `commande_id INT UNSIGNED NOT NULL` -- `type_item ENUM('produit','menu') NOT NULL` -- `produit_id INT UNSIGNED NULL` -- `menu_id INT UNSIGNED NULL` -- `libelle_snapshot VARCHAR(120) NOT NULL` -- `prix_unitaire_ttc_cents_snapshot INT UNSIGNED NOT NULL CHECK (prix_unitaire_ttc_cents_snapshot > 0)` -- `quantite SMALLINT UNSIGNED NOT NULL DEFAULT 1 CHECK (quantite > 0)` -- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `name` | VARCHAR(120) | NO | Unique name, e.g. "Sesame Bun" | +| `unit` | VARCHAR(40) | NO | Packaging unit label (free-form, not ENUM) | +| `stock_quantity` | INT NOT NULL DEFAULT 0 | NO | Current stock. Signed INT to detect negative (alert) | +| `pack_size` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Units per restocking pack | +| `pack_label` | VARCHAR(80) | YES | Human label of the pack | +| `low_stock_threshold` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Alert threshold | +| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivate obsolete ingredients | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit | +| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit | -**ON DELETE CASCADE** sur `commande_id` : si la commande disparait, ses lignes aussi. -**ON DELETE RESTRICT** sur `produit_id` et `menu_id` : impossible de supprimer un produit/menu reference par une commande historique (preserve les references meme si on snapshote). +No FK. Root table for the Ingredients & Stock sub-domain. -**CHECK polymorphisme** : exclusivite mutuelle `produit_id` / `menu_id` selon `type_item` (cf. dictionary note 6). +--- -### 4.7 `commande_event` +### 4.7 `product_ingredient` + +Associative table carrying recipe and customisation metadata. Composite PK. ``` -commande_event (id, #commande_id, event_type, [from_statut], to_statut, - [#user_id], [payload], created_at) +product_ingredient (#product_id, #ingredient_id, quantity_normal, quantity_maxi, + is_removable, is_addable, extra_price_cents) - PK : id - FK : commande_id -> commande(id) ON DELETE CASCADE - FK : user_id -> user(id) ON DELETE SET NULL - IDX : (commande_id, created_at) - IDX : (user_id, created_at) - IDX : (event_type, created_at) + PK : (product_id, ingredient_id) + FK : product_id -> product(id) ON DELETE CASCADE + FK : ingredient_id -> ingredient(id) ON DELETE RESTRICT + CHK : quantity_normal > 0 + CHK : quantity_maxi >= quantity_normal + CHK : extra_price_cents >= 0 ``` -Types : -- `id INT UNSIGNED AUTO_INCREMENT` -- `commande_id INT UNSIGNED NOT NULL` -- `event_type ENUM('CREATED','PAID','PREPARING_STARTED','READY','DELIVERED','CANCELLED') NOT NULL` -- `from_statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') NULL` -- `to_statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') NOT NULL` -- `user_id INT UNSIGNED NULL` -- `payload JSON NULL` -- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` +| Column | Type | NULL | Notes | +|---|---|---|---| +| `product_id` | INT UNSIGNED | NO | FK -> product | +| `ingredient_id` | INT UNSIGNED | NO | FK -> ingredient | +| `quantity_normal` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Units consumed in Normal format | +| `quantity_maxi` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Units consumed in Maxi format; equals `quantity_normal` for burger/sauce (format-invariant), higher for side/drink | +| `is_removable` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Customer may remove at no cost | +| `is_addable` | TINYINT(1) NOT NULL DEFAULT 0 | NO | Customer may add an extra unit | +| `extra_price_cents` | INT UNSIGNED NOT NULL DEFAULT 0 | NO | Surcharge if `is_addable=1` and customer adds it | -**ON DELETE CASCADE** sur `commande_id` : si la commande est purgee, son journal disparait avec elle. -**ON DELETE SET NULL** sur `user_id` : si un equipier est supprime, les events restent (l'audit reste consultable, l'attribution individuelle est perdue). +**ON DELETE CASCADE** on `product_id`: if a product is deleted, its recipe rows are deleted. +**ON DELETE RESTRICT** on `ingredient_id`: cannot delete an ingredient still referenced in a +recipe. Admin must remove the product-ingredient link first. -**Pas d'`updated_at`** : table append-only. Aucun UPDATE applicatif autorise (cf. mlt.md RG-T10). +No timestamps. Join table with attributes. -**Pas de CHECK croise from_statut/to_statut** : la verification de la machine a etats est applicative (mlt section 12), un CHECK SQL serait trop rigide (event_type peut prendre des valeurs non encore prevues). +--- -### 4.8 `user` +### 4.8 `allergen` ``` -user (id, email, password_hash, nom, prenom, #role_id, est_actif, [last_login_at], - created_at, updated_at) +allergen (id, code, name, [description]) - PK : id - UK : email - FK : role_id -> role(id) ON DELETE RESTRICT - IDX : (est_actif, role_id) + PK : id + UK : code ``` -Types : -- `id INT UNSIGNED AUTO_INCREMENT` -- `email VARCHAR(254) NOT NULL` (RFC 5321) -- `password_hash VARCHAR(255) NOT NULL` (argon2id, cf. `.env` `PASSWORD_ALGO`) -- `nom VARCHAR(60) NOT NULL` -- `prenom VARCHAR(60) NOT NULL` -- `role_id INT UNSIGNED NOT NULL` -- `est_actif TINYINT(1) NOT NULL DEFAULT 1` -- `last_login_at DATETIME NULL` -- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` -- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `code` | VARCHAR(30) | NO | Machine code, e.g. `gluten`, `milk` | +| `name` | VARCHAR(80) | NO | Display name | +| `description` | TEXT | YES | Optional guidance | -**ON DELETE RESTRICT** sur `role_id` : impossible de supprimer un role qui a encore des users (passer par `est_actif = 0` sur le role avant de supprimer). +No FK. Reference table; 14 rows at seed (INCO Regulation (EU) 1169/2011). +No `updated_at`: allergen catalogue is considered stable (additions require a migration, not a UI action). -### 4.9 `role` +--- + +### 4.9 `ingredient_allergen` + +Pure join table. Composite PK. ``` -role (id, code, libelle, [description], est_actif, created_at, updated_at) +ingredient_allergen (#ingredient_id, #allergen_id) - PK : id - UK : code + PK : (ingredient_id, allergen_id) + FK : ingredient_id -> ingredient(id) ON DELETE CASCADE + FK : allergen_id -> allergen(id) ON DELETE RESTRICT ``` -Types : -- `id INT UNSIGNED AUTO_INCREMENT` -- `code VARCHAR(40) NOT NULL` -- `libelle VARCHAR(80) NOT NULL` -- `description TEXT NULL` -- `est_actif TINYINT(1) NOT NULL DEFAULT 1` -- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` -- `updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` +| Column | Type | NULL | Notes | +|---|---|---|---| +| `ingredient_id` | INT UNSIGNED | NO | FK -> ingredient | +| `allergen_id` | INT UNSIGNED | NO | FK -> allergen | -Aucune FK. Table racine du sous-domaine RBAC. +**ON DELETE CASCADE** on `ingredient_id`: if an ingredient is deleted, its allergen links go with it. +**ON DELETE RESTRICT** on `allergen_id`: an allergen in the regulated catalogue cannot be deleted. -### 4.10 `permission` +No timestamps. Pure join table. + +--- + +### 4.10 `role` + +Placed before `user` because `user` depends on `role`. ``` -permission (id, code, libelle, [description], created_at) +role (id, code, label, [description], [default_route], [order_source], + is_active, created_at, updated_at) - PK : id - UK : code + PK : id + UK : code ``` -Types : -- `id INT UNSIGNED AUTO_INCREMENT` -- `code VARCHAR(60) NOT NULL` (format `.`) -- `libelle VARCHAR(120) NOT NULL` -- `description TEXT NULL` -- `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP` +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `code` | VARCHAR(40) | NO | Machine code: `admin`, `manager`, `kitchen`, `counter`, `drive` | +| `label` | VARCHAR(80) | NO | Display name | +| `description` | TEXT | YES | Optional | +| `default_route` | VARCHAR(120) | YES | Landing screen, e.g. `/admin/dashboard` | +| `order_source` | ENUM('kiosk','counter','drive') | YES | Auto-tagged source when this role creates an order; NULL for admin/manager | +| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivation preserves history | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit | +| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit | -Pas d'`updated_at` : les permissions sont declarees en migration et ne sont pas modifiees via UI (cf. RBAC statique cote permissions, dictionary 3.10 et MCD 4.3). +No FK. Root table for RBAC. -### 4.11 `role_permission` (table associative) +--- + +### 4.11 `user` + +``` +user (id, email, password_hash, first_name, last_name, #role_id, + is_active, [last_login_at], created_at, updated_at) + + PK : id + UK : email + FK : role_id -> role(id) ON DELETE RESTRICT + IDX : (is_active, role_id) +``` + +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `email` | VARCHAR(254) | NO | RFC 5321 max length | +| `password_hash` | VARCHAR(255) | NO | argon2id hash | +| `first_name` | VARCHAR(60) | NO | | +| `last_name` | VARCHAR(60) | NO | | +| `role_id` | INT UNSIGNED | NO | FK -> role | +| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivation without deletion | +| `last_login_at` | DATETIME | YES | Audit, dormant account detection | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit | +| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit | + +**ON DELETE RESTRICT** on `role_id`: a role cannot be deleted while users hold it. +Deactivate the role first (`is_active = 0`), then reassign users before deleting. + +--- + +### 4.12 `role_visible_source` + +Pure join table. Composite PK. + +``` +role_visible_source (#role_id, source) + + PK : (role_id, source) + FK : role_id -> role(id) ON DELETE CASCADE +``` + +| Column | Type | NULL | Notes | +|---|---|---|---| +| `role_id` | INT UNSIGNED | NO | FK -> role | +| `source` | ENUM('kiosk','counter','drive') | NO | Order source visible on dashboard | + +**ON DELETE CASCADE** on `role_id`: if a role is deleted, its dashboard source filters go with it. + +No timestamps. Pure join table. + +Seed data: +- `kitchen`: kiosk, counter, drive +- `counter`: kiosk, counter +- `drive`: drive +- `admin`, `manager`: no rows (global view, no source filter) + +--- + +### 4.13 `permission` + +``` +permission (id, code, label, [description], created_at) + + PK : id + UK : code +``` + +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `code` | VARCHAR(60) | NO | Format `.` | +| `label` | VARCHAR(120) | NO | Display name | +| `description` | TEXT | YES | Optional | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit | + +No `updated_at`: permissions are declared in migration and not modified via UI. +Catalogue is frozen at 23 codes (see dictionary section 3.17). + +--- + +### 4.14 `role_permission` + +Pure join table. Composite PK. ``` role_permission (#role_id, #permission_id) - PK : (role_id, permission_id) - FK : role_id -> role(id) ON DELETE CASCADE - FK : permission_id -> permission(id) ON DELETE CASCADE - IDX : permission_id (acces inverse "quels roles ont cette permission ?") + PK : (role_id, permission_id) + FK : role_id -> role(id) ON DELETE CASCADE + FK : permission_id -> permission(id) ON DELETE CASCADE + IDX : permission_id ``` -Types : -- `role_id INT UNSIGNED NOT NULL` -- `permission_id INT UNSIGNED NOT NULL` - -**ON DELETE CASCADE des deux cotes** : suppression d'un role ou d'une permission supprime ses mappings. - -Pas de timestamps (table de jointure pure, cf. dictionary note 5). - ---- - -## 5. Recapitulatif des contraintes referentielles - -| FK | Reference | ON DELETE | Justification | +| Column | Type | NULL | Notes | |---|---|---|---| -| `produit.categorie_id` | `categorie(id)` | RESTRICT | Pas d'orphelin produit | -| `menu.categorie_id` | `categorie(id)` | RESTRICT | Idem | -| `menu_produit.menu_id` | `menu(id)` | CASCADE | Composition disparait avec le menu | -| `menu_produit.produit_id` | `produit(id)` | RESTRICT | Pas de cascade : un produit reference dans un menu ne peut pas etre supprime sans amender la composition | -| `commande.--` | (aucune FK vers user) | - | Tracabilite via commande_event | -| `ligne_commande.commande_id` | `commande(id)` | CASCADE | Lignes disparaissent avec la commande | -| `ligne_commande.produit_id` | `produit(id)` | RESTRICT | Preserve l'integrite historique | -| `ligne_commande.menu_id` | `menu(id)` | RESTRICT | Idem | -| `commande_event.commande_id` | `commande(id)` | CASCADE | Journal disparait avec la commande | -| `commande_event.user_id` | `user(id)` | SET NULL | Audit conserve, attribution individuelle perdue | -| `user.role_id` | `role(id)` | RESTRICT | Pas d'user sans role | -| `role_permission.role_id` | `role(id)` | CASCADE | Mapping disparait avec le role | -| `role_permission.permission_id` | `permission(id)` | CASCADE | Mapping disparait avec la permission | +| `role_id` | INT UNSIGNED | NO | FK -> role | +| `permission_id` | INT UNSIGNED | NO | FK -> permission | -**Cles** : -- **CASCADE** : la donnee dependante n'a pas de sens hors de son parent (lignes / events / mappings) -- **RESTRICT** : suppression du parent bloquee tant que des references existent (catalogue, role) -- **SET NULL** : preserve la donnee enfant en perdant le lien (audit event sans attribution) +**ON DELETE CASCADE** on both FKs: deleting a role or a permission removes its mappings. +The secondary index on `permission_id` supports the reverse query "which roles have this +permission?" without scanning the full table. + +No timestamps. Pure join table. --- -## 6. Index complementaires +### 4.15 `customer_order` -Au-dela des PK / UK / FK qui creent des index automatiquement, indexes ajoutes pour les requetes frequentes identifiees au MCT/MLT : +``` +customer_order (id, order_number, source, service_mode, status, + total_ht_cents, total_vat_cents, total_ttc_cents, + [paid_at], [delivered_at], [cancelled_at], + created_at, updated_at) -| Table | Index | Justification (operation MCT) | -|---|---|---| -| `produit` | `(categorie_id, est_disponible, ordre)` | Chargement catalogue kiosk (op 1) : filtre par categorie + disponible + tri par ordre | -| `menu` | `(categorie_id, est_disponible, ordre)` | Idem produit | -| `menu_produit` | `(menu_id, position)` | Chargement composition d'un menu | -| `commande` | `(source, created_at)` | Stats "par canal" + tri chronologique | -| `commande` | `(statut, created_at)` | Files d'attente preparation/accueil (ops 6, 9) | -| `commande` | `created_at` | Stats agregations live | -| `ligne_commande` | `commande_id` | Recuperation des lignes d'une commande | -| `commande_event` | `(commande_id, created_at)` | Historique d'une commande | -| `commande_event` | `(user_id, created_at)` | Actions d'un equipier sur une periode | -| `commande_event` | `(event_type, created_at)` | Stats "combien de cancellations cette semaine ?" | -| `user` | `(est_actif, role_id)` | Login + permissions check (op 23) | -| `role_permission` | `permission_id` | Acces inverse "quels roles ont cette permission ?" | + PK : id + UK : order_number + IDX : (status, created_at) + IDX : (source, created_at) + IDX : created_at + CHK : total_ht_cents >= 0 + CHK : total_vat_cents >= 0 + CHK : total_ttc_cents > 0 + CHK : total_ttc_cents = total_ht_cents + total_vat_cents + CHK : source != 'drive' OR service_mode = 'drive' +``` -**Index NON ajoutes** (volontaire) : -- `commande.numero` : UK suffit, pas de range query attendue dessus -- `commande.mode_consommation` : faible cardinalite (3 valeurs), un index n'est pas rentable, full scan acceptable -- `commande.paye_a` : NULL pour la majorite des lignes (commande encore en cours), index peu utile - ---- - -## 7. Contraintes CHECK (MariaDB 10.2+) - -Verification au niveau SGBD pour les invariants critiques. Defense-in-depth contre les bugs applicatifs. - -| Table | CHECK | Pourquoi | -|---|---|---| -| `produit` | `prix_ttc_cents > 0` | Prix nul ou negatif = bug | -| `menu` | `prix_ttc_cents > 0` | Idem | -| `commande` | `total_ht_cents >= 0` | Plancher autorise (commande vide transitoire ?) | -| `commande` | `total_tva_cents >= 0` | Idem | -| `commande` | `total_ttc_cents > 0` | TTC nul = bug | -| `commande` | `total_ttc_cents = total_ht_cents + total_tva_cents` | Invariant arithmetique | -| `commande` | `source != 'drive' OR mode_consommation = 'drive'` | Contrainte croisee (dictionary note 8, mlt RG-T09) | -| `ligne_commande` | `prix_unitaire_ttc_cents_snapshot > 0` | Snapshot prix non nul | -| `ligne_commande` | `quantite > 0` | Quantite non nulle | -| `ligne_commande` | `(type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL) OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL)` | Polymorphisme exclusif (dictionary note 6) | - ---- - -## 8. Cross-validation entites MCD -> tables MLD - -| Entite MCD | Table MLD | Notes | -|---|---|---| -| `categorie` (3.1) | `categorie` (4.1) | 1:1 | -| `produit` (3.2) | `produit` (4.2) | 1:1 | -| `menu` (3.3) | `menu` (4.3) | 1:1 | -| `menu_produit` (3.4) | `menu_produit` (4.4) | Entite associative -> table de jointure avec PK composite | -| `commande` (3.5) | `commande` (4.5) | 1:1, attribut `source` ajoute (decision 2026-05-28) | -| `ligne_commande` (3.6) | `ligne_commande` (4.6) | 1:1, polymorphisme materialise par 2 FKs nullables + CHECK | -| `commande_event` (3.7) | `commande_event` (4.7) | 1:1, table append-only, decision 2026-05-28 | -| `user` (3.8) | `user` (4.8) | 1:1 | -| `role` (3.9) | `role` (4.9) | 1:1 | -| `permission` (3.10) | `permission` (4.10) | 1:1 | -| `role_permission` (3.11) | `role_permission` (4.11) | Entite associative -> table de jointure avec PK composite | - -**Conclusion** : 11/11 entites tracees. Aucune entite MCD ne reste sans table, aucune table MLD ne sort du modele conceptuel. - ---- - -## 9. Estimation volumes et taille - -| Table | Volume 6 mois | Taille moyenne ligne | Taille totale | +| Column | Type | NULL | Notes | |---|---|---|---| -| `categorie` | ~10 | 200 octets | < 1 Ko | -| `produit` | ~70 | 400 octets | ~30 Ko | -| `menu` | ~15 | 400 octets | ~6 Ko | -| `menu_produit` | ~80 | 30 octets | ~2 Ko | -| `commande` | ~30k | 300 octets | ~9 Mo | -| `ligne_commande` | ~150k | 200 octets | ~30 Mo | -| `commande_event` | ~180k | 200 octets | ~36 Mo | -| `user` | ~20 | 500 octets | ~10 Ko | -| `role` | ~5 | 200 octets | ~1 Ko | -| `permission` | ~40 | 300 octets | ~12 Ko | -| `role_permission` | ~80 | 30 octets | ~2 Ko | +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `order_number` | VARCHAR(20) | NO | Format `K/C/D-YYYY-MM-DD-NNN` by channel | +| `source` | ENUM('kiosk','counter','drive') | NO | Input channel | +| `service_mode` | ENUM('dine_in','takeaway','drive') | NO | Consumption mode (stats only, no fiscal role) | +| `status` | ENUM('pending_payment','paid','delivered','cancelled') NOT NULL DEFAULT 'pending_payment' | NO | 4-state machine | +| `total_ht_cents` | INT UNSIGNED | NO | Ex-VAT total snapshot | +| `total_vat_cents` | INT UNSIGNED | NO | VAT amount snapshot | +| `total_ttc_cents` | INT UNSIGNED | NO | Incl.-VAT total; must equal HT + VAT | +| `paid_at` | DATETIME | YES | Timestamp of transition to `paid` | +| `delivered_at` | DATETIME | YES | Timestamp of transition to `delivered` | +| `cancelled_at` | DATETIME | YES | Timestamp of cancellation | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Used as `service_day` base | +| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit | -**Total : ~75 Mo sur 6 mois**. Largement gerable par MariaDB sur le conteneur Wakdo (volume `wakdo_db_data` named volume, cf. `docker-compose.yml`). +No FK toward `user`: staff attribution is not stored on the order. Operational accountability +is covered by `stock_movement.user_id` for stock actions. -Les indexes ajoutent typiquement 30-50% du volume des tables, soit ~30 Mo supplementaires. **Estimation totale : ~100-110 Mo / 6 mois**. +**4-state machine**: `pending_payment -> paid -> delivered` (+ `cancelled`). States `preparing` +and `ready` are dropped (decision D4). KPI: `delivered_at - paid_at` (target SLA ~10 min). + +**`service_day` computation** (used in stats queries — NOT a stored column): +```sql +CASE WHEN HOUR(created_at) < 10 + THEN DATE(created_at) - INTERVAL 1 DAY + ELSE DATE(created_at) +END +``` +Cutoff: 10:00. The generated-column formula with `INTERVAL 4 HOUR 30 MINUTE` from v0.1 was +incorrect and is dropped (decision D6). + +**VAT calculation**: totals on `customer_order` are the sum of line-level calculations. +Line-level VAT: `unit_price_cents_snapshot * quantity` is the TTC amount per line; +HT = `ROUND(ttc_cents * 100 / (100 + vat_rate_per_cent))` where `vat_rate_per_cent` +is `vat_rate_snapshot / 10`. Computed at application layer at cart validation. + +**`source = 'drive' => service_mode = 'drive'`**: the CHECK enforces this at DB level. --- -## 10. Decisions reportees au DDL et a P2 +### 4.16 `order_item` -Les decisions suivantes sont laissees au DDL (`db/migrations/0001_init_schema.sql`) ou aux phases ulterieures, parce qu'elles concernent l'implementation et pas la modelisation logique : +``` +order_item (id, #order_id, item_type, [#product_id], [#menu_id], format, + label_snapshot, unit_price_cents_snapshot, vat_rate_snapshot, + quantity, created_at) -1. **Triggers ou colonnes generees** : `service_day` (cf. PROJECT_CONTEXT section 2) pourrait etre une `GENERATED ALWAYS AS (DATE_SUB(created_at, INTERVAL 4 HOUR + 30 MINUTE))` virtuelle pour eviter le calcul applicatif. A evaluer en P3 si les stats sont penibles. -2. **Partitionnement** : `commande_event` pourrait etre partitionne par mois si le volume depasse les estimations. Pas pour MVP. -3. **Foreign Key index** : MariaDB cree automatiquement un index sur la FK lors de la declaration, sauf si un index utilisable existe deja. A verifier explicitement dans le DDL. -4. **Collation** : `utf8mb4_unicode_ci` retenu (sensible diacritiques et casse pour les recherches). Si besoin de tri locale francais strict, passer en `utf8mb4_fr_0900_ai_ci` (MySQL 8) ou rester en `unicode_ci`. -5. **Engine** : `InnoDB` par defaut (ACID + FKs). Pas de MEMORY ni Archive. -6. **Charset emoji** : `utf8mb4` (4 octets / char max) couvre les emojis au cas ou ils apparaitraient dans `description` produit ou `payload` JSON. + PK : id + FK : order_id -> customer_order(id) ON DELETE CASCADE + FK : product_id -> product(id) ON DELETE RESTRICT + FK : menu_id -> menu(id) ON DELETE RESTRICT + IDX : order_id + CHK : unit_price_cents_snapshot > 0 + CHK : vat_rate_snapshot IN (55, 100) + CHK : quantity > 0 + CHK : (item_type = 'product' AND product_id IS NOT NULL AND menu_id IS NULL) + OR (item_type = 'menu' AND menu_id IS NOT NULL AND product_id IS NULL) +``` + +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `order_id` | INT UNSIGNED | NO | FK -> customer_order | +| `item_type` | ENUM('product','menu') | NO | Discriminator | +| `product_id` | INT UNSIGNED | YES | Non-null if `item_type = 'product'`, NULL otherwise | +| `menu_id` | INT UNSIGNED | YES | Non-null if `item_type = 'menu'`, NULL otherwise | +| `format` | ENUM('normal','maxi') NOT NULL DEFAULT 'normal' | NO | Menu format. For standalone products, value is `normal` | +| `label_snapshot` | VARCHAR(120) | NO | Label at time of order | +| `unit_price_cents_snapshot` | INT UNSIGNED | NO | Unit price incl. VAT at time of order | +| `vat_rate_snapshot` | SMALLINT UNSIGNED | NO | VAT rate per-mille at time of order | +| `quantity` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Quantity (e.g. 3 drinks = 1 line, quantity=3) | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit | + +**ON DELETE CASCADE** on `order_id`: lines are deleted with the order. +**ON DELETE RESTRICT** on `product_id` and `menu_id`: a product or menu referenced in an +historical order line cannot be deleted. The snapshot makes the FK reference non-critical +for display, but RESTRICT avoids silent orphaning of the relational structure. + +**Polymorphism exclusivity CHECK**: MariaDB 10.2+ enforces this at INSERT/UPDATE time. --- -## 11. A faire au prochain sprint (DDL + Seed) +### 4.17 `order_item_selection` -1. **DDL** (`db/migrations/0001_init_schema.sql`) : transcrire ce MLD en CREATE TABLE executables, dans l'ordre des dependances (categorie -> produit/menu -> menu_produit -> commande -> ligne_commande/commande_event ; permission -> role -> role_permission ; user en dernier). +Customer's choice for one slot of a menu order line. -2. **Seed** (`db/seeds/0001_demo_data.sql`) : INSERT pour : - - 9 categories + 53 produits + 13 menus a partir des JSON sources (`docs/merise/_sources/`), prix normalises en centimes - - 1 admin par defaut + roles (admin, manager, equipier-comptoir, equipier-drive) - - Permissions declarees (CRUD produit/menu/categorie/user/role + commande operationnelles) - - Quelques commandes d'exemple pour les demos +``` +order_item_selection (id, #order_item_id, #menu_slot_id, #product_id, label_snapshot) -3. **Export fallback JSON** (`scripts/export-fallback.{sh|php}`) : extrait des donnees seed vers `src/public/borne/data/*.json` pour le mode "Bloc 1 isole" (kiosk sans BDD pour les tests). + PK : id + FK : order_item_id -> order_item(id) ON DELETE CASCADE + FK : menu_slot_id -> menu_slot(id) ON DELETE RESTRICT + FK : product_id -> product(id) ON DELETE RESTRICT + IDX : order_item_id +``` -4. **Tests de validation DDL** : verifier que : - - Toutes les CHECK contraintes sont declenchees comme attendu (tests d'integration) - - Les ON DELETE CASCADE / RESTRICT se comportent comme specifie - - Les indexes accelerent reellement les requetes ciblees (EXPLAIN sur les requetes types du MCT) +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `order_item_id` | INT UNSIGNED | NO | FK -> order_item (must be a menu-type line) | +| `menu_slot_id` | INT UNSIGNED | NO | FK -> menu_slot (which slot was filled) | +| `product_id` | INT UNSIGNED | NO | FK -> product (chosen by customer) | +| `label_snapshot` | VARCHAR(120) | NO | Product label at time of order | -5. **Migration tooling** : decider de l'outil (phinx, doctrine migrations, ou homemade PHP script). Cf. PROJECT_CONTEXT pour le choix retenu. +**ON DELETE CASCADE** on `order_item_id`: if the parent order line is deleted, its slot +selections go with it. +**ON DELETE RESTRICT** on `menu_slot_id` and `product_id`: historical slot choice records +must not be silently broken by catalogue changes. + +Note: the business constraint that `order_item_id` references a line with `item_type='menu'` +is enforced at application layer (not in MariaDB without a trigger or deferred constraint). + +--- + +### 4.18 `order_item_modifier` + +Ingredient-level modification applied by the customer to a product or the fixed burger of a menu. + +``` +order_item_modifier (id, #order_item_id, #ingredient_id, action, extra_price_cents) + + PK : id + FK : order_item_id -> order_item(id) ON DELETE CASCADE + FK : ingredient_id -> ingredient(id) ON DELETE RESTRICT + IDX : order_item_id + CHK : extra_price_cents >= 0 +``` + +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `order_item_id` | INT UNSIGNED | NO | FK -> order_item | +| `ingredient_id` | INT UNSIGNED | NO | FK -> ingredient | +| `action` | ENUM('remove','add') | NO | `remove` = free removal; `add` = extra unit | +| `extra_price_cents` | INT UNSIGNED NOT NULL DEFAULT 0 | NO | Snapshot of surcharge at time of order (0 for removals) | + +**ON DELETE CASCADE** on `order_item_id`: if the order line is deleted, its modifiers go with it. +**ON DELETE RESTRICT** on `ingredient_id`: an ingredient referenced in a historical modifier +cannot be deleted. + +**Modifier attachment for menu lines**: the modifiable product is the fixed burger, resolved +via `order_item.menu_id -> menu.burger_product_id`. No additional FK column is needed on +this table (see dictionary note 10). + +--- + +### 4.19 `stock_movement` + +Append-only audit log of all stock changes per ingredient. + +``` +stock_movement (id, #ingredient_id, movement_type, delta, + [#order_id], [#user_id], [note], created_at) + + PK : id + FK : ingredient_id -> ingredient(id) ON DELETE RESTRICT + FK : order_id -> customer_order(id) ON DELETE SET NULL + FK : user_id -> user(id) ON DELETE SET NULL + IDX : (ingredient_id, created_at) + IDX : (movement_type, created_at) +``` + +| Column | Type | NULL | Notes | +|---|---|---|---| +| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | +| `ingredient_id` | INT UNSIGNED | NO | FK -> ingredient | +| `movement_type` | ENUM('sale','cancellation','restock','inventory_correction') | NO | Nature of movement | +| `delta` | INT | NO | Signed change: negative for consumption, positive for restock/cancellation/correction | +| `order_id` | INT UNSIGNED | YES | FK -> customer_order; non-null for `sale` and `cancellation` | +| `user_id` | INT UNSIGNED | YES | FK -> user; null for automated sale decrements | +| `note` | VARCHAR(255) | YES | Optional human note | +| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Immutable timestamp | + +**ON DELETE RESTRICT** on `ingredient_id`: an ingredient with a movement history cannot be +deleted. Admin must archive the ingredient (`is_active = 0`) instead. +**ON DELETE SET NULL** on `order_id`: if an order is purged from the system, its movement +records remain with `order_id = NULL`. The audit log is preserved; only the order link is lost. +**ON DELETE SET NULL** on `user_id`: if a user is deleted, movement records remain with +`user_id = NULL`. Audit is preserved; individual attribution is lost. + +**Immutability rule**: no UPDATE or DELETE at application layer. Corrections are new rows +with `movement_type = 'inventory_correction'` and a signed `delta`. + +No `updated_at`. Immutable append-only table. + +--- + +## 5. Referential integrity summary + +| FK column | References | ON DELETE | Rationale | +|---|---|---|---| +| `product.category_id` | `category(id)` | RESTRICT | No orphaned product | +| `menu.category_id` | `category(id)` | RESTRICT | Same | +| `menu.burger_product_id` | `product(id)` | RESTRICT | Menu definition requires its anchor burger | +| `menu_slot.menu_id` | `menu(id)` | CASCADE | Slots have no meaning without their menu | +| `menu_slot_option.menu_slot_id` | `menu_slot(id)` | CASCADE | Eligibility list disappears with the slot | +| `menu_slot_option.product_id` | `product(id)` | RESTRICT | Removing a product must not silently break menus | +| `product_ingredient.product_id` | `product(id)` | CASCADE | Recipe disappears with the product | +| `product_ingredient.ingredient_id` | `ingredient(id)` | RESTRICT | Cannot remove ingredient still in a recipe | +| `ingredient_allergen.ingredient_id` | `ingredient(id)` | CASCADE | Allergen links disappear with the ingredient | +| `ingredient_allergen.allergen_id` | `allergen(id)` | RESTRICT | Regulated allergen catalogue is immutable | +| `user.role_id` | `role(id)` | RESTRICT | A user cannot exist without a role | +| `role_visible_source.role_id` | `role(id)` | CASCADE | Dashboard filters disappear with the role | +| `role_permission.role_id` | `role(id)` | CASCADE | Permission mappings disappear with the role | +| `role_permission.permission_id` | `permission(id)` | CASCADE | Permission mappings disappear with the permission | +| `order_item.order_id` | `customer_order(id)` | CASCADE | Lines disappear with the order | +| `order_item.product_id` | `product(id)` | RESTRICT | Historical reference must not be silently orphaned | +| `order_item.menu_id` | `menu(id)` | RESTRICT | Same | +| `order_item_selection.order_item_id` | `order_item(id)` | CASCADE | Slot choices disappear with the line | +| `order_item_selection.menu_slot_id` | `menu_slot(id)` | RESTRICT | Historical slot record preserved | +| `order_item_selection.product_id` | `product(id)` | RESTRICT | Historical choice record preserved | +| `order_item_modifier.order_item_id` | `order_item(id)` | CASCADE | Modifiers disappear with the line | +| `order_item_modifier.ingredient_id` | `ingredient(id)` | RESTRICT | Historical modifier record preserved | +| `stock_movement.ingredient_id` | `ingredient(id)` | RESTRICT | Ingredient with history cannot be deleted | +| `stock_movement.order_id` | `customer_order(id)` | SET NULL | Audit preserved, order link lost | +| `stock_movement.user_id` | `user(id)` | SET NULL | Audit preserved, user attribution lost | + +**Key used**: CASCADE = child has no meaning without parent; RESTRICT = parent deletion +blocked while children exist; SET NULL = child is preserved, only the link is severed. + +--- + +## 6. CHECK constraints summary + +| Table | CHECK expression | Purpose | +|---|---|---| +| `product` | `price_cents > 0` | Zero or negative price is a bug | +| `product` | `vat_rate IN (55, 100)` | Only two legal VAT rates for this model | +| `menu` | `price_normal_cents > 0` | Same as product | +| `menu` | `price_maxi_cents > 0` | Same | +| `ingredient` | `stock_quantity >= 0` | Negative stock is an alert, not a valid state | +| `ingredient` | `pack_size > 0` | Pack size of zero makes restock logic incoherent | +| `ingredient` | `low_stock_threshold >= 0` | Threshold cannot be negative | +| `product_ingredient` | `quantity_normal > 0` | Recipe quantity of zero is meaningless | +| `product_ingredient` | `quantity_maxi >= quantity_normal` | Maxi consumes at least as much as Normal (side/drink more, burger/sauce equal) | +| `product_ingredient` | `extra_price_cents >= 0` | No negative surcharge | +| `customer_order` | `total_ht_cents >= 0` | Zero is allowed (edge case during cart building) | +| `customer_order` | `total_vat_cents >= 0` | Same | +| `customer_order` | `total_ttc_cents > 0` | A validated order must have a positive total | +| `customer_order` | `total_ttc_cents = total_ht_cents + total_vat_cents` | Arithmetic invariant; defence-in-depth vs application bugs | +| `customer_order` | `source != 'drive' OR service_mode = 'drive'` | Cross-dimension constraint (dict. note 5) | +| `order_item` | `unit_price_cents_snapshot > 0` | Non-zero price at transaction time | +| `order_item` | `vat_rate_snapshot IN (55, 100)` | Snapshot must match allowed rates | +| `order_item` | `quantity > 0` | Non-zero quantity | +| `order_item` | `(item_type='product' AND product_id IS NOT NULL AND menu_id IS NULL) OR (item_type='menu' AND menu_id IS NOT NULL AND product_id IS NULL)` | Polymorphism: exactly one FK populated per discriminator value | +| `order_item_modifier` | `extra_price_cents >= 0` | Snapshot of surcharge; cannot be negative | + +--- + +## 7. Recommended indexes (beyond PK / UK / FK auto-indexes) + +MariaDB InnoDB creates an index automatically for each FK declaration (if no usable index +exists). The following additional indexes target frequent query patterns identified in the +MCT / MLT. + +| Table | Index columns | Query pattern | +|---|---|---| +| `product` | `(category_id, is_available, display_order)` | Kiosk catalogue load: filter by category + availability, sort by order | +| `menu` | `(category_id, is_available, display_order)` | Same pattern for menus | +| `menu_slot` | `(menu_id, display_order)` | Menu builder: load all slots of a menu in order | +| `customer_order` | `(status, created_at)` | Active orders queue: pending/paid orders sorted by time | +| `customer_order` | `(source, created_at)` | Per-channel analytics and order filtering | +| `customer_order` | `created_at` | Time-range aggregations (hourly stats, `service_day`) | +| `order_item` | `order_id` | Retrieve all lines of an order | +| `order_item_selection` | `order_item_id` | Retrieve slot choices for a menu line | +| `order_item_modifier` | `order_item_id` | Retrieve ingredient modifications for a line | +| `stock_movement` | `(ingredient_id, created_at)` | Per-ingredient stock history (dict. section 3.19) | +| `stock_movement` | `(movement_type, created_at)` | Stats: cancellations per week, restocks per month | +| `role_permission` | `permission_id` | Reverse query: "which roles have this permission?" | +| `user` | `(is_active, role_id)` | Login check + permission resolution | + +**Indexes not added** (intentional): +- `customer_order.order_number`: UK index is sufficient; no range query expected on this column. +- `customer_order.service_mode`: low cardinality (3 values); full scan on the status index + with a `service_mode` filter is acceptable at expected volume. +- `customer_order.paid_at`: NULL for most in-flight rows; sparse index provides limited benefit. + +--- + +## 8. Cross-validation MLD <-> MCD + +Verification that all 19 MCD entities map to a table, and that all tables trace to the MCD. + +| MCD entity | MLD table | Mapping type | Notes | +|---|---|---|---| +| `category` (C1) | `category` (4.1) | 1:1 entity | | +| `product` (C2) | `product` (4.2) | 1:1 entity | | +| `menu` (C3) | `menu` (4.3) | 1:1 entity | New: `burger_product_id`, `price_normal_cents`, `price_maxi_cents` | +| `menu_slot` (C4) | `menu_slot` (4.4) | 1:1 entity | New entity (v0.2) | +| `menu_slot_option` (C5) | `menu_slot_option` (4.5) | Join table (composite PK) | New entity (v0.2) | +| `ingredient` (C6) | `ingredient` (4.6) | 1:1 entity | New entity (v0.2) | +| `product_ingredient` (C7) | `product_ingredient` (4.7) | Join table with attributes | New entity (v0.2) | +| `allergen` (C8) | `allergen` (4.8) | 1:1 entity | New entity (v0.2) | +| `ingredient_allergen` (C9) | `ingredient_allergen` (4.9) | Join table (composite PK) | New entity (v0.2) | +| `role` (C10) | `role` (4.10) | 1:1 entity | New: `default_route`, `order_source` | +| `user` (C11) | `user` (4.11) | 1:1 entity | Columns renamed to English | +| `role_visible_source` (C12) | `role_visible_source` (4.12) | Join table (composite PK) | New entity (v0.2) | +| `permission` (C13) | `permission` (4.13) | 1:1 entity | | +| `role_permission` (C14) | `role_permission` (4.14) | Join table (composite PK) | | +| `customer_order` (C15) | `customer_order` (4.15) | 1:1 entity | Renamed from `commande`; 4-state machine; phase timestamps | +| `order_item` (C16) | `order_item` (4.16) | 1:1 entity | New: `format`, `vat_rate_snapshot`; polymorphism CHECK | +| `order_item_selection` (C17) | `order_item_selection` (4.17) | 1:1 entity | New entity (v0.2) | +| `order_item_modifier` (C18) | `order_item_modifier` (4.18) | 1:1 entity | New entity (v0.2) | +| `stock_movement` (C19) | `stock_movement` (4.19) | 1:1 entity | New entity (v0.2) | + +**Result**: 19/19 entities mapped. No entity without a table; no table outside the MCD. + +**Dropped from v0.1**: `commande_event` (replaced by `paid_at`, `delivered_at`, `cancelled_at` +phase timestamps on `customer_order` — decision 2.A); `menu_produit` fixed-composition model +(replaced by `menu_slot` + `menu_slot_option` — decision D1). + +--- + +## 9. Volume estimation (6 months) + +| Table | Rows at 6 months | Avg row size | Est. size | +|---|---|---|---| +| `category` | ~10 | 200 bytes | < 1 KB | +| `product` | ~55 | 400 bytes | ~22 KB | +| `menu` | ~13 | 450 bytes | ~6 KB | +| `menu_slot` | ~40 | 150 bytes | ~6 KB | +| `menu_slot_option` | ~150 | 30 bytes | ~5 KB | +| `ingredient` | ~100 | 300 bytes | ~30 KB | +| `product_ingredient` | ~400 | 40 bytes | ~16 KB | +| `allergen` | 14 | 200 bytes | ~3 KB | +| `ingredient_allergen` | ~200 | 20 bytes | ~4 KB | +| `role` | ~5 | 200 bytes | ~1 KB | +| `user` | ~20 | 500 bytes | ~10 KB | +| `role_visible_source` | ~7 | 15 bytes | < 1 KB | +| `permission` | 23 | 250 bytes | ~6 KB | +| `role_permission` | ~80 | 15 bytes | ~2 KB | +| `customer_order` | ~30k | 300 bytes | ~9 MB | +| `order_item` | ~150k | 250 bytes | ~37 MB | +| `order_item_selection` | ~300k | 150 bytes | ~45 MB | +| `order_item_modifier` | ~150k | 80 bytes | ~12 MB | +| `stock_movement` | ~500k | 180 bytes | ~90 MB | + +**Estimated total**: ~190 MB data + ~60-80 MB for indexes = ~250-270 MB over 6 months. +Manageable on the MariaDB container (`wakdo_db_data` named volume in `docker-compose.yml`). + +`stock_movement` is the highest-volume table (~5-15 rows per order across all ingredients). +The `(ingredient_id, created_at)` index is the primary query path for per-ingredient +history; it will carry meaningful write amplification at scale. + +--- + +## 10. Decisions deferred to DDL and P2 + +1. **MariaDB generated column** for `service_day`: a `VIRTUAL GENERATED` column is technically + possible in MariaDB 5.7+ syntax. If stats queries prove burdensome without a materialised + column, a `STORED GENERATED` column could be added as a migration. For this model, the + applicative CASE expression is retained (simpler, avoids generated-column edge cases). +2. **Partitioning**: `stock_movement` could be partitioned by month if volume exceeds + estimates. Not in scope for the initial DDL. +3. **Triggers**: stock decrement on `paid` transition and re-credit on `cancelled` (from `paid`) + could be implemented as MariaDB triggers or as application-layer logic. To be decided at P2. +4. **Collation**: `utf8mb4_unicode_ci` retained (Unicode-compliant, case-insensitive). + If strict French alphabetical sort is needed, `utf8mb4_fr_0900_ai_ci` is available in + MySQL 8 but not MariaDB; `unicode_ci` is the portable choice. +5. **Migration tooling**: Phinx, Doctrine Migrations, or a plain PHP script. Decision at P2. +6. **`order_item_id` constraint for selections**: the business rule that + `order_item_selection.order_item_id` must reference a line with `item_type='menu'` + is enforced at application layer. A MariaDB trigger could reinforce this at DB level if + needed. + +--- + +## 11. Next steps (DDL + Seed) + +1. **DDL** (`db/migrations/0001_init_schema.sql`): transcribe this MLD into executable + `CREATE TABLE` statements, in dependency order: + - `category` -> `product`, `ingredient`, `allergen`, `role` + - `menu` (depends on `category`, `product`) + - `menu_slot` (depends on `menu`), `menu_slot_option` (depends on `menu_slot`, `product`) + - `product_ingredient` (depends on `product`, `ingredient`) + - `ingredient_allergen` (depends on `ingredient`, `allergen`) + - `user` (depends on `role`), `role_visible_source` (depends on `role`) + - `permission`, `role_permission` (depends on `role`, `permission`) + - `customer_order` + - `order_item` (depends on `customer_order`, `product`, `menu`) + - `order_item_selection` (depends on `order_item`, `menu_slot`, `product`) + - `order_item_modifier` (depends on `order_item`, `ingredient`) + - `stock_movement` (depends on `ingredient`, `customer_order`, `user`) + +2. **Seed** (`db/seeds/0001_demo_data.sql`): + - 9 categories + 53 products + 13 menus from JSON sources (`docs/merise/_sources/`) + - 13 menus with slots and slot options + - 14 allergens (INCO EU 1169/2011) + - Sample ingredient catalogue with recipes + - 5 roles with `role_permission` matrix and `role_visible_source` data + - 1 admin user + - Sample orders for demo + +3. **Fallback JSON export** (`scripts/export-fallback.{sh|php}`): extract seed data to + `src/public/borne/data/*.json` for isolated kiosk mode (Bloc 1 without DB). + +4. **DDL validation tests**: confirm CHECK constraints trigger as expected; confirm + ON DELETE CASCADE / RESTRICT / SET NULL behaviours match specification. From 6057ef990f405bf2f64a1a246e5b6aed7dc60c80 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 4 Jun 2026 15:17:33 +0000 Subject: [PATCH 7/8] docs(merise): rewrite MCT to prod-like v0.2 (4-state machine) Drop MARK_IN_PREPARATION / MARK_READY; DELIVER_ORDER as single counter/drive gesture. Add stock operations (sale decrement, restock, inventory_correction) and RBAC operations. Actors: 5 seed roles + customer. --- docs/merise/mct.md | 907 +++++++++++++++++++++++---------------------- 1 file changed, 460 insertions(+), 447 deletions(-) diff --git a/docs/merise/mct.md b/docs/merise/mct.md index 4df7ae5..05b3935 100644 --- a/docs/merise/mct.md +++ b/docs/merise/mct.md @@ -1,598 +1,611 @@ -# Modele Conceptuel des Traitements (MCT) - Wakdo +# Model of Conceptual Treatments (MCT) — Wakdo -**Phase Merise** : P1 - Conception, etape 3 (apres MCD) -**Statut** : v0.1 -**Date** : 2026-05-21 -**Branche** : `feat/p1-conception` -**Auteur methodologie** : BYAN +**Merise phase** : P1 - Conception, step 3 (after MCD) +**Version** : v0.2 — prod-like, 4-state machine +**Date** : 2026-06-04 +**Branch** : `feat/p1-conception` +**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7) +**Author** : BYAN (methodology layer) --- -## 1. Objet du document +## 1. Purpose -Le MCT (Modele Conceptuel des Traitements) decrit les **operations metier** du domaine Wakdo -sous la forme canonique Merise : **evenement declencheur -> operation -> resultat emis**. +The MCT (Model of Conceptual Treatments) describes the **business operations** of the Wakdo +domain in the canonical Merise form: **triggering event -> operation -> emitted result**. -Il repond a la question : que se passe-t-il dans le domaine, et quand ? -Il ne repond pas a la question : qui fait quoi, sur quel poste, dans quel ordre organisationnel -(cette dimension est volontairement omise - le MOT est saute, raccourci agile assume, coheret -avec le cadre RNCP solo). +It answers the question: what happens in the domain, and when? +It does not answer: who does what, on which workstation, in which organisational order +(the MOT level is intentionally skipped — agile shortcut, consistent with the solo RNCP +framework). -Le MCT couvre : -- Le parcours commande de bout en bout (borne kiosk, comptoir, drive) -- La gestion du catalogue (manager/admin) -- La gestion des utilisateurs et roles (admin) -- La connexion au back-office (tous acteurs back) +The MCT covers: +- The order lifecycle end-to-end (kiosk, counter, drive) +- Catalogue management (manager / admin) +- User and role management (admin) +- Back-office authentication (all back-office actors) -**Acteurs identifies** : +**Identified actors**: -| Acteur | Code | Interface | -|--------|------|-----------| -| Client (borne) | CLIENT | Kiosk tactile (public, non authentifie) | -| Accueil | ACCUEIL | Back-office, role `accueil` | -| Preparation (cuisine) | CUISINE | Back-office, role `preparation` | -| Manager / Administrateur | ADMIN | Back-office, role `admin` | -| Systeme | SYS | Logique interne API / PHP | +| Actor | Code | Interface | +|-------|------|-----------| +| Customer (kiosk) | CUSTOMER | Touch kiosk (public, unauthenticated) | +| Counter staff | COUNTER | Back-office, role `counter` | +| Drive staff | DRIVE | Back-office, role `drive` | +| Kitchen staff | KITCHEN | Back-office, role `kitchen` (read-only on orders) | +| Manager | MANAGER | Back-office, role `manager` | +| Administrator | ADMIN | Back-office, role `admin` | +| System | SYS | Internal API / PHP logic | -**Cross-reference MCD** : chaque operation manipule des entites du MCD (section 9). Le MCT est -construit en coherence avec la machine a etats de `commande.statut` : +**MCD cross-reference**: each operation references entities from the MCD (section 14). +The MCT is consistent with the `customer_order.status` state machine: ``` -pending_payment -> paid -> preparing -> ready -> delivered - | | | | - +-------------+-----------+----------+-> cancelled (depuis tout etat non remis) +pending_payment -> paid -> delivered + | | + +--------------+-----------> cancelled (from any non-terminal state) ``` +**Dropped states** (compared to v0.1): `preparing` and `ready` are removed. +Rationale: in a fast-food context the kitchen display (KDS) is a visual system; staff read +the ticket and act. The single staff gesture is "deliver". KPI is total time +`delivered_at - paid_at` (SLA approx. 10 min). KDS colour coding is computed from +`now - paid_at`; no additional stored state is required. + +**Dropped operations** (compared to v0.1): `MARK_IN_PREPARATION` (`MARQUER_EN_PREPARATION`) +and `MARK_READY` (`MARQUER_PRETE`) are removed because their intermediate states no longer +exist. `DELIVER_ORDER` becomes the sole status-advancing action for counter/drive staff. + --- -## 2. Conventions de representation +## 2. Representation conventions -### Format d'une operation +### Operation format ``` -[EVENEMENT(S) DECLENCHEUR(S)] +[TRIGGERING EVENT(S)] | - | [REGLE DE SYNCHRONISATION / CONDITION] + | [SYNCHRONISATION RULE / CONDITION] v ( OPERATION ) | v -[RESULTAT(S) EMIS] +[EMITTED RESULT(S)] ``` -**Synchronisations** : -- `ET` : tous les evenements doivent etre presents simultanement pour declencher l'operation -- `OU` : l'un quelconque des evenements suffit +**Synchronisations**: +- `AND`: all events must be present simultaneously to trigger the operation. +- `OR`: any one of the events is sufficient. -**Conditions** : exprimees entre crochets `[condition]` sur l'arc entrant. +**Conditions**: expressed in square brackets `[condition]` on the incoming arc. -### Notation textuelle adoptee +### Textual notation -Pour chaque operation, le document presente : -- **Evenement(s) declencheur(s)** : ce qui arrive et provoque l'operation -- **Acteur(s)** : qui est a l'origine (OU qui valide) -- **Synchronisation** : `ET` / `OU` si plusieurs evenements, condition -- **Operation** : nom de l'operation, description de ce qu'elle fait -- **Entites MCD touchees** : lecture (R) ou ecriture (W) sur les entites du MCD -- **Resultat(s)** : ce qui est emis ou produit a l'issue de l'operation +For each operation the document provides: +- **Triggering event(s)**: what occurs and causes the operation. +- **Actor(s)**: who initiates (or validates). +- **Synchronisation**: `AND` / `OR` if multiple events, plus condition. +- **Operation**: name and description of what it does. +- **MCD entities touched**: read (R) or write (W). +- **Result(s)**: what is emitted or produced. --- -## 3. Domaine 1 - Parcours commande (borne kiosk) +## 3. Domain 1 — Order lifecycle (kiosk) -Ce domaine couvre le cycle de vie d'une commande initiee depuis la borne client. +### 3.1 LOAD_CATALOGUE -### 3.1 Charger le catalogue - -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | Le client ouvre la borne (connexion au kiosk) | -| **Acteur** | CLIENT | -| **Synchronisation** | Aucune (evenement unique) | -| **Condition** | La borne est en service (dans la plage horaire 10h00-01h00) | -| **Operation** | CHARGER_CATALOGUE | -| **Description** | Recuperation de la liste des categories actives, des produits disponibles et des menus disponibles pour affichage sur la borne | -| **Entites MCD** | R : `categorie` (est_actif=1), `produit` (est_disponible=1), `menu` (est_disponible=1), `menu_produit` | -| **Resultat** | Catalogue charge, borne affiche la page d'accueil | +| Field | Value | +|-------|-------| +| **Triggering event** | Customer opens the kiosk (connection to the kiosk endpoint) | +| **Actor** | CUSTOMER | +| **Synchronisation** | None (single event) | +| **Condition** | The kiosk is in service (within business hours 10:00-01:00) | +| **Operation** | LOAD_CATALOGUE | +| **Description** | Retrieval of active categories, available products, and available menus (with their slots and eligible options) for display on the kiosk screen. | +| **MCD entities** | R: `category` (is_active=1), `product` (is_available=1), `menu` (is_available=1), `menu_slot`, `menu_slot_option`, `ingredient` (is_active=1), `allergen`, `ingredient_allergen` | +| **Result** | Catalogue loaded; kiosk displays the home screen | --- -### 3.2 Composer le panier +### 3.2 COMPOSE_CART -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | Le client selectionne un produit ou un menu sur la borne | -| **Acteur** | CLIENT | -| **Synchronisation** | Evenement repetable (OU : ajout produit, ajout menu, modification quantite, suppression item) | -| **Condition** | Le produit ou menu selectionne est disponible (`est_disponible=1`) | -| **Operation** | COMPOSER_PANIER | -| **Description** | Construction du panier en memoire : ajout d'un article (produit unitaire ou menu), avec eventuellement une option de taille (+0,50 EUR sur accompagnements et boissons), recalcul du total TTC. Le panier est une structure volatile cote client ; aucune ecriture en BDD a ce stade. | -| **Entites MCD** | R : `produit`, `menu`, `menu_produit` - W : aucune (etat volatile front) | -| **Resultat** | Panier mis a jour, total recalcule, affichage recapitulatif | +| Field | Value | +|-------|-------| +| **Triggering event** | Customer selects a product or a menu on the kiosk | +| **Actor** | CUSTOMER | +| **Synchronisation** | Repeatable event (OR: add product, add menu, change quantity, remove item, choose menu slot, choose format Normal/Maxi, add/remove ingredient modifier) | +| **Condition** | The selected product or menu has `is_available=1` | +| **Operation** | COMPOSE_CART | +| **Description** | In-memory cart construction: add an item (standalone product or menu), select slot products (`order_item_selection`), optionally modify ingredients (`order_item_modifier`), choose Normal or Maxi format for menus, recalculate TTC total. The cart is a volatile client-side structure; no database write at this stage. | +| **MCD entities** | R: `product`, `menu`, `menu_slot`, `menu_slot_option`, `ingredient`, `product_ingredient` — W: none (volatile front-end state) | +| **Result** | Cart updated, total recalculated, summary displayed | --- -### 3.3 Valider et passer la commande +### 3.3 CREATE_ORDER -| Champ | Valeur | -|-------|--------| -| **Evenements declencheurs** | 1. Client confirme le panier (appui sur "Valider") ET 2. Client saisit son numero de commande | -| **Acteur** | CLIENT | -| **Synchronisation** | ET (les deux actions sont requises) | -| **Condition** | Le panier contient au moins 1 article. Le numero saisi est non vide. | -| **Operation** | PASSER_COMMANDE | -| **Description** | Creation de la commande en base : insertion d'une ligne `commande` avec statut `pending_payment`, snapshot du total HT/TVA/TTC au taux en vigueur, source `kiosk`. Creation des lignes `ligne_commande` avec snapshot des libelles et prix. Le systeme genere le numero de commande au format `K-YYYY-MM-DD-NNN`. Le client saisit ensuite son numero de commande (substitut de paiement dans le cadre RNCP) : la commande passe au statut `paid`. La transition `pending_payment -> paid` est atomique dans cette operation. | -| **Entites MCD** | R : `produit`, `menu` (snapshot prix/libelle) - W : `commande` (INSERT statut `pending_payment`, puis UPDATE statut `paid`), `ligne_commande` (INSERT N lignes), `commande_event` (INSERT 2 events : `CREATED` user_id=NULL puis `PAID` user_id=NULL — kiosk = auto-validation, pas d'equipier) | -| **Resultat** | Commande creee (statut `paid` en fin d'operation), numero affiche au client, evenement COMMANDE_CREEE emis vers le domaine preparation | +| Field | Value | +|-------|-------| +| **Triggering events** | 1. Customer confirms cart (presses "Validate") AND 2. Customer enters their order number (RNCP payment substitute) | +| **Actor** | CUSTOMER | +| **Synchronisation** | AND (both actions required) | +| **Condition** | Cart contains at least 1 item. The order number entered is non-empty. | +| **Operation** | CREATE_ORDER | +| **Description** | Atomic order creation: INSERT `customer_order` with status `pending_payment`, source `kiosk`, snapshot of HT/VAT/TTC totals (computed line by line using `vat_rate` snapshotted per item). INSERT `order_item` lines with `label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot`. INSERT `order_item_selection` for each slot filled in a menu item. INSERT `order_item_modifier` for each ingredient modification. Decrement `ingredient.stock_quantity` for each ingredient consumed (adjusted by modifiers: remove => no decrement; add => extra decrement); INSERT one `stock_movement` row of type `sale` per affected ingredient unit. Stock decrements and order insert are within the same transaction. After the customer enters their order number, the status transitions `pending_payment -> paid` within the same transaction; `paid_at` is set. The system generates the order number in format `K-YYYY-MM-DD-NNN`. | +| **MCD entities** | 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) | +| **Result** | Order created (status `paid` at end of operation), order number displayed to customer, logical event ORDER_CREATED emitted toward the preparation domain | --- -### 3.4 Confirmer la commande au client +### 3.4 DISPLAY_CONFIRMATION -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | COMMANDE_CREEE (retour API 201 apres PASSER_COMMANDE) | -| **Acteur** | SYS | -| **Synchronisation** | Aucune | -| **Condition** | La reponse API contient un id, un numero et un statut `paid` (la transition `pending_payment -> paid` s'est executee dans PASSER_COMMANDE) | -| **Operation** | AFFICHER_CONFIRMATION | -| **Description** | Affichage de l'ecran de confirmation sur la borne avec le numero de commande. La borne se reinitialise ensuite pour le client suivant. | -| **Entites MCD** | R : aucune nouvelle lecture BDD (les donnees sont dans la reponse API) | -| **Resultat** | Ecran de confirmation affiche, borne disponible pour la commande suivante | +| Field | Value | +|-------|-------| +| **Triggering event** | ORDER_CREATED (API response 201 after CREATE_ORDER) | +| **Actor** | SYS | +| **Synchronisation** | None | +| **Condition** | API response contains an id, an order_number and status `paid` | +| **Operation** | DISPLAY_CONFIRMATION | +| **Description** | Display of the confirmation screen on the kiosk with the order number. The kiosk then resets for the next customer. | +| **MCD entities** | R: none (data is in the API response) | +| **Result** | Confirmation screen displayed; kiosk available for next order | --- -## 4. Domaine 2 - Parcours commande (comptoir et drive) +## 4. Domain 2 — Order lifecycle (counter and drive) -Ce domaine couvre la saisie manuelle d'une commande par un equipier accueil pour un client -au comptoir ou au drive. +### 4.1 CREATE_COUNTER_ORDER -### 4.1 Saisir une commande manuelle - -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'equipier accueil initie une nouvelle commande depuis le back-office | -| **Acteur** | ACCUEIL | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur est authentifie et possede la permission `commande.create`. La source est `comptoir` ou `drive`. | -| **Operation** | SAISIR_COMMANDE_MANUELLE | -| **Description** | Composition du panier via le back-office : selection de produits et menus, choix du mode de consommation, choix de la source (`comptoir` ou `drive`). Logique identique a PASSER_COMMANDE cote kiosk, a la difference que l'acteur est un equipier authentifie. La transition `pending_payment -> paid` est atomique dans cette operation (l'equipier valide le paiement du client). | -| **Entites MCD** | R : `produit`, `menu`, `menu_produit` - W : `commande` (INSERT statut `pending_payment`, puis UPDATE statut `paid`, source `comptoir` ou `drive`), `ligne_commande` (INSERT), `commande_event` (INSERT 2 events : `CREATED` user_id=acteur puis `PAID` user_id=acteur) | -| **Resultat** | Commande creee (statut `paid` en fin d'operation), numero imprime ou annonce au client | +| Field | Value | +|-------|-------| +| **Triggering event** | A counter or drive staff member initiates a new order from the back-office | +| **Actor** | COUNTER or DRIVE | +| **Synchronisation** | None | +| **Condition** | The actor is authenticated and holds permission `order.create`. The `source` is `counter` or `drive` (auto-tagged from `role.order_source`). | +| **Operation** | CREATE_COUNTER_ORDER | +| **Description** | Manual order composition via the back-office: select products and menus, choose service mode (`dine_in`/`takeaway`/`drive`), fill menu slots, add ingredient modifiers. Identical creation logic to CREATE_ORDER (snapshot, stock decrement in same transaction, atomic `pending_payment -> paid` transition). The `source` is auto-tagged from `role.order_source` (counter -> `counter`, drive -> `drive`). Order number format: `C-YYYY-MM-DD-NNN` (counter) or `D-YYYY-MM-DD-NNN` (drive). Cross-constraint: if `source = 'drive'` then `service_mode = 'drive'` (verified at creation). | +| **MCD entities** | 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`) | +| **Result** | Order created (status `paid`), order number communicated to customer | --- -## 5. Domaine 3 - Preparation (cuisine) +## 5. Domain 3 — Preparation display (kitchen) -### 5.1 Consulter les commandes a preparer +### 5.1 LIST_ORDERS_DISPLAY -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'equipier cuisine accede a sa vue ou rafraichit la liste | -| **Acteur** | CUISINE | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur est authentifie et possede la permission `commande.read`. | -| **Operation** | LISTER_COMMANDES_A_PREPARER | -| **Description** | Lecture des commandes de statut `paid` triees par `created_at` croissant (heure de passage croissante, tous canaux confondus). Affichage du numero, du contenu (lignes avec libelle snapshot), et de la source (kiosk/comptoir/drive). | -| **Entites MCD** | R : `commande` (statut=`paid`), `ligne_commande` | -| **Resultat** | Liste des commandes en attente de preparation affichee, triee par heure croissante | +| Field | Value | +|-------|-------| +| **Triggering event** | Kitchen staff accesses or refreshes the preparation display | +| **Actor** | KITCHEN (or COUNTER, DRIVE, ADMIN) | +| **Synchronisation** | None | +| **Condition** | The actor is authenticated and holds permission `order.read`. | +| **Operation** | LIST_ORDERS_DISPLAY | +| **Description** | Read `customer_order` rows with status `paid`, filtered by sources visible to the actor's role (from `role_visible_source`): kitchen sees all sources; counter sees kiosk+counter; drive sees drive. Orders are sorted by `paid_at` ascending (oldest first). For each order, display: order number, source, content (`order_item` with `label_snapshot`, `quantity`, format, slot selections, ingredient modifiers). KDS colour is computed from `now - paid_at` against the SLA threshold (approx. 10 min), not stored. Kitchen staff performs no status transition — this is a read-only operation. | +| **MCD entities** | R: `customer_order` (status=`paid`), `order_item`, `order_item_selection`, `order_item_modifier`, `role_visible_source` | +| **Result** | Preparation display list shown, sorted by payment time ascending | --- -### 5.2 Marquer une commande en preparation +## 6. Domain 4 — Delivery to customer -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'equipier cuisine clique sur "Prendre en charge" pour une commande | -| **Acteur** | CUISINE | -| **Synchronisation** | Aucune | -| **Condition** | La commande est au statut `paid`. L'acteur possede la permission `commande.update`. | -| **Operation** | MARQUER_EN_PREPARATION | -| **Description** | Transition de statut `paid` -> `preparing` sur la commande. Mise a jour de `updated_at`. La commande disparait de la file "a preparer" et passe dans la file "en preparation". | -| **Entites MCD** | W : `commande` (UPDATE statut `paid` -> `preparing`), `commande_event` (INSERT event `PREPARING_STARTED` user_id=acteur) | -| **Resultat** | Commande au statut `preparing`, evenement COMMANDE_EN_PREPARATION emis | +### 6.1 DELIVER_ORDER + +| Field | Value | +|-------|-------| +| **Triggering events** | 1. The order is at status `paid` AND 2. Counter or drive staff clicks "Delivered" | +| **Actor** | COUNTER or DRIVE | +| **Synchronisation** | AND | +| **Condition** | The order has status `paid`. The actor holds permission `order.deliver`. The actor's role is consistent with the order source (counter staff handles kiosk+counter orders; drive staff handles drive orders — filtered by role_visible_source). | +| **Operation** | DELIVER_ORDER | +| **Description** | Single-gesture transition `paid -> delivered`. Sets `delivered_at = NOW()`. The order moves to history. This operation replaces the v0.1 two-step sequence (mark-ready then deliver); the kitchen's visual confirmation (KDS) is sufficient before this action. | +| **MCD entities** | W: `customer_order` (UPDATE status `paid` -> `delivered`, `delivered_at = NOW()`) | +| **Result** | Order at status `delivered`, lifecycle complete | --- -### 5.3 Marquer une commande prete +## 7. Domain 5 — Cancellation -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'equipier cuisine clique sur "Pret" pour une commande en preparation | -| **Acteur** | CUISINE | -| **Synchronisation** | Aucune | -| **Condition** | La commande est au statut `preparing`. L'acteur possede la permission `commande.update`. | -| **Operation** | MARQUER_PRETE | -| **Description** | Transition de statut `preparing` -> `ready`. Mise a jour de `updated_at`. La commande est desormais visible pour l'accueil qui peut la remettre au client. | -| **Entites MCD** | W : `commande` (UPDATE statut `preparing` -> `ready`), `commande_event` (INSERT event `READY` user_id=acteur) | -| **Resultat** | Commande au statut `ready`, evenement COMMANDE_PRETE emis vers l'accueil | +### 7.1 CANCEL_ORDER + +| Field | Value | +|-------|-------| +| **Triggering event** | An authorised actor requests cancellation of an order | +| **Actor** | COUNTER, DRIVE, or ADMIN | +| **Synchronisation** | None | +| **Condition** | The order exists. `customer_order.status` is in `['pending_payment', 'paid']`. Terminal statuses `delivered` and `cancelled` cannot transition to `cancelled`. The actor holds permission `order.cancel`. | +| **Operation** | CANCEL_ORDER | +| **Description** | Transition from current status to `cancelled`. Sets `cancelled_at = NOW()`. The order is retained in the database for history and stats (no physical deletion). If the current status is `paid`, stock is re-credited: for each ingredient consumed by the order (accounting for modifiers), `ingredient.stock_quantity` is incremented; one `stock_movement` row of type `cancellation` is inserted per affected ingredient unit. Stock re-credit and status update are within the same transaction. | +| **MCD entities** | R: `order_item`, `order_item_modifier`, `ingredient`, `product_ingredient` — W: `customer_order` (UPDATE status -> `cancelled`, `cancelled_at = NOW()`), `ingredient` (UPDATE stock_quantity, conditional on status `paid`), `stock_movement` (INSERT type `cancellation`, conditional on status `paid`) | +| **Result** | Order at status `cancelled`, visible in admin history | --- -## 6. Domaine 4 - Remise au client (accueil) +## 8. Domain 6 — Catalogue management -### 6.1 Consulter les commandes pretes +### 8.1 CREATE_PRODUCT -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'equipier accueil accede a sa vue ou rafraichit la liste | -| **Acteur** | ACCUEIL | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur est authentifie et possede la permission `commande.read`. | -| **Operation** | LISTER_COMMANDES_PRETES | -| **Description** | Lecture des commandes de statut `ready`. Affichage du numero de commande, contenu, source. | -| **Entites MCD** | R : `commande` (statut=`ready`), `ligne_commande` | -| **Resultat** | Liste des commandes pretes affichee | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin or manager submits the product creation form | +| **Actor** | ADMIN or MANAGER | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `product.create`. Target category exists and `is_active=1`. `name` is non-empty. `price_cents > 0`. | +| **Operation** | CREATE_PRODUCT | +| **Description** | INSERT a new `product` with its category, name, price in cents, VAT rate in per-mille (`vat_rate`: 100=10%, 55=5.5%, default 100), optional image path. `is_available=1` by default. | +| **MCD entities** | R: `category` (FK validation) — W: `product` (INSERT) | +| **Result** | Product created, redirect to product list | --- -### 6.2 Declarer une commande livree +### 8.2 UPDATE_PRODUCT -| Champ | Valeur | -|-------|--------| -| **Evenements declencheurs** | 1. La commande est au statut `ready` ET 2. L'equipier accueil clique sur "Livree" | -| **Acteur** | ACCUEIL | -| **Synchronisation** | ET | -| **Condition** | La commande est au statut `ready`. L'acteur possede la permission `commande.update`. | -| **Operation** | DECLARER_LIVREE | -| **Description** | Transition de statut `ready` -> `delivered`. Fin du cycle de vie de la commande. La commande passe en historique. | -| **Entites MCD** | W : `commande` (UPDATE statut `ready` -> `delivered`), `commande_event` (INSERT event `DELIVERED` user_id=acteur) | -| **Resultat** | Commande au statut `delivered`, cycle de vie termine | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin or manager submits the product update form | +| **Actor** | ADMIN or MANAGER | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `product.update`. Product exists. New values respect constraints (`price_cents > 0`, non-empty name). | +| **Operation** | UPDATE_PRODUCT | +| **Description** | UPDATE modifiable columns (`name`, `description`, `price_cents`, `vat_rate`, `image_path`, `is_available`, `display_order`, `category_id`). Snapshots already stored in `order_item` are not affected (historical integrity guaranteed by design). | +| **MCD entities** | W: `product` (UPDATE) | +| **Result** | Product updated, product list refreshed | --- -## 7. Domaine 5 - Annulation +### 8.3 DELETE_PRODUCT -### 7.1 Annuler une commande - -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | Un acteur autorise demande l'annulation d'une commande | -| **Acteur** | ACCUEIL ou ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | La commande est dans un statut annulable : `pending_payment`, `paid`, `preparing` ou `ready`. Seuls les statuts finaux `delivered` et `cancelled` ne peuvent pas transitionner vers `cancelled` : une commande reste annulable tant qu'elle n'a pas ete remise au client (modification, annulation ou remboursement). L'acteur possede la permission `commande.cancel`. | -| **Operation** | ANNULER_COMMANDE | -| **Description** | Transition du statut courant vers `cancelled`. Mise a jour de `updated_at`. La commande reste en base pour l'historique et les stats (pas de suppression physique). | -| **Entites MCD** | W : `commande` (UPDATE statut -> `cancelled`), `commande_event` (INSERT event `CANCELLED` user_id=acteur, `payload` peut contenir la raison) | -| **Resultat** | Commande au statut `cancelled`, visible dans l'historique admin | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin confirms deletion of a product | +| **Actor** | ADMIN | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `product.delete`. Product is not a slot option in any `menu_slot_option` (FK `ON DELETE RESTRICT`). Product is not referenced in any `order_item` historical line (FK `ON DELETE RESTRICT`). Preliminary check required. | +| **Operation** | DELETE_PRODUCT | +| **Description** | Physical deletion of the product if no FK constraint blocks. If the product is referenced in a menu slot or historical order line, deletion is blocked. The recommended alternative is to deactivate (`is_available=0`). Also blocks if the product is the `burger_product_id` of any `menu`. | +| **MCD entities** | W: `product` (DELETE — blocked if referenced in `menu_slot_option`, `order_item`, or `menu.burger_product_id`) | +| **Result** | Product deleted OR error "product in use" | --- -## 8. Domaine 6 - Gestion du catalogue (admin) +### 8.4 CREATE_MENU -### 8.1 Creer un produit - -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin soumet le formulaire de creation de produit | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `produit.create`. La categorie ciblee existe et est active. Le libelle est non vide. Le prix est strictement positif. | -| **Operation** | CREER_PRODUIT | -| **Description** | Insertion d'un nouveau produit en base avec sa categorie, son libelle, son prix en centimes, son image (upload optionnel). `est_disponible` a `1` par defaut. | -| **Entites MCD** | R : `categorie` (validation FK) - W : `produit` (INSERT) | -| **Resultat** | Produit cree, retour a la liste des produits | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin or manager submits the menu creation form with its slot configuration | +| **Actor** | ADMIN or MANAGER | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `menu.create`. `name` is non-empty. `price_normal_cents > 0`, `price_maxi_cents > 0`. `burger_product_id` references an existing product. At least one slot is defined with at least one option. | +| **Operation** | CREATE_MENU | +| **Description** | Transaction: INSERT `menu` (with `burger_product_id`, `price_normal_cents`, `price_maxi_cents`), then INSERT `menu_slot` rows (one per slot: drink, side, sauce...), then INSERT `menu_slot_option` rows (eligible products per slot). | +| **MCD entities** | R: `product` (burger FK validation, slot options validation), `category` — W: `menu` (INSERT), `menu_slot` (INSERT), `menu_slot_option` (INSERT) | +| **Result** | Menu created with its slot configuration, visible on the kiosk | --- -### 8.2 Modifier un produit +### 8.5 UPDATE_MENU -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin soumet le formulaire de modification d'un produit existant | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `produit.update`. Le produit existe. Les nouvelles valeurs respectent les contraintes (prix > 0, libelle non vide). | -| **Operation** | MODIFIER_PRODUIT | -| **Description** | Mise a jour des colonnes modifiables (`libelle`, `description`, `prix_ttc_cents`, `image_path`, `est_disponible`, `ordre`, `categorie_id`). Les snapshots deja stockes dans `ligne_commande` ne sont pas affectes (integrite historique garantie par le design). | -| **Entites MCD** | W : `produit` (UPDATE) | -| **Resultat** | Produit mis a jour, liste produits rafraichie | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin or manager submits the menu update form | +| **Actor** | ADMIN or MANAGER | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `menu.update`. Menu exists. Updated configuration preserves at least one slot with at least one option. | +| **Operation** | UPDATE_MENU | +| **Description** | UPDATE `menu` columns. If slot configuration is modified: DELETE all `menu_slot_option` rows for this menu's slots, DELETE `menu_slot` rows, then re-INSERT (delete-and-reinsert pattern, atomic in transaction). Snapshots in `order_item` are not affected. | +| **MCD entities** | W: `menu` (UPDATE), `menu_slot` (DELETE + INSERT), `menu_slot_option` (DELETE + INSERT) | +| **Result** | Menu updated | --- -### 8.3 Supprimer un produit +### 8.6 DELETE_MENU -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin confirme la suppression d'un produit | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `produit.delete`. Le produit n'est pas compose dans un menu actif (FK `menu_produit.produit_id` avec ON DELETE RESTRICT). Verification prealable requise. | -| **Operation** | SUPPRIMER_PRODUIT | -| **Description** | Suppression physique du produit si aucune contrainte FK ne bloque. Si le produit est reference dans un menu, la suppression est bloquee (RESTRICT en base). La consequence metier est que l'admin doit d'abord retirer le produit de tous les menus qui le contiennent. | -| **Entites MCD** | W : `produit` (DELETE - bloque si reference dans `menu_produit`) | -| **Resultat** | Produit supprime OU erreur "produit utilise dans un menu" | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin confirms deletion of a menu | +| **Actor** | ADMIN | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `menu.delete`. Menu is not referenced in any `order_item` historical line (FK `ON DELETE RESTRICT`). Preliminary check required. | +| **Operation** | DELETE_MENU | +| **Description** | If no `order_item` references this menu: DELETE `menu_slot_option` (CASCADE from `menu_slot`), DELETE `menu_slot` (CASCADE from `menu`), DELETE `menu`. If historical references exist, propose deactivation (`is_available=0`) instead. | +| **MCD entities** | W: `menu_slot_option` (DELETE CASCADE), `menu_slot` (DELETE CASCADE), `menu` (DELETE — blocked if referenced in `order_item`) | +| **Result** | Menu deleted OR error "menu present in historical orders" | --- -### 8.4 Creer un menu +### 8.7 MANAGE_CATEGORY -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin soumet le formulaire de creation de menu avec sa composition | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `menu.create`. Le libelle est non vide. Le prix est strictement positif. Au moins un produit de role `burger` est associe (contrainte metier : un menu sans burger n'a pas de sens). | -| **Operation** | CREER_MENU | -| **Description** | Insertion du menu (`menu`) puis insertion des lignes de composition (`menu_produit`) : pour chaque produit selectionne, un enregistrement avec son role (burger, accompagnement, boisson, sauce) et sa position. | -| **Entites MCD** | R : `produit` (validation des composants), `categorie` - W : `menu` (INSERT), `menu_produit` (INSERT N lignes) | -| **Resultat** | Menu cree avec sa composition, visible sur la borne | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin or manager creates, updates, or deactivates a category | +| **Actor** | ADMIN or MANAGER | +| **Synchronisation** | OR (create, update, deactivation) | +| **Condition** | Actor holds permission `category.manage`. For deactivation: products and menus in the category are not auto-deactivated in DB (no CASCADE on `is_active`); the application layer proposes deactivating child products/menus. | +| **Operation** | MANAGE_CATEGORY | +| **Description** | CRUD on `category`. Deactivation (`is_active=0`) hides the category and its products from the kiosk without physical deletion. Physical deletion is blocked if products or menus reference this category (FK `ON DELETE RESTRICT`). | +| **MCD entities** | W: `category` (INSERT / UPDATE / conditional DELETE) | +| **Result** | Category created / updated / deactivated | --- -### 8.5 Modifier un menu +### 8.8 MANAGE_INGREDIENT -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin soumet le formulaire de modification d'un menu existant | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `menu.update`. Le menu existe. La composition modifiee conserve au moins un produit de role `burger`. | -| **Operation** | MODIFIER_MENU | -| **Description** | Mise a jour des colonnes du menu. Si la composition est modifiee : suppression de toutes les lignes `menu_produit` pour ce menu puis reinsertion (pattern delete-and-reinsert, plus simple que le diff ligne a ligne). Les snapshots deja commandes ne sont pas affectes. | -| **Entites MCD** | W : `menu` (UPDATE), `menu_produit` (DELETE + INSERT) | -| **Resultat** | Menu mis a jour | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin or manager creates, updates, or deactivates an ingredient; or manages product composition (`product_ingredient`) or allergen mapping (`ingredient_allergen`) | +| **Actor** | ADMIN or MANAGER | +| **Synchronisation** | OR (create ingredient, update ingredient, update composition, update allergen mapping) | +| **Condition** | Actor holds permission `ingredient.manage`. | +| **Operation** | MANAGE_INGREDIENT | +| **Description** | CRUD on `ingredient` (name, unit, pack_size, pack_label, low_stock_threshold, is_active). Manage `product_ingredient` composition (quantity_normal, quantity_maxi, is_removable, is_addable, extra_price_cents) for any product. Manage `ingredient_allergen` mapping (14 EU regulated allergens). Deactivating an ingredient (`is_active=0`) hides it from the configurator without deletion. Physical deletion of `ingredient` is blocked if referenced in `product_ingredient` (FK `ON DELETE RESTRICT`) or `stock_movement` (FK `ON DELETE RESTRICT`). | +| **MCD entities** | R: `product` (FK validation), `allergen` (FK validation) — W: `ingredient` (INSERT/UPDATE/DELETE conditional), `product_ingredient` (INSERT/UPDATE/DELETE), `ingredient_allergen` (INSERT/DELETE) | +| **Result** | Ingredient / composition / allergen mapping updated | --- -### 8.6 Supprimer un menu +## 9. Domain 7 — Stock management -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin confirme la suppression d'un menu | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `menu.delete`. La suppression d'un menu ne bloque pas les `ligne_commande` historiques (FK avec ON DELETE RESTRICT sur `ligne_commande.menu_id`). Verification prealable requise. | -| **Operation** | SUPPRIMER_MENU | -| **Description** | Suppression en cascade des lignes `menu_produit` (ON DELETE CASCADE), puis suppression du menu si aucune `ligne_commande` historique ne le reference. | -| **Entites MCD** | W : `menu_produit` (DELETE CASCADE), `menu` (DELETE - bloque si reference dans `ligne_commande`) | -| **Resultat** | Menu supprime OU erreur "menu present dans des commandes historiques" | +### 9.1 RESTOCK + +| Field | Value | +|-------|-------| +| **Triggering event** | Manager or admin records a delivery of ingredient packs | +| **Actor** | MANAGER or ADMIN | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `stock.manage`. Ingredient exists and `is_active=1`. Number of packs `N >= 1`. | +| **Operation** | RESTOCK | +| **Description** | UPDATE `ingredient.stock_quantity += N * pack_size`. INSERT one `stock_movement` row: type `restock`, delta `+= N * pack_size`, `user_id` of the actor, optional `note` (e.g. delivery reference). Both writes are in the same transaction. | +| **MCD entities** | R: `ingredient` — W: `ingredient` (UPDATE stock_quantity), `stock_movement` (INSERT type `restock`) | +| **Result** | Stock incremented, movement logged | --- -### 8.7 Gerer les categories +### 9.2 INVENTORY_COUNT -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin cree, modifie ou desactive une categorie | -| **Acteur** | ADMIN | -| **Synchronisation** | OU (create, update, desactivation) | -| **Condition** | L'acteur possede la permission `categorie.manage`. Pour une desactivation : les produits et menus de la categorie sont desactives en cascade applicative (pas de FK CASCADE ici, logique PHP). | -| **Operation** | GERER_CATEGORIE | -| **Description** | CRUD sur l'entite `categorie`. La desactivation d'une categorie (`est_actif=0`) masque ses produits de la borne sans suppression physique. La suppression physique est bloquee si des produits ou menus y sont rattaches (ON DELETE RESTRICT). | -| **Entites MCD** | W : `categorie` (INSERT / UPDATE / DELETE conditionnel) | -| **Resultat** | Categorie creee / mise a jour / desactivee | +| Field | Value | +|-------|-------| +| **Triggering event** | A staff member or manager records the result of a physical inventory count | +| **Actor** | KITCHEN, COUNTER, DRIVE, MANAGER, or ADMIN | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `stock.count`. Ingredient exists. Physical count `actual_quantity >= 0`. | +| **Operation** | INVENTORY_COUNT | +| **Description** | Compute `delta = actual_quantity - ingredient.stock_quantity` (may be negative or positive). UPDATE `ingredient.stock_quantity = actual_quantity`. INSERT one `stock_movement` row: type `inventory_correction`, delta = computed discrepancy, `user_id` of the actor, optional `note`. Both writes in the same transaction. | +| **MCD entities** | R: `ingredient` (read current stock_quantity) — W: `ingredient` (UPDATE stock_quantity), `stock_movement` (INSERT type `inventory_correction`) | +| **Result** | Stock reconciled to physical count, discrepancy logged | --- -## 9. Domaine 7 - Gestion des utilisateurs et roles (admin) +### 9.3 READ_STOCK -### 9.1 Creer un utilisateur - -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin soumet le formulaire de creation d'utilisateur | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `user.create`. L'email n'existe pas deja en base. Un role valide est selectionne. | -| **Operation** | CREER_USER | -| **Description** | Insertion de l'utilisateur avec hash du mot de passe (argon2id). L'email est unique. Le `role_id` est obligatoire (FK NOT NULL). | -| **Entites MCD** | R : `role` (validation FK) - W : `user` (INSERT) | -| **Resultat** | Utilisateur cree, peut se connecter au back-office | +| Field | Value | +|-------|-------| +| **Triggering event** | An authorised actor accesses the stock view | +| **Actor** | KITCHEN, COUNTER, DRIVE, MANAGER, or ADMIN | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `stock.read`. | +| **Operation** | READ_STOCK | +| **Description** | Read `ingredient` list with current `stock_quantity`, `low_stock_threshold`, `pack_size`, `pack_label`. Low-stock alert computed at display time: `stock_quantity <= low_stock_threshold`. Optional: read `stock_movement` history for a given ingredient, filtered by date range. | +| **MCD entities** | R: `ingredient`, `stock_movement` (optional history) | +| **Result** | Stock list displayed with low-stock indicators | --- -### 9.2 Modifier un utilisateur +## 10. Domain 8 — User and role management (admin) -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin soumet le formulaire de modification | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `user.update`. L'utilisateur existe. Si le mot de passe est fourni, il est rehache. | -| **Operation** | MODIFIER_USER | -| **Description** | Mise a jour des champs modifiables (`nom`, `prenom`, `email`, `role_id`, `est_actif`). Si un nouveau mot de passe est saisi, il remplace le hash existant. | -| **Entites MCD** | W : `user` (UPDATE) | -| **Resultat** | Utilisateur mis a jour | +### 10.1 CREATE_USER + +| Field | Value | +|-------|-------| +| **Triggering event** | Admin submits the user creation form | +| **Actor** | ADMIN | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `user.create`. Email does not already exist in `user.email` (UNIQUE constraint). A valid and active `role_id` is selected. | +| **Operation** | CREATE_USER | +| **Description** | INSERT user with argon2id password hash. Email is unique. `role_id` is mandatory (FK NOT NULL). `is_active=1` by default. `last_login_at=NULL` at creation. | +| **MCD entities** | R: `role` (FK validation) — W: `user` (INSERT) | +| **Result** | User created, can log into the back-office | --- -### 9.3 Desactiver un utilisateur +### 10.2 UPDATE_USER -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin clique sur "Desactiver" pour un utilisateur | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `user.update`. L'admin ne peut pas se desactiver lui-meme (protection applicative). | -| **Operation** | DESACTIVER_USER | -| **Description** | Mise a jour de `est_actif=0`. La session active de l'utilisateur est invalidee au prochain acces (verification `est_actif` dans le middleware d'authentification). L'utilisateur n'est pas supprime, son historique reste tracable. | -| **Entites MCD** | W : `user` (UPDATE est_actif=0) | -| **Resultat** | Utilisateur desactive, acces back-office bloque | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin submits the user update form | +| **Actor** | ADMIN | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `user.update`. User exists. If a new password is provided, it is re-hashed. | +| **Operation** | UPDATE_USER | +| **Description** | UPDATE modifiable fields (`first_name`, `last_name`, `email`, `role_id`, `is_active`). If a new password is supplied, it replaces the existing hash (argon2id rehash). | +| **MCD entities** | W: `user` (UPDATE) | +| **Result** | User updated | --- -### 9.4 Gerer la matrice role-permissions +### 10.3 DEACTIVATE_USER -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'admin modifie l'assignation des permissions pour un role | -| **Acteur** | ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'acteur possede la permission `role.manage`. Les permissions selectionnees existent en base. | -| **Operation** | GERER_MATRICE_RBAC | -| **Description** | Mise a jour de la table `role_permission` pour un role donne : suppression des anciennes assignations et insertion des nouvelles (pattern delete-and-reinsert). Les permissions elles-memes sont statiques (declarees en migration, non modifiables via UI). | -| **Entites MCD** | R : `role`, `permission` - W : `role_permission` (DELETE + INSERT) | -| **Resultat** | Matrice RBAC mise a jour, prise en effet au prochain acces des utilisateurs portant ce role | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin clicks "Deactivate" for a user | +| **Actor** | ADMIN | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `user.deactivate`. Admin cannot deactivate their own account (application-level protection). | +| **Operation** | DEACTIVATE_USER | +| **Description** | UPDATE `is_active=0`. The user's active session is invalidated on next access (middleware checks `is_active=1` on each authenticated request). User is not deleted; history remains traceable. | +| **MCD entities** | W: `user` (UPDATE is_active=0) | +| **Result** | User deactivated, back-office access blocked | --- -## 10. Domaine 8 - Authentification back-office +### 10.4 MANAGE_RBAC -### 10.1 Se connecter au back-office - -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | Un acteur soumet le formulaire de connexion | -| **Acteur** | ACCUEIL / CUISINE / ADMIN | -| **Synchronisation** | Aucune | -| **Condition** | L'email existe en base. Le mot de passe correspond au hash argon2id. L'utilisateur est actif (`est_actif=1`). | -| **Operation** | AUTHENTIFIER_USER | -| **Description** | Verification des identifiants. Si valides : regeneration de l'identifiant de session (protection contre la fixation de session), stockage du `user_id` et du `role_id` en session, mise a jour de `last_login_at`. Idle timeout : 4h. Absolute timeout : 10h. | -| **Entites MCD** | R : `user` (verification), `role` (chargement permissions) - W : `user` (UPDATE last_login_at) | -| **Resultat** | Session ouverte, redirection vers la vue correspondant au role | +| Field | Value | +|-------|-------| +| **Triggering event** | Admin modifies permission assignments for a role, or creates / updates a custom role | +| **Actor** | ADMIN | +| **Synchronisation** | OR (update role permissions, create custom role, update role attributes) | +| **Condition** | Actor holds permission `role.manage`. Selected permissions exist in the `permission` catalogue. | +| **Operation** | MANAGE_RBAC | +| **Description** | Update `role_permission` for a given role: DELETE existing assignments, INSERT new ones (delete-and-reinsert, atomic in transaction). Permissions themselves are static (declared in migration, not modifiable via UI). Also covers: CREATE/UPDATE custom `role` (code, label, description, default_route, order_source), UPDATE `role_visible_source` (visible dashboard sources for the role). RBAC architecture rule: application code tests permissions, not role names — adding a new role with correct permissions requires no code change. | +| **MCD entities** | R: `role`, `permission` — W: `role_permission` (DELETE + INSERT), `role` (INSERT/UPDATE for custom roles), `role_visible_source` (INSERT/DELETE) | +| **Result** | RBAC matrix updated, effective immediately for new requests of users bearing this role | --- -### 10.2 Se deconnecter du back-office +## 11. Domain 9 — Stats and KPI -| Champ | Valeur | -|-------|--------| -| **Evenement declencheur** | L'acteur clique sur "Deconnexion" ou la session expire | -| **Acteur** | ACCUEIL / CUISINE / ADMIN / SYS (expiration) | -| **Synchronisation** | OU | -| **Condition** | Une session valide est ouverte | -| **Operation** | DECONNECTER_USER | -| **Description** | Destruction de la session PHP (`session_destroy()`). La session est supprimee cote serveur. Le cookie de session est invalide. | -| **Entites MCD** | Aucune ecriture en base (la gestion de session est en PHP natif, hors BDD pour MVP) | -| **Resultat** | Session detruite, redirection vers la page de connexion | +### 11.1 READ_STATS + +| Field | Value | +|-------|-------| +| **Triggering event** | Manager or admin accesses the stats dashboard | +| **Actor** | MANAGER or ADMIN | +| **Synchronisation** | None | +| **Condition** | Actor holds permission `stats.read`. | +| **Operation** | READ_STATS | +| **Description** | Aggregate queries on `customer_order` and `order_item`. Key aggregations: order count and revenue (TTC) by `service_day` (computed with CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END; cutoff at 10:00); top products by `label_snapshot` COUNT in `order_item`; cancellation rate; average delivery time `delivered_at - paid_at`; breakdown by `source` and `service_mode`. Queries exclude cancelled orders from revenue sums but include them in volume counts. No additional stored column for `service_day`; computation at query time. | +| **MCD entities** | R: `customer_order`, `order_item` | +| **Result** | Stats dashboard displayed | --- -## 11. Machine a etats de commande.statut +## 12. Domain 10 — Back-office authentication -Synthese des transitions couvertes par les operations du MCT. +### 12.1 AUTHENTICATE_USER + +| Field | Value | +|-------|-------| +| **Triggering event** | An actor submits the login form | +| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN | +| **Synchronisation** | None | +| **Condition** | Email exists in database. Password matches argon2id hash. User `is_active=1`. | +| **Operation** | AUTHENTICATE_USER | +| **Description** | Credential verification. If valid: session ID regeneration (protection against session fixation), storage of `user_id` and `role_id` in session, UPDATE `last_login_at`. Idle timeout: 4h. Absolute timeout: 10h. Redirect to `role.default_route`. | +| **MCD entities** | R: `user` (verification), `role` (load permissions, default_route), `role_permission` — W: `user` (UPDATE last_login_at) | +| **Result** | Session opened, redirect to role-specific default view | + +--- + +### 12.2 LOGOUT_USER + +| Field | Value | +|-------|-------| +| **Triggering event** | Actor clicks "Logout" OR session expires | +| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN / SYS (expiry) | +| **Synchronisation** | OR | +| **Condition** | A valid session is open | +| **Operation** | LOGOUT_USER | +| **Description** | PHP session destruction (`session_destroy()`). Session deleted server-side. Session cookie invalidated. | +| **MCD entities** | No database write (session management is in PHP native, outside DB for this project) | +| **Result** | Session destroyed, redirect to login page | + +--- + +## 13. State machine — customer_order.status + +Summary of transitions covered by MCT operations. ``` - [CLIENT / ACCUEIL] - PASSER_COMMANDE - SAISIR_COMMANDE_MANUELLE - | - v - [ pending_payment ] (commande composee, paiement en attente) - | - [CLIENT / ACCUEIL] paiement confirme - (atomique dans PASSER_COMMANDE / SAISIR_COMMANDE_MANUELLE) - | - v - [ paid ] - | - [CUISINE] MARQUER_EN_PREPARATION - | - v - [ preparing ] - | - [CUISINE] MARQUER_PRETE - | - v - [ ready ] - | - [ACCUEIL] DECLARER_LIVREE - | - v - [ delivered ] (terminal, non annulable) + [CUSTOMER / COUNTER / DRIVE] + CREATE_ORDER + CREATE_COUNTER_ORDER + | + v + [ pending_payment ] (order composed, payment pending) + | + [CUSTOMER / COUNTER / DRIVE] payment confirmed + (atomic within CREATE_ORDER / CREATE_COUNTER_ORDER) + | + v + [ paid ] + | + [COUNTER / DRIVE] DELIVER_ORDER + | + v + [ delivered ] (terminal, cannot be cancelled) - Depuis pending_payment / paid / preparing / ready : - [ACCUEIL ou ADMIN] ANNULER_COMMANDE - | - v - [ cancelled ] (terminal) + From pending_payment / paid: + [COUNTER, DRIVE, or ADMIN] CANCEL_ORDER + | + v + [ cancelled ] (terminal) ``` -**Note sur la transition `pending_payment -> paid`** : dans le cadre RNCP, le paiement est -remplace par la saisie du numero de commande par le client (borne) ou par la validation de -l'equipier (comptoir/drive). La transition est atomique au sein des operations PASSER_COMMANDE -et SAISIR_COMMANDE_MANUELLE. Le statut `pending_payment` est visible en base le temps de la -transaction, et le statut final stocke est `paid`. Ce decoupage en deux etats reflete la -semantique metier (le client compose SA commande, PUIS il paie) et preserve la capacite -d'evolution vers un paiement reel sans migration destructive. +**Note on the `pending_payment -> paid` transition**: in the RNCP context, payment is +replaced by the customer entering their order number (kiosk) or by staff validation +(counter/drive). The transition is atomic within CREATE_ORDER and CREATE_COUNTER_ORDER. +The `pending_payment` status is not observable outside the transaction. + +**Dropped from v0.1**: `preparing` and `ready` states; `MARK_IN_PREPARATION` and `MARK_READY` +operations. Kitchen staff have a read-only view of `paid` orders (LIST_ORDERS_DISPLAY). The +single delivery action (DELIVER_ORDER) collapses the v0.1 three-step sequence into one gesture. --- -## 12. Tableau de synthese des operations +## 14. Operations summary table -| # | Operation | Domaine | Acteur | Entites W | Entites R | -|---|-----------|---------|--------|-----------|-----------| -| 1 | CHARGER_CATALOGUE | Commande kiosk | CLIENT | - | categorie, produit, menu, menu_produit | -| 2 | COMPOSER_PANIER | Commande kiosk | CLIENT | - (volatile) | produit, menu, menu_produit | -| 3 | PASSER_COMMANDE | Commande kiosk | CLIENT | commande, ligne_commande, commande_event | produit, menu | -| 4 | AFFICHER_CONFIRMATION | Commande kiosk | SYS | - | - | -| 5 | SAISIR_COMMANDE_MANUELLE | Commande comptoir/drive | ACCUEIL | commande, ligne_commande, commande_event | produit, menu, menu_produit | -| 6 | LISTER_COMMANDES_A_PREPARER | Preparation | CUISINE | - | commande, ligne_commande | -| 7 | MARQUER_EN_PREPARATION | Preparation | CUISINE | commande, commande_event | - | -| 8 | MARQUER_PRETE | Preparation | CUISINE | commande, commande_event | - | -| 9 | LISTER_COMMANDES_PRETES | Remise client | ACCUEIL | - | commande, ligne_commande | -| 10 | DECLARER_LIVREE | Remise client | ACCUEIL | commande, commande_event | - | -| 11 | ANNULER_COMMANDE | Annulation | ACCUEIL / ADMIN | commande, commande_event | - | -| 12 | CREER_PRODUIT | Catalogue | ADMIN | produit | categorie | -| 13 | MODIFIER_PRODUIT | Catalogue | ADMIN | produit | - | -| 14 | SUPPRIMER_PRODUIT | Catalogue | ADMIN | produit | menu_produit | -| 15 | CREER_MENU | Catalogue | ADMIN | menu, menu_produit | produit, categorie | -| 16 | MODIFIER_MENU | Catalogue | ADMIN | menu, menu_produit | - | -| 17 | SUPPRIMER_MENU | Catalogue | ADMIN | menu_produit, menu | ligne_commande | -| 18 | GERER_CATEGORIE | Catalogue | ADMIN | categorie | produit, menu | -| 19 | CREER_USER | RBAC | ADMIN | user | role | -| 20 | MODIFIER_USER | RBAC | ADMIN | user | - | -| 21 | DESACTIVER_USER | RBAC | ADMIN | user | - | -| 22 | GERER_MATRICE_RBAC | RBAC | ADMIN | role_permission | role, permission | -| 23 | AUTHENTIFIER_USER | Auth | ALL BACK | user | user, role | -| 24 | DECONNECTER_USER | Auth | ALL BACK | - | - | +| # | Operation | Domain | Actor | W Entities | R Entities | +|---|-----------|--------|-------|------------|------------| +| 1 | LOAD_CATALOGUE | Order kiosk | CUSTOMER | — | category, product, menu, menu_slot, menu_slot_option, ingredient, allergen, ingredient_allergen | +| 2 | COMPOSE_CART | Order kiosk | CUSTOMER | — (volatile) | product, menu, menu_slot, menu_slot_option, ingredient, product_ingredient | +| 3 | CREATE_ORDER | Order kiosk | CUSTOMER | customer_order, order_item, order_item_selection, order_item_modifier, ingredient, stock_movement | product, menu, ingredient, product_ingredient | +| 4 | DISPLAY_CONFIRMATION | Order kiosk | SYS | — | — | +| 5 | CREATE_COUNTER_ORDER | Order counter/drive | COUNTER/DRIVE | customer_order, order_item, order_item_selection, order_item_modifier, ingredient, stock_movement | product, menu, menu_slot, menu_slot_option, ingredient, product_ingredient | +| 6 | LIST_ORDERS_DISPLAY | Preparation | KITCHEN/COUNTER/DRIVE/ADMIN | — | customer_order, order_item, order_item_selection, order_item_modifier, role_visible_source | +| 7 | DELIVER_ORDER | Delivery | COUNTER/DRIVE | customer_order | — | +| 8 | CANCEL_ORDER | Cancellation | COUNTER/DRIVE/ADMIN | customer_order, ingredient, stock_movement | order_item, order_item_modifier, ingredient, product_ingredient | +| 9 | CREATE_PRODUCT | Catalogue | ADMIN/MANAGER | product | category | +| 10 | UPDATE_PRODUCT | Catalogue | ADMIN/MANAGER | product | — | +| 11 | DELETE_PRODUCT | Catalogue | ADMIN | product | menu_slot_option, order_item, menu | +| 12 | CREATE_MENU | Catalogue | ADMIN/MANAGER | menu, menu_slot, menu_slot_option | product, category | +| 13 | UPDATE_MENU | Catalogue | ADMIN/MANAGER | menu, menu_slot, menu_slot_option | — | +| 14 | DELETE_MENU | Catalogue | ADMIN | menu_slot_option, menu_slot, menu | order_item | +| 15 | MANAGE_CATEGORY | Catalogue | ADMIN/MANAGER | category | product, menu | +| 16 | MANAGE_INGREDIENT | Catalogue | ADMIN/MANAGER | ingredient, product_ingredient, ingredient_allergen | product, allergen | +| 17 | RESTOCK | Stock | MANAGER/ADMIN | ingredient, stock_movement | ingredient | +| 18 | INVENTORY_COUNT | Stock | KITCHEN/COUNTER/DRIVE/MANAGER/ADMIN | ingredient, stock_movement | ingredient | +| 19 | READ_STOCK | Stock | KITCHEN/COUNTER/DRIVE/MANAGER/ADMIN | — | ingredient, stock_movement | +| 20 | CREATE_USER | RBAC | ADMIN | user | role | +| 21 | UPDATE_USER | RBAC | ADMIN | user | — | +| 22 | DEACTIVATE_USER | RBAC | ADMIN | user | — | +| 23 | MANAGE_RBAC | RBAC | ADMIN | role_permission, role, role_visible_source | role, permission | +| 24 | READ_STATS | Stats | MANAGER/ADMIN | — | customer_order, order_item | +| 25 | AUTHENTICATE_USER | Auth | ALL BACK | user | user, role, role_permission | +| 26 | LOGOUT_USER | Auth | ALL BACK | — | — | -**Total : 24 operations** couvrant la totalite du cycle de vie metier Wakdo. +**Total: 26 operations** covering the complete Wakdo business lifecycle. --- -## 13. Cross-validation MCT -> MCD (mantra #34) +## 15. MCT -> MCD cross-validation (mantra #34) -Verification que chaque entite du MCD participe a au moins une operation du MCT. +Verification that each MCD entity participates in at least one MCT operation. -| Entite MCD | Operations qui la lisent | Operations qui l'ecrivent | Couverture | -|------------|--------------------------|--------------------------|------------| -| `categorie` | 1, 12, 15, 18 | 18 | OK | -| `produit` | 1, 2, 3, 5, 12, 14 | 12, 13, 14 | OK | -| `menu` | 1, 2, 3, 5, 15, 17 | 15, 16, 17 | OK | -| `menu_produit` | 1, 2, 5, 14 | 15, 16, 17 | OK | -| `commande` | 6, 9 | 3, 5, 7, 8, 10, 11 | OK | -| `ligne_commande` | 6, 9, 17 | 3, 5 | OK | -| `commande_event` | - (lecture via SELECT historique non listee comme operation) | 3, 5, 7, 8, 10, 11 | OK | -| `user` | 23 | 19, 20, 21, 23 | OK | -| `role` | 19, 22, 23 | 22 | OK | -| `permission` | 22 | - (statique, migration) | OK (*) | -| `role_permission` | - | 22 | OK | +| MCD entity | Operations that read | Operations that write | Coverage | +|------------|---------------------|----------------------|----------| +| `category` | 1, 9, 12, 15 | 15 | OK | +| `product` | 1, 2, 3, 5, 9, 11, 12 | 9, 10, 11 | OK | +| `menu` | 1, 2, 3, 5, 12, 14 | 12, 13, 14 | OK | +| `menu_slot` | 1, 2, 5 | 12, 13, 14 | OK | +| `menu_slot_option` | 1, 2, 5, 11 | 12, 13, 14 | OK | +| `ingredient` | 1, 2, 3, 5, 8, 16, 17, 18, 19 | 3, 5, 8, 16, 17, 18 | OK | +| `product_ingredient` | 2, 3, 5, 8 | 16 | OK | +| `allergen` | 1 | — (static seed) | OK (*) | +| `ingredient_allergen` | 1 | 16 | OK | +| `customer_order` | 6, 8, 24 | 3, 5, 7, 8 | OK | +| `order_item` | 6, 8, 14, 24 | 3, 5 | OK | +| `order_item_selection` | 6 | 3, 5 | OK | +| `order_item_modifier` | 6, 8 | 3, 5 | OK | +| `user` | 25 | 20, 21, 22, 25 | OK | +| `role` | 20, 23, 25 | 23 | OK | +| `role_visible_source` | 6 | 23 | OK | +| `permission` | 23 | — (static seed) | OK (*) | +| `role_permission` | 25 | 23 | OK | +| `stock_movement` | 19 | 3, 5, 8, 17, 18 | OK | -(*) `permission` est en lecture seule via les operations MCT : ses valeurs sont declarees en -migration SQL et ne sont pas modifiables via UI (RBAC statique cote permissions, dynamique -cote roles). Cette decision est documentee dans le MCD section 4.3. +(*) `allergen` and `permission` are read-only at the MCT level: their values are declared +in seed migrations and are not modifiable via the UI. `allergen` is managed indirectly +via `ingredient_allergen` in MANAGE_INGREDIENT. -**Conclusion** : 11/11 entites couvertes. Coherence MCT <-> MCD validee. - ---- - -## 14. Points d'incoherence detectes et signalement - -Les points suivants necessite une attention ou une decision de l'auteur : - -### 14.1 Divergence `commande.statut` entre dictionnaire et PROJECT_CONTEXT - RESOLUE - -- **Machine canonique retenue** : `pending_payment -> paid -> preparing -> ready -> delivered` (transitions nominales) ; `cancelled` atteignable depuis tout etat non remis (`pending_payment`, `paid`, `preparing`, `ready`), pour couvrir modification, annulation et remboursement client. -- **Arbitrage** : la regle metier confirmee impose deux phases successives : le client compose sa commande (statut `pending_payment`), puis il paie (statut `paid`). PROJECT_CONTEXT utilisait un terme `pending` simplifie qui ne refletait pas cette distinction. La machine canonique du dictionnaire est la source de verite. La transition `pending_payment -> paid` est atomique dans les operations PASSER_COMMANDE et SAISIR_COMMANDE_MANUELLE dans le cadre RNCP (substitut de paiement = saisie du numero). Ce point est considere comme clos. - -### 14.2 Absence d'acteur `user` lie a `commande` - RESOLUE (2026-05-28) - -**Decision actee** : pas de colonne `user_id` directe sur `commande`, mais une table d'audit dediee `commande_event` (cf. dictionnaire 3.7, MCD 4.2.bis). Pattern event sourcing simplifie. Chaque operation qui modifie `commande.statut` insere une ligne dans `commande_event` avec l'utilisateur a l'origine de la transition (NULL si auto-validation kiosk). Tracabilite complete sans denormalisation lourde sur `commande`. - -### 14.3 Colonne `source` absente de `commande` dans le dictionnaire - RESOLUE (2026-05-28) - -**Decision actee** : ajout d'une colonne `source ENUM('kiosk','comptoir','drive')` sur `commande`, en plus de `mode_consommation`. Les deux dimensions sont **distinctes** : -- `source` = canal de saisie (kiosk / comptoir / drive) - input -- `mode_consommation` = mode de consommation fiscal (sur_place / a_emporter / drive) - output - -Contrainte croisee : `source = drive` implique `mode_consommation = drive` (verifiee au MLT lors de la creation de commande). Pour `kiosk` et `comptoir`, les deux dimensions sont independantes. Documente dans le dictionnaire notes 8 et 9. - -### 14.4 Stats et `service_day` - -PROJECT_CONTEXT documente une logique `service_day` (section 2). Le MCT ne couvre pas -l'agregation des stats (cron 04h30). Ce traitement est volontairement hors scope MCT (c'est -un traitement technique automatise, pas un traitement metier interactif). Il sera documente -dans le MLT (section cron). +**Conclusion**: 19/19 entities covered. MCT <-> MCD consistency validated. From 392ba9a04035cde533e4f5f1427cfabbaa5cadba Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 4 Jun 2026 15:17:33 +0000 Subject: [PATCH 8/8] docs(merise): rewrite MLT to prod-like v0.2 (logical treatment rules) service_day 10h cutoff, VAT snapshot by line, drive cross-constraint, atomic stock decrement/re-credit, optimistic concurrency on status, dashboard filter by role_visible_source, Maxi format multiplier (quantity_normal/quantity_maxi). --- docs/merise/mlt.md | 859 ++++++++++++++++++++++++--------------------- 1 file changed, 454 insertions(+), 405 deletions(-) diff --git a/docs/merise/mlt.md b/docs/merise/mlt.md index 6ef460c..abcff87 100644 --- a/docs/merise/mlt.md +++ b/docs/merise/mlt.md @@ -1,588 +1,637 @@ -# Modele Logique des Traitements (MLT) - Wakdo +# Model of Logical Treatments (MLT) — Wakdo -**Phase Merise** : P1 - Conception, etape 4 (derive du MCT) -**Statut** : v0.1 -**Date** : 2026-05-21 -**Branche** : `feat/p1-conception` -**Auteur methodologie** : BYAN +**Merise phase** : P1 - Conception, step 4 (derived from MCT) +**Version** : v0.2 — prod-like, 4-state machine +**Date** : 2026-06-04 +**Branch** : `feat/p1-conception` +**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7) +**Author** : BYAN (methodology layer) --- -## 1. Objet du document +## 1. Purpose -Le MLT (Modele Logique des Traitements) raffine chaque operation du MCT en precisant : -- les **preconditions** (ce qui doit etre vrai avant l'execution) -- les **regles de traitement** (validations, calculs, logique metier) -- les **postconditions** (l'etat garanti apres succes) -- les **sorties** (donnees produites ou evenements emis) +The MLT (Model of Logical Treatments) refines each MCT operation by specifying: +- **preconditions** — what must be true before execution +- **business rules** — validation, computation, business logic +- **postconditions** — the state guaranteed after success +- **outputs** — produced data or emitted events +- **error cases** — alternative outputs when a condition fails -Il fait le lien entre le MCT (niveau conceptuel) et l'implementation PHP/SQL (niveau physique). -Toutes les references aux entites/attributs sont celles du dictionnaire de donnees -(`docs/merise/dictionary.md`) et du MCD (`docs/merise/mcd.md`). +It bridges the MCT (conceptual level) and the PHP/SQL implementation (physical level). +All entity/attribute references use the names from `docs/merise/dictionary.md` (English, +snake_case). All monetary amounts are in integer cents. -**Conventions de ce document** : -- `[PRE]` : precondition - doit etre satisfaite pour que l'operation s'execute -- `[RG]` : regle de gestion - logique metier appliquee pendant l'execution -- `[POST]` : postcondition - etat de la base garanti apres succes -- `[OUT]` : sortie - donnee ou evenement produit -- `[ERR]` : cas d'erreur - sortie alternative si une condition echoue +**Tag conventions**: +- `[PRE]` — precondition; must be satisfied for the operation to execute +- `[RG]` — business rule (regle de gestion); logic applied during execution +- `[POST]` — postcondition; database state guaranteed after success +- `[OUT]` — output; data or event produced +- `[ERR]` — error case; alternative output when a condition fails --- -## 2. Domaine 1 - Parcours commande (borne kiosk) +## 2. Transverse business rules -### 2.1 CHARGER_CATALOGUE +These rules apply to multiple operations and are centralised here to avoid repetition. -**Correspond a MCT section 3.1** +| Rule code | Label | Operations concerned | +|-----------|-------|----------------------| +| **RG-T01** | CSRF token verified on every back-office POST/PUT/DELETE form | AUTH, all admin ops | +| **RG-T02** | Session active + `user.is_active = 1` verified on each authenticated request | All domains 3-10 | +| **RG-T03** | Permission verified via `role_permission` before executing operation | All domains 3-10 | +| **RG-T04** | All monetary amounts are manipulated in integer cents; EUR conversion at output only | 3.3, 4.1, 8.1, 8.4 | +| **RG-T05** | Snapshots (`label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot`) on `order_item` are not modified after INSERT (historical integrity of placed orders — design guarantee) | 3.3, 4.1, 8.2, 8.5 | +| **RG-T06** | All SQL queries use PDO with prepared statements; no user data concatenated into SQL | All operations | +| **RG-T07** | Status transition UPDATE statements include `AND status = ` in the WHERE clause (optimistic concurrency protection against double transition) | 6.1, 7.1 | +| **RG-T08** | Operations touching multiple tables execute in an atomic database transaction; partial failure triggers full rollback | 3.3, 4.1, 7.1, 8.4, 9.1, 9.2 | +| **RG-T09** | Cross-constraint on `customer_order`: `source = 'drive'` implies `service_mode = 'drive'`; verified at order creation. Materialisable as a MariaDB CHECK: `CHECK (source != 'drive' OR service_mode = 'drive')`. | 3.3, 4.1 | +| **RG-T10** | VAT computation is line-by-line: each `order_item` carries its own `vat_rate_snapshot` (per-mille integer snapshotted from `product.vat_rate`). Order totals (`total_ht_cents`, `total_vat_cents`, `total_ttc_cents`) are the sum of line-level amounts. | 3.3, 4.1 | +| **RG-T11** | Stock decrements at the `pending_payment -> paid` transition and re-credits at `paid -> cancelled` are within the same database transaction as the status update (no orphan decrement). | 3.3, 4.1, 7.1 | +| **RG-T12** | Dashboard filter by source: each role's visible sources are read from `role_visible_source`; the query uses `WHERE customer_order.source IN (role_visible_sources)`. | 6.1 | -| Tag | Contenu | +--- + +## 3. Domain 1 — Order lifecycle (kiosk) + +### 3.1 LOAD_CATALOGUE + +**Corresponds to MCT section 3.1** + +| Tag | Content | |-----|---------| -| **[PRE-1]** | La requete provient d'un client sur la borne (endpoint public, pas d'authentification requise) | -| **[PRE-2]** | La plage horaire courante est comprise dans la fenetre de service (10h00-01h00) ; hors fenetre, la borne affiche un message de fermeture | -| **[RG-1]** | Lecture de toutes les `categorie` avec `est_actif = 1`, ordonnees par `categorie.ordre ASC` | -| **[RG-2]** | Pour chaque categorie, lecture des `produit` avec `est_disponible = 1` et `categorie_id = categorie.id`, ordonnes par `produit.ordre ASC` | -| **[RG-3]** | Lecture de tous les `menu` avec `est_disponible = 1`, avec jointure sur `menu_produit` pour la composition (roles et positions) | -| **[RG-4]** | Les prix sont retournes en centimes (INT) ; la conversion en EUR est effectuee cote front | -| **[POST-1]** | Aucune ecriture en base. L'etat de la base est inchange. | -| **[OUT-1]** | Reponse JSON : `{data: {categories: [...], produits: {...}, menus: [...]}}` | -| **[ERR-1]** | Si la BDD est inaccessible : reponse `{data: null, error: {code: "DB_ERROR", message: "..."}}` et le front bascule sur le JSON fallback statique | +| **[PRE-1]** | Request originates from the kiosk endpoint (public, no authentication required) | +| **[PRE-2]** | Current time is within the service window (10:00-01:00); outside the window the kiosk displays a closed message | +| **[RG-1]** | Read all `category` rows with `is_active = 1`, ordered by `category.display_order ASC` | +| **[RG-2]** | For each category, read `product` rows with `is_available = 1` and matching `category_id`, ordered by `product.display_order ASC` | +| **[RG-3]** | Read all `menu` rows with `is_available = 1`; for each menu, load `menu_slot` rows ordered by `menu_slot.display_order ASC`; for each slot, load eligible products via `menu_slot_option JOIN product` (where `product.is_available = 1`) | +| **[RG-4]** | For each product, compute allergens by joining `product_ingredient -> ingredient_allergen -> allergen` (no manual re-entry per product) | +| **[RG-5]** | For each product with `product_ingredient` rows, load `ingredient` composition (for the configurator) | +| **[RG-6]** | Prices are returned in integer cents; EUR conversion is performed client-side | +| **[POST-1]** | No database write; database state unchanged | +| **[OUT-1]** | JSON response: `{data: {categories: [...], products: {...}, menus: [{..., slots: [{..., options: [...]}]}]}}` | +| **[ERR-1]** | DB unreachable: response `{data: null, error: {code: "DB_ERROR"}}` and front-end falls back to static JSON | --- -### 2.2 COMPOSER_PANIER +### 3.2 COMPOSE_CART -**Correspond a MCT section 3.2** +**Corresponds to MCT section 3.2** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | Le catalogue a ete charge en memoire front (CHARGER_CATALOGUE effectue) | -| **[PRE-2]** | L'article selectionne (produit ou menu) est present dans le catalogue charge et a `est_disponible = 1` | -| **[RG-1]** | Le panier est une structure en memoire JavaScript (tableau d'items). Aucune persistance BDD a ce stade. | -| **[RG-2]** | Chaque item du panier contient : `type` (`produit` ou `menu`), `item_id`, `libelle`, `prix_unitaire_ttc_cents`, `quantite`, `options` (taille si applicable) | -| **[RG-3]** | Option grande taille : si `type = 'produit'` et le produit appartient aux categories `frites` ou `boissons`, l'option `grande_taille` ajoute 50 centimes au `prix_unitaire_ttc_cents` de cet item | -| **[RG-4]** | Si un item de meme `(type, item_id, options)` existe deja dans le panier, sa quantite est incrementee plutot qu'un nouvel item est ajoute | -| **[RG-5]** | Le total du panier est recalcule apres chaque modification : `SUM(prix_unitaire_ttc_cents * quantite)` sur tous les items | -| **[POST-1]** | Aucune ecriture en base. Etat panier en memoire mis a jour. | -| **[OUT-1]** | Affichage du recapitulatif panier avec le total TTC | -| **[ERR-1]** | Si un produit est passe a `est_disponible = 0` entre le chargement du catalogue et la validation, la verification se produit a l'etape PASSER_COMMANDE (validation serveur) | +| **[PRE-1]** | Catalogue loaded into front-end memory (LOAD_CATALOGUE completed) | +| **[PRE-2]** | Selected item (product or menu) is present in the loaded catalogue with `is_available = 1` | +| **[RG-1]** | Cart is a JavaScript in-memory structure (array of items); no database persistence at this stage | +| **[RG-2]** | Each item contains: `type` (`product` or `menu`), `item_id`, `label`, `unit_price_cents` (snapshot from catalogue), `quantity`, `format` (`normal` or `maxi`, for menus), `slot_selections` (array of `{menu_slot_id, product_id, label}` for menu items), `modifiers` (array of `{ingredient_id, action, extra_price_cents}`) | +| **[RG-3]** | Format Normal/Maxi (menu items only): `normal` uses `menu.price_normal_cents`; `maxi` uses `menu.price_maxi_cents`. No individual component price change is stored; the price differential is at menu level. | +| **[RG-4]** | Ingredient modifier rules: `action = 'remove'` requires `is_removable = 1` on `product_ingredient` (free); `action = 'add'` requires `is_addable = 1` (may carry `extra_price_cents`). These constraints are verified at cart composition time against the loaded catalogue. | +| **[RG-5]** | If an item with the same `(type, item_id, format, slot_selections, modifiers)` already exists in the cart, its quantity is incremented rather than adding a new item | +| **[RG-6]** | Cart total recomputed after each change: `SUM(unit_price_cents * quantity + modifier_extras)` across all items | +| **[POST-1]** | No database write; cart in-memory state updated | +| **[OUT-1]** | Cart summary displayed with TTC total | +| **[ERR-1]** | If a product becomes `is_available = 0` between catalogue load and order submission, the server-side validation in CREATE_ORDER catches it | --- -### 2.3 PASSER_COMMANDE +### 3.3 CREATE_ORDER -**Correspond a MCT section 3.3** +**Corresponds to MCT section 3.3** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | Le panier contient au moins 1 item (`items.length >= 1`) | -| **[PRE-2]** | Le numero de commande saisi par le client est non vide (validation front) | -| **[PRE-3]** | Le body JSON POST est valide (schema validation cote API) | -| **[RG-1]** | Pour chaque item du panier, le systeme verifie en base que le produit ou menu est encore `est_disponible = 1`. Si un item n'est plus disponible, la commande est rejetee avec un message liste des articles indisponibles. | -| **[RG-2]** | Determination du `tva_taux_pourmille` selon `mode_consommation` : `sur_place` = 1000 (10%), `a_emporter` = 550 (5,5%), `drive` = 550 (5,5%). Ref : service-public.fr article F31407 | -| **[RG-3]** | Calcul des montants (tout en centimes, entiers) : `total_ttc_cents = SUM(prix_unitaire_ttc_cents * quantite)` ; `total_ht_cents = ROUND(total_ttc_cents * 1000 / (1000 + tva_taux_pourmille))` ; `total_tva_cents = total_ttc_cents - total_ht_cents` | -| **[RG-4]** | Generation du numero de commande : format `K-YYYY-MM-DD-NNN` ou NNN est le compteur du jour de service courant (SELECT COUNT + 1 sur le `service_day` en cours, avec verrou pour eviter les doublons en concurrence) | -| **[RG-5]** | Insertion atomique (transaction) : INSERT `commande` puis INSERT N lignes `ligne_commande`. En cas d'echec partiel, rollback complet. | -| **[RG-6]** | Les snapshots `libelle_snapshot` et `prix_unitaire_ttc_cents_snapshot` sont copies depuis les entites courantes au moment de l'insertion (integrite historique). Ces valeurs ne sont pas modifiees apres insertion. | -| **[RG-7]** | La commande est inseree avec `statut = 'pending_payment'`. Une fois le numero de commande saisi par le client (substitut de paiement RNCP), le statut est mis a jour en `paid` dans la meme transaction. La transition `pending_payment -> paid` est atomique : aucun autre acteur ne peut observer le statut `pending_payment`. | -| **[POST-1]** | Une ligne `commande` existe en base avec `statut = 'paid'`, `source = 'kiosk'`, tous les montants calcules. La phase `pending_payment` n'est pas observable en dehors de la transaction. | -| **[POST-2]** | `N` lignes `ligne_commande` existent en base, referençant chacune soit un `produit_id` soit un `menu_id` (contrainte d'exclusivite verifiee) | -| **[POST-3]** | `commande.numero` est unique en base (contrainte UNIQUE sur la colonne) | -| **[OUT-1]** | Reponse HTTP 201 : `{data: {id: int, numero: string, statut: 'paid'}}` | -| **[OUT-2]** | Evenement logique COMMANDE_CREEE disponible pour le domaine preparation (la vue preparation se rafraichit - polling ou push selon implementation) | -| **[ERR-1]** | Panier vide : HTTP 422, `{error: {code: "EMPTY_CART"}}` | -| **[ERR-2]** | Article indisponible : HTTP 422, `{error: {code: "ITEM_UNAVAILABLE", items: [...]}}` | -| **[ERR-3]** | Erreur BDD / timeout : HTTP 500 avec rollback, `{error: {code: "DB_ERROR"}}` | +| **[PRE-1]** | Cart contains at least 1 item (`items.length >= 1`) | +| **[PRE-2]** | Order number entered by customer is non-empty (front-end validation) | +| **[PRE-3]** | POST JSON body is valid (schema validation at API layer) | +| **[RG-1]** | Server-side availability check: for each item, verify `product.is_available = 1` or `menu.is_available = 1`. If any item is unavailable, reject with list of unavailable articles. | +| **[RG-2 — service_day]** | `service_day` for a given order is computed at query time as: `CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END`. Cutoff is 10:00. This is NOT stored as a column — computed at query time only. The v0.1 formula with `INTERVAL 4 HOUR 30 MINUTE` was incorrect and is dropped. | +| **[RG-3 — order number]** | Order number format: `K-YYYY-MM-DD-NNN` where NNN is the sequential counter for the current service_day for the `kiosk` source (SELECT COUNT + 1 with a table-level lock or serialised insert to avoid duplicate generation under concurrency). Source is `kiosk` (set by the kiosk endpoint, derived from the public entry point). | +| **[RG-4 — VAT by line]** | For each `order_item`: `vat_rate_snapshot` is copied from `product.vat_rate`. Line amounts: `unit_ttc = unit_price_cents_snapshot`; `unit_ht = ROUND(unit_ttc * 1000 / (1000 + vat_rate_snapshot))`; `unit_vat = unit_ttc - unit_ht`. Order totals: `total_ttc_cents = SUM(unit_ttc * quantity)` across all lines; `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` (verified before INSERT). | +| **[RG-5 — atomic transaction]** | All writes within one database transaction: (1) INSERT `customer_order` (status `pending_payment`, source `kiosk`, service_mode from cart, computed totals); (2) INSERT `order_item` rows (label_snapshot, unit_price_cents_snapshot, vat_rate_snapshot, quantity, format, item_type, product_id or menu_id); (3) INSERT `order_item_selection` rows for each slot filled in a menu item (order_item_id, menu_slot_id, product_id, label_snapshot); (4) INSERT `order_item_modifier` rows for each ingredient modification (order_item_id, ingredient_id, action, extra_price_cents snapshot); (5) for each ingredient consumed: compute units = `(order_item.format = 'maxi' ? product_ingredient.quantity_maxi : product_ingredient.quantity_normal) * order_item.quantity`, adjusted by modifiers (remove => no decrement for that ingredient; add => extra decrement); UPDATE `ingredient.stock_quantity -= units`; INSERT `stock_movement` (type `sale`, delta = -units, order_id, user_id = NULL for kiosk); (6) UPDATE `customer_order` SET status = `paid`, `paid_at = NOW()`. All six steps commit together or roll back entirely. | +| **[RG-6 — cross-constraint]** | Source `kiosk` implies no particular service_mode constraint; the customer selects `dine_in` or `takeaway`. The drive cross-constraint (RG-T09) does not apply to kiosk-originated orders. | +| **[RG-7 — immutability]** | After INSERT, `label_snapshot`, `unit_price_cents_snapshot`, and `vat_rate_snapshot` are not modified even if the source product is later renamed or repriced (see RG-T05). | +| **[POST-1]** | One `customer_order` row exists with `status = 'paid'`, `source = 'kiosk'`, all totals computed, `paid_at` set. The `pending_payment` phase is not observable outside the transaction. | +| **[POST-2]** | N `order_item` rows exist, each referencing either a `product_id` (item_type='product') or a `menu_id` (item_type='menu') — exclusivity constraint verified. | +| **[POST-3]** | `customer_order.order_number` is unique in the database (UNIQUE constraint). | +| **[POST-4]** | `ingredient.stock_quantity` decremented for each consumed ingredient unit; one `stock_movement` row of type `sale` per affected ingredient. | +| **[OUT-1]** | HTTP 201: `{data: {id: int, order_number: string, status: 'paid'}}` | +| **[OUT-2]** | Logical event ORDER_CREATED available for preparation domain (preparation display refreshes via polling or server push depending on implementation) | +| **[ERR-1]** | Empty cart: HTTP 422, `{error: {code: "EMPTY_CART"}}` | +| **[ERR-2]** | Unavailable item: HTTP 422, `{error: {code: "ITEM_UNAVAILABLE", items: [...]}}` | +| **[ERR-3]** | DB error / timeout: HTTP 500 with rollback, `{error: {code: "DB_ERROR"}}` | --- -### 2.4 AFFICHER_CONFIRMATION +### 3.4 DISPLAY_CONFIRMATION -**Correspond a MCT section 3.4** +**Corresponds to MCT section 3.4** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | La reponse API PASSER_COMMANDE a retourne HTTP 201 avec un objet `{id, numero, statut}` | -| **[RG-1]** | Le numero de commande est affiche en grand sur l'ecran de confirmation | -| **[RG-2]** | Apres un delai configurable (suggestion : 15 secondes), la borne se reinitialise automatiquement pour le client suivant | -| **[POST-1]** | Aucune ecriture en base | -| **[OUT-1]** | Ecran de confirmation affiche avec le numero | -| **[ERR-1]** | Si la reponse API est en erreur : affichage d'un message d'erreur generic et proposition de recommencer | +| **[PRE-1]** | CREATE_ORDER returned HTTP 201 with `{id, order_number, status: 'paid'}` | +| **[RG-1]** | Order number displayed prominently on the confirmation screen | +| **[RG-2]** | After a configurable delay (suggestion: 15 seconds), the kiosk auto-resets for the next customer | +| **[POST-1]** | No database write | +| **[OUT-1]** | Confirmation screen displayed with order number | +| **[ERR-1]** | If API response is an error: generic error message displayed with option to retry | --- -## 3. Domaine 2 - Parcours commande (comptoir et drive) +## 4. Domain 2 — Order lifecycle (counter and drive) -### 3.1 SAISIR_COMMANDE_MANUELLE +### 4.1 CREATE_COUNTER_ORDER -**Correspond a MCT section 4.1** +**Corresponds to MCT section 4.1** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie (session valide, `est_actif = 1`) | -| **[PRE-2]** | L'acteur possede la permission `commande.create` (verifiee via `role_permission`) | -| **[PRE-3]** | Le panier compose contient au moins 1 article | -| **[RG-1]** | Logique de creation identique a PASSER_COMMANDE (RG-1 a RG-7), a la difference suivante : la `source` est `comptoir` ou `drive` selon le canal selectionne par l'equipier. La meme sequence `pending_payment -> paid` est appliquee de facon atomique dans la transaction. | -| **[RG-2]** | Le `mode_consommation` est saisi par l'equipier (sur_place / a_emporter / drive) | -| **[RG-3]** | Le format du numero de commande est identique : `K-YYYY-MM-DD-NNN` (meme generateur, meme compteur du jour de service) | -| **[POST-1]** | Une ligne `commande` existe en base avec `statut = 'paid'`, `source = 'comptoir'` ou `'drive'`. Le statut `pending_payment` est transitoire et non observable hors transaction. | -| **[POST-2]** | `N` lignes `ligne_commande` existent, avec snapshots | -| **[OUT-1]** | Confirmation affichee dans le back-office, numero de commande communique au client | -| **[ERR-1]** | Memes cas d'erreur que PASSER_COMMANDE (ERR-1, ERR-2, ERR-3) | +| **[PRE-1]** | Actor is authenticated (valid session, `user.is_active = 1`) | +| **[PRE-2]** | Actor holds permission `order.create` (verified via `role_permission`) | +| **[PRE-3]** | Cart contains at least 1 item | +| **[RG-1]** | Creation logic identical to CREATE_ORDER (RG-1 through RG-7 apply), with the following differences: `source` is auto-tagged from `role.order_source` (counter role -> `counter`, drive role -> `drive`); `service_mode` is selected by the staff member (`dine_in` / `takeaway` / `drive`); `user_id` is set to the authenticated user's id in `stock_movement` rows (instead of NULL for kiosk). | +| **[RG-2 — cross-constraint]** | If `source = 'drive'` then `service_mode` must be `'drive'` (RG-T09); verified before INSERT. HTTP 422 if violated. | +| **[RG-3 — order number]** | Format: `C-YYYY-MM-DD-NNN` for counter source; `D-YYYY-MM-DD-NNN` for drive source. Sequential NNN counter is per source per service_day. | +| **[RG-4 — stock]** | Same stock decrement logic as CREATE_ORDER RG-5; `stock_movement.user_id` is set to the authenticated staff member's id. | +| **[POST-1]** | One `customer_order` row with `status = 'paid'`, `source = 'counter'` or `'drive'`, `paid_at` set. | +| **[POST-2]** | N `order_item` rows with snapshots. Slot selections and modifiers written identically to kiosk flow. | +| **[POST-3]** | Stock decremented; movements logged with actor `user_id`. | +| **[OUT-1]** | HTTP 201: `{data: {id: int, order_number: string, status: 'paid'}}`. Order number communicated to customer. | +| **[ERR-1]** | Same error cases as CREATE_ORDER (ERR-1, ERR-2, ERR-3) | +| **[ERR-2]** | Cross-constraint violation (`source = drive` but `service_mode != drive`): HTTP 422, `{error: {code: "INVALID_SERVICE_MODE"}}` | --- -## 4. Domaine 3 - Preparation (cuisine) +## 5. Domain 3 — Preparation display (kitchen) -### 4.1 LISTER_COMMANDES_A_PREPARER +### 5.1 LIST_ORDERS_DISPLAY -**Correspond a MCT section 5.1** +**Corresponds to MCT section 5.1** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, `est_actif = 1`, role `preparation` ou `admin` | -| **[PRE-2]** | L'acteur possede la permission `commande.read` | -| **[RG-1]** | Requete : `SELECT commande.*, ligne_commande.* FROM commande JOIN ligne_commande ON ... WHERE commande.statut = 'paid' ORDER BY commande.created_at ASC` | -| **[RG-2]** | Tous les canaux sont confondus (kiosk + comptoir + drive) | -| **[RG-3]** | Pour chaque commande, les lignes sont affichees avec `libelle_snapshot` et `quantite` (les snapshots sont utilises, pas de re-jointure sur produit/menu) | -| **[POST-1]** | Aucune ecriture en base | -| **[OUT-1]** | Liste des commandes au statut `paid`, ordonnee par heure croissante | +| **[PRE-1]** | Actor is authenticated, `is_active = 1` | +| **[PRE-2]** | Actor holds permission `order.read` | +| **[RG-1 — source filter]** | Retrieve visible sources for the actor's role: `SELECT source FROM role_visible_source WHERE role_id = :role_id`. Kitchen sees all three; counter sees `kiosk` and `counter`; drive sees `drive`. | +| **[RG-2 — query]** | `SELECT customer_order.*, order_item.* FROM customer_order JOIN order_item ON order_item.order_id = customer_order.id WHERE customer_order.status = 'paid' AND customer_order.source IN (:visible_sources) ORDER BY customer_order.paid_at ASC` | +| **[RG-3 — item detail]** | For each order line of type `menu`, also load `order_item_selection` rows (slot choices). For all lines, load `order_item_modifier` rows (ingredient modifications). Display uses snapshots (`label_snapshot`, `quantity`, `format`); no re-join on `product` or `menu` tables needed. | +| **[RG-4 — KDS colour]** | Colour indicator computed at render time: `elapsed = NOW() - customer_order.paid_at`; green if elapsed < SLA threshold (configurable, approx. 10 min); amber if approaching; red if exceeded. Not stored; computed client-side or in PHP before response. | +| **[RG-5 — read only]** | Kitchen staff perform no status transition from this view. No UPDATE is issued by this operation. | +| **[POST-1]** | No database write | +| **[OUT-1]** | List of orders with status `paid`, filtered by role, sorted by `paid_at` ascending, with full item detail (selections, modifiers, KDS colour) | --- -### 4.2 MARQUER_EN_PREPARATION +## 6. Domain 4 — Delivery to customer -**Correspond a MCT section 5.2** +### 6.1 DELIVER_ORDER -| Tag | Contenu | +**Corresponds to MCT section 6.1** + +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `commande.update` | -| **[PRE-2]** | La commande ciblee existe et son `statut = 'paid'` | -| **[RG-1]** | `UPDATE commande SET statut = 'preparing', updated_at = NOW() WHERE id = :id AND statut = 'paid'` | -| **[RG-2]** | La clause `AND statut = 'paid'` dans le UPDATE protege contre les mises a jour concurrentes (si deux equipiers cliquent simultanement, seul le premier reussit - le second recoit 0 rows affected) | -| **[POST-1]** | `commande.statut = 'preparing'`, `commande.updated_at` mis a jour | -| **[OUT-1]** | HTTP 200 ou redirection avec message de succes. La commande disparait de la liste "a preparer" et apparait dans la liste "en preparation". | -| **[ERR-1]** | Si `statut != 'paid'` au moment du UPDATE (concurrence) : HTTP 409 `{error: {code: "INVALID_TRANSITION"}}` | +| **[PRE-1]** | Actor is authenticated, holds permission `order.deliver` | +| **[PRE-2]** | Targeted order exists and `status = 'paid'` | +| **[PRE-3]** | Order source is in the actor's visible sources (verified via `role_visible_source`) | +| **[RG-1]** | `UPDATE customer_order SET status = 'delivered', delivered_at = NOW(), updated_at = NOW() WHERE id = :id AND status = 'paid'` | +| **[RG-2 — concurrency]** | The `AND status = 'paid'` clause in the UPDATE protects against concurrent double-delivery: if two staff members click simultaneously, only the first succeeds (second receives 0 rows affected). | +| **[RG-3]** | `delivered` is a terminal status: no further transition is defined from this status (application constraint, not enforced as a DB trigger). | +| **[POST-1]** | `customer_order.status = 'delivered'`, `delivered_at` set, lifecycle complete. Order passes to history. | +| **[OUT-1]** | HTTP 200 with confirmation. Order disappears from the `paid` queue. | +| **[ERR-1]** | Invalid transition (status was not `paid` when UPDATE executed — concurrency): HTTP 409, `{error: {code: "INVALID_TRANSITION"}}` | +| **[ERR-2]** | Order source not in actor's visible sources: HTTP 403, `{error: {code: "FORBIDDEN"}}` | --- -### 4.3 MARQUER_PRETE +## 7. Domain 5 — Cancellation -**Correspond a MCT section 5.3** +### 7.1 CANCEL_ORDER -| Tag | Contenu | +**Corresponds to MCT section 7.1** + +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `commande.update` | -| **[PRE-2]** | La commande ciblee existe et son `statut = 'preparing'` | -| **[RG-1]** | `UPDATE commande SET statut = 'ready', updated_at = NOW() WHERE id = :id AND statut = 'preparing'` | -| **[RG-2]** | Meme protection contre la concurrence que MARQUER_EN_PREPARATION | -| **[POST-1]** | `commande.statut = 'ready'`, `commande.updated_at` mis a jour | -| **[OUT-1]** | La commande devient visible dans la vue "pretes" de l'accueil | -| **[ERR-1]** | Transition invalide : HTTP 409 `{error: {code: "INVALID_TRANSITION"}}` | +| **[PRE-1]** | Actor is authenticated, holds permission `order.cancel` | +| **[PRE-2]** | Targeted order exists | +| **[PRE-3]** | `customer_order.status` is in `['pending_payment', 'paid']`. Terminal statuses `delivered` and `cancelled` cannot transition to `cancelled`. | +| **[RG-1 — status update]** | `UPDATE customer_order SET status = 'cancelled', cancelled_at = NOW(), updated_at = NOW() WHERE id = :id AND status IN ('pending_payment', 'paid')` | +| **[RG-2 — concurrency]** | The `AND status IN (...)` clause protects against concurrent cancellation (see RG-T07). | +| **[RG-3 — stock re-credit — conditional]** | Re-credit applies only if the order was at status `paid` before cancellation. Orders at `pending_payment` had not yet decremented stock (the decrement occurs at the `paid` transition). For each `order_item` line of a `paid` order, recompute ingredient units consumed: `(order_item.format = 'maxi' ? product_ingredient.quantity_maxi : product_ingredient.quantity_normal) * order_item.quantity`, adjusted by `order_item_modifier` rows (remove modifier -> ingredient was not decremented, so no re-credit; add modifier -> ingredient had extra decrement, so extra re-credit). UPDATE `ingredient.stock_quantity += units`. INSERT `stock_movement` (type `cancellation`, delta = +units, order_id, user_id of actor). | +| **[RG-4 — transaction]** | Status update and stock re-credit (when applicable) execute in the same database transaction (RG-T11). | +| **[RG-5 — history]** | Order is not physically deleted; retained for history and stats. Cancelled orders are excluded from revenue totals but included in volume counts in READ_STATS. `order_item` rows are not deleted (ON DELETE CASCADE is not triggered); they allow reconstruction of what was ordered. | +| **[POST-1]** | `customer_order.status = 'cancelled'`, `cancelled_at` set, terminal state. | +| **[POST-2]** | If prior status was `paid`: `ingredient.stock_quantity` re-credited; one `stock_movement` row of type `cancellation` per affected ingredient. | +| **[OUT-1]** | HTTP 200 with cancellation confirmation | +| **[ERR-1]** | Attempt to cancel a delivered or already cancelled order: HTTP 422, `{error: {code: "CANNOT_CANCEL_IN_STATE", current_status: "..."}}` | +| **[ERR-2]** | Concurrent cancellation (0 rows affected by UPDATE): HTTP 409, `{error: {code: "INVALID_TRANSITION"}}` | --- -## 5. Domaine 4 - Remise au client (accueil) +## 8. Domain 6 — Catalogue management -### 5.1 LISTER_COMMANDES_PRETES +### 8.1 CREATE_PRODUCT -**Correspond a MCT section 6.1** +**Corresponds to MCT section 8.1** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `commande.read` | -| **[RG-1]** | `SELECT commande.*, ligne_commande.* FROM commande JOIN ligne_commande ON ... WHERE commande.statut = 'ready' ORDER BY commande.updated_at ASC` | -| **[RG-2]** | Tri par `updated_at` croissant : les commandes pretes depuis le plus longtemps apparaissent en premier | -| **[POST-1]** | Aucune ecriture en base | -| **[OUT-1]** | Liste des commandes au statut `ready` | +| **[PRE-1]** | Actor authenticated, holds permission `product.create` | +| **[PRE-2]** | `category_id` references an existing category with `is_active = 1` | +| **[RG-1]** | Form validation: `name` non-empty, `price_cents > 0`, `category_id` valid, `vat_rate` in `(55, 100)` | +| **[RG-2]** | Image upload (optional): validate MIME type (JPEG, PNG, WEBP), max size configurable (suggestion: 2 MB), store under `UPLOAD_DIR/products/`, record relative path in `image_path` | +| **[RG-3]** | `is_available = 1` by default at INSERT | +| **[RG-4]** | `display_order` set to `MAX(display_order) + 1` for the target category, or 0 if first product | +| **[POST-1]** | One `product` row in the database with all valid fields | +| **[OUT-1]** | Redirect to category product list with success message | +| **[ERR-1]** | Validation failure: inline field errors displayed | +| **[ERR-2]** | Invalid image (type or size): specific error message | --- -### 5.2 DECLARER_LIVREE +### 8.2 UPDATE_PRODUCT -**Correspond a MCT section 6.2** +**Corresponds to MCT section 8.2** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `commande.update` | -| **[PRE-2]** | La commande ciblee existe et son `statut = 'ready'` | -| **[RG-1]** | `UPDATE commande SET statut = 'delivered', updated_at = NOW() WHERE id = :id AND statut = 'ready'` | -| **[RG-2]** | `delivered` est un statut terminal : aucune transition n'est prevue depuis ce statut (contrainte applicative, non enfoercee en base) | -| **[POST-1]** | `commande.statut = 'delivered'`. Cycle de vie termine. La commande passe dans l'historique. | -| **[OUT-1]** | Confirmation de livraison affichee | -| **[ERR-1]** | Transition invalide : HTTP 409 `{error: {code: "INVALID_TRANSITION"}}` | +| **[PRE-1]** | Actor authenticated, holds permission `product.update` | +| **[PRE-2]** | Target `product.id` exists | +| **[RG-1]** | Same validations as CREATE_PRODUCT on modified fields | +| **[RG-2]** | If a new image is uploaded, the old image file is deleted from the filesystem (volume cleanup) | +| **[RG-3]** | `label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot` in historical `order_item` rows are not modified (see RG-T05) | +| **[POST-1]** | `product` updated, `updated_at` refreshed | +| **[OUT-1]** | Redirect to product list with success message | --- -## 6. Domaine 5 - Annulation +### 8.3 DELETE_PRODUCT -### 6.1 ANNULER_COMMANDE +**Corresponds to MCT section 8.3** -**Correspond a MCT section 7.1** - -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `commande.cancel` | -| **[PRE-2]** | La commande ciblee existe | -| **[PRE-3]** | `commande.statut` est dans `['pending_payment', 'paid', 'preparing', 'ready']`. Seuls les statuts finaux `delivered` et `cancelled` ne permettent pas la transition vers `cancelled` : une commande reste annulable tant qu'elle n'a pas ete remise (modification, annulation ou remboursement a la demande du client). | -| **[RG-1]** | `UPDATE commande SET statut = 'cancelled', updated_at = NOW() WHERE id = :id AND statut IN ('pending_payment', 'paid', 'preparing', 'ready')` | -| **[RG-2]** | La commande n'est pas supprimee physiquement : elle reste en base pour l'historique et les stats (les commandes annulees sont exclues du CA mais comptees dans les volumes). | -| **[RG-3]** | Les lignes `ligne_commande` ne sont pas supprimees (ON DELETE CASCADE n'est pas declenche) : elles permettent de savoir ce qui avait ete commande. | -| **[POST-1]** | `commande.statut = 'cancelled'`, etat terminal | -| **[OUT-1]** | Confirmation d'annulation | -| **[ERR-1]** | Tentative d'annulation d'une commande deja remise ou annulee (`delivered`, `cancelled`) : HTTP 422 `{error: {code: "CANNOT_CANCEL_IN_STATE"}}` | -| **[ERR-2]** | Transition invalide (concurrence) : HTTP 409 `{error: {code: "INVALID_TRANSITION"}}` | +| **[PRE-1]** | Actor authenticated, holds permission `product.delete` | +| **[PRE-2]** | Target `product.id` exists | +| **[RG-1]** | Pre-check (PHP): is the product referenced in `menu_slot_option.product_id`? If yes, display blocking message listing the menus. | +| **[RG-2]** | Pre-check (PHP): is the product the `burger_product_id` of any `menu`? If yes, block with message to delete or reassign the menu first. | +| **[RG-3]** | Pre-check (PHP): is the product referenced in `order_item.product_id` (historical orders)? FK `ON DELETE RESTRICT` blocks at DB level. Recommended response: propose deactivation (`is_available=0`) rather than deletion. | +| **[RG-4]** | FK constraints (`menu_slot_option.product_id ON DELETE RESTRICT`, `order_item.product_id ON DELETE RESTRICT`) enforce the constraint even if the PHP check is bypassed. | +| **[POST-1]** | Product deleted if no FK constraint was blocking | +| **[OUT-1]** | Redirect to product list with success message | +| **[ERR-1]** | Product in menu slot: HTTP 422 or inline message with blocking menu list | +| **[ERR-2]** | Product in historical orders: message proposing deactivation instead | --- -## 7. Domaine 6 - Gestion du catalogue (admin) +### 8.4 CREATE_MENU -### 7.1 CREER_PRODUIT +**Corresponds to MCT section 8.4** -**Correspond a MCT section 8.1** - -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `produit.create` | -| **[PRE-2]** | Le `categorie_id` fourni correspond a une `categorie` existante et active | -| **[RG-1]** | Validation du formulaire : `libelle` non vide, `prix_ttc_cents > 0`, `categorie_id` valide | -| **[RG-2]** | Si une image est uploadee : validation du type MIME (JPEG, PNG, WEBP uniquement), taille max configurable (suggestion : 2 Mo), stockage dans le volume `wakdo_uploads`, enregistrement du chemin relatif dans `image_path` | -| **[RG-3]** | `est_disponible = 1` par defaut a l'insertion | -| **[RG-4]** | `ordre` est affecte a la valeur MAX(ordre) + 1 pour la categorie ciblee, ou 0 si premiere insertion | -| **[POST-1]** | Un enregistrement `produit` existe en base avec tous les champs valides | -| **[OUT-1]** | Redirection vers la liste des produits de la categorie, message de succes | -| **[ERR-1]** | Validation echouee : affichage des erreurs de champ inline | -| **[ERR-2]** | Image invalide (type ou taille) : message d'erreur specifique | +| **[PRE-1]** | Actor authenticated, holds permission `menu.create` | +| **[PRE-2]** | `burger_product_id` references an existing, available product | +| **[PRE-3]** | At least one `menu_slot` is defined with at least one `menu_slot_option` | +| **[RG-1]** | Validation: `name` non-empty, `price_normal_cents > 0`, `price_maxi_cents > 0`, `burger_product_id` valid, all `product_id` values in slot options exist | +| **[RG-2]** | Transaction: INSERT `menu`, then INSERT `menu_slot` rows (name, slot_type, is_required, display_order), then INSERT `menu_slot_option` rows (menu_slot_id, product_id) | +| **[RG-3]** | Valid `slot_type` values (from dictionary ENUM): `drink`, `side`, `sauce`, `dessert`, `extra` | +| **[POST-1]** | One `menu` row, N `menu_slot` rows, M `menu_slot_option` rows in the database | +| **[OUT-1]** | Redirect to menu list with success message | +| **[ERR-1]** | Invalid configuration (no slot, no option): business error message | +| **[ERR-2]** | Slot option product unavailable: warning (menu can be created; product availability is checked at order time) | --- -### 7.2 MODIFIER_PRODUIT +### 8.5 UPDATE_MENU -**Correspond a MCT section 8.2** +**Corresponds to MCT section 8.5** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `produit.update` | -| **[PRE-2]** | Le `produit.id` cible existe en base | -| **[RG-1]** | Memes validations que CREER_PRODUIT sur les champs modifies | -| **[RG-2]** | Si une nouvelle image est uploadee, l'ancienne image est supprimee du filesystem (nettoyage du volume) | -| **[RG-3]** | Les `libelle_snapshot` et `prix_unitaire_ttc_cents_snapshot` dans les `ligne_commande` historiques ne sont pas modifies par ce traitement (integrite des commandes passees) | -| **[POST-1]** | `produit` mis a jour, `updated_at` rafraichi | -| **[OUT-1]** | Redirection vers la liste, message de succes | +| **[PRE-1]** | Actor authenticated, holds permission `menu.update` | +| **[PRE-2]** | Target `menu.id` exists | +| **[RG-1]** | Same validations as CREATE_MENU on modified fields | +| **[RG-2]** | If slot configuration is modified: `DELETE FROM menu_slot_option WHERE menu_slot_id IN (SELECT id FROM menu_slot WHERE menu_id = :id)`, then `DELETE FROM menu_slot WHERE menu_id = :id`, then re-INSERT (delete-and-reinsert pattern, atomic in transaction) | +| **[RG-3]** | `label_snapshot` values in historical `order_item_selection` rows are not affected (see RG-T05) | +| **[POST-1]** | `menu` updated; `menu_slot` and `menu_slot_option` rebuilt | +| **[OUT-1]** | Redirect with success message | --- -### 7.3 SUPPRIMER_PRODUIT +### 8.6 DELETE_MENU -**Correspond a MCT section 8.3** +**Corresponds to MCT section 8.6** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `produit.delete` | -| **[PRE-2]** | Le `produit.id` cible existe en base | -| **[RG-1]** | Verification prealable (PHP) : le produit est-il reference dans `menu_produit` ? Si oui, afficher un message "Ce produit est utilise dans X menu(s) : [liste]. Retirez-le d'abord des menus." et bloquer. | -| **[RG-2]** | La FK `menu_produit.produit_id` est definie avec `ON DELETE RESTRICT` en base : meme si la verification applicative est contournee, la base bloque la suppression. | -| **[RG-3]** | Si le produit est reference dans des `ligne_commande` historiques (FK `ON DELETE RESTRICT`), la suppression est egalement bloquee. Gestion recommandee : desactiver le produit (`est_disponible = 0`) plutot que le supprimer. | -| **[POST-1]** | Si aucune contrainte : le produit est supprime de la base | -| **[OUT-1]** | Redirection vers la liste, message de succes | -| **[ERR-1]** | Produit utilise dans un menu : HTTP 422 ou affichage inline avec liste des menus bloquants | -| **[ERR-2]** | Produit dans des commandes historiques : message "Ce produit a deja ete commande. Desactivez-le plutot que de le supprimer." | +| **[PRE-1]** | Actor authenticated, holds permission `menu.delete` | +| **[PRE-2]** | Target `menu.id` exists | +| **[RG-1]** | Pre-check (PHP): is the menu referenced in `order_item.menu_id`? FK `ON DELETE RESTRICT`. If yes, propose deactivation (`is_available=0`) instead of deletion. | +| **[RG-2]** | If no historical reference: DELETE `menu` triggers CASCADE to `menu_slot` (which cascades to `menu_slot_option`) | +| **[POST-1]** | `menu`, its `menu_slot` rows, and its `menu_slot_option` rows deleted | +| **[OUT-1]** | Redirect with success message | +| **[ERR-1]** | Menu in historical orders: message proposing deactivation instead | --- -### 7.4 CREER_MENU +### 8.7 MANAGE_CATEGORY -**Correspond a MCT section 8.4** +**Corresponds to MCT section 8.7** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `menu.create` | -| **[PRE-2]** | Au moins un produit de role `burger` est inclus dans la composition | -| **[PRE-3]** | Tous les `produit_id` de la composition existent et sont `est_disponible = 1` | -| **[RG-1]** | Validation : `libelle` non vide, `prix_ttc_cents > 0`, composition valide (au moins burger) | -| **[RG-2]** | Transaction : INSERT `menu`, puis INSERT N lignes `menu_produit` avec `menu_id`, `produit_id`, `role`, `position` | -| **[RG-3]** | Les roles valides pour `menu_produit.role` sont : `burger`, `accompagnement`, `boisson`, `sauce`, `dessert` (ENUM en base) | -| **[POST-1]** | Un enregistrement `menu` et ses lignes `menu_produit` existent en base | -| **[OUT-1]** | Redirection vers la liste des menus, message de succes | -| **[ERR-1]** | Composition invalide (pas de burger) : message d'erreur metier | -| **[ERR-2]** | Produit de la composition indisponible : avertissement (le menu peut etre cree avec ce produit, mais sera potentiellement affiche comme "incomplet" sur la borne) | +| **[PRE-1]** | Actor authenticated, holds permission `category.manage` | +| **[RG-CREATE]** | `name` and `slug` non-empty and unique in the database; `display_order` set to MAX + 1 | +| **[RG-UPDATE]** | UPDATE `name`, `slug`, `image_path`, `display_order`, `is_active` | +| **[RG-DEACTIVATE]** | Deactivation (`is_active=0`) does not auto-deactivate child products/menus in the DB (no CASCADE on `is_active`). PHP layer proposes to the admin to also deactivate child products/menus, or the kiosk filter on `category.is_active = 1` implicitly hides them. | +| **[RG-DELETE]** | Physical deletion blocked if `product.category_id` or `menu.category_id` references this category (FK `ON DELETE RESTRICT`). Propose deactivation. | +| **[POST-CREATE]** | New `category` row in database | +| **[POST-UPDATE]** | `category` updated, `updated_at` refreshed | +| **[OUT-1]** | Confirmation, redirect to category list | --- -### 7.5 MODIFIER_MENU +### 8.8 MANAGE_INGREDIENT -**Correspond a MCT section 8.5** +**Corresponds to MCT section 8.8** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `menu.update` | -| **[PRE-2]** | Le `menu.id` cible existe | -| **[RG-1]** | Memes validations que CREER_MENU sur les champs modifies | -| **[RG-2]** | Si la composition est modifiee : `DELETE FROM menu_produit WHERE menu_id = :id`, puis INSERT des nouvelles lignes (pattern delete-and-reinsert, atomique en transaction) | -| **[RG-3]** | Les snapshots dans `ligne_commande` ne sont pas affectes | -| **[POST-1]** | `menu` mis a jour, composition `menu_produit` reconstruite | -| **[OUT-1]** | Redirection, message de succes | +| **[PRE-1]** | Actor authenticated, holds permission `ingredient.manage` | +| **[RG-CREATE-ING]** | `name` non-empty and UNIQUE; `unit` non-empty; `pack_size >= 1`; `low_stock_threshold >= 0`; `stock_quantity` defaults to 0 at creation | +| **[RG-UPDATE-ING]** | UPDATE `name`, `unit`, `pack_size`, `pack_label`, `low_stock_threshold`, `is_active` | +| **[RG-DEACTIVATE-ING]** | `is_active=0` hides ingredient from configurator. Physical deletion blocked if referenced in `product_ingredient` (FK `ON DELETE RESTRICT`) or `stock_movement` (FK `ON DELETE RESTRICT`). | +| **[RG-COMPOSITION]** | UPDATE `product_ingredient`: for each ingredient in a product's recipe, set `quantity_normal`, `quantity_maxi`, `is_removable`, `is_addable`, `extra_price_cents`. Delete-and-reinsert pattern within transaction. | +| **[RG-ALLERGEN]** | Manage `ingredient_allergen`: INSERT or DELETE `(ingredient_id, allergen_id)` pairs. Allergen list is read-only (14 rows fixed by EU regulation 1169/2011). | +| **[POST-1]** | `ingredient` / `product_ingredient` / `ingredient_allergen` rows updated | +| **[OUT-1]** | Confirmation, redirect to ingredient list or product composition form | --- -### 7.6 SUPPRIMER_MENU +## 9. Domain 7 — Stock management -**Correspond a MCT section 8.6** +### 9.1 RESTOCK -| Tag | Contenu | +**Corresponds to MCT section 9.1** + +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `menu.delete` | -| **[PRE-2]** | Le `menu.id` cible existe | -| **[RG-1]** | Verification prealable : le menu est-il reference dans des `ligne_commande` historiques ? FK `ON DELETE RESTRICT`. Si oui, proposer la desactivation (`est_disponible = 0`) plutot que la suppression. | -| **[RG-2]** | Si aucune `ligne_commande` ne le reference : DELETE du menu (cascade automatique sur `menu_produit` via `ON DELETE CASCADE`) | -| **[POST-1]** | Menu et ses lignes `menu_produit` supprimes | -| **[OUT-1]** | Redirection, message de succes | -| **[ERR-1]** | Menu present dans des commandes historiques : message "Ce menu a deja ete commande. Desactivez-le plutot que de le supprimer." | +| **[PRE-1]** | Actor authenticated, holds permission `stock.manage` | +| **[PRE-2]** | Target ingredient exists and `is_active = 1` | +| **[PRE-3]** | Number of packs `N >= 1` | +| **[RG-1]** | `delta = N * ingredient.pack_size` | +| **[RG-2]** | Transaction: `UPDATE ingredient SET stock_quantity = stock_quantity + :delta WHERE id = :id`; INSERT `stock_movement` (ingredient_id, movement_type=`restock`, delta=+delta, order_id=NULL, user_id=actor, note=optional) | +| **[RG-3]** | `stock_movement` is append-only: no UPDATE or DELETE on this table (corrections are new rows) | +| **[POST-1]** | `ingredient.stock_quantity` incremented by `delta`. One `stock_movement` row of type `restock` inserted. | +| **[OUT-1]** | Confirmation with new stock level displayed | --- -### 7.7 GERER_CATEGORIE +### 9.2 INVENTORY_COUNT -**Correspond a MCT section 8.7** +**Corresponds to MCT section 9.2** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `categorie.manage` | -| **[RG-CREATE]** | `libelle` et `slug` non vides et uniques en base. `ordre` affecte a MAX + 1. | -| **[RG-UPDATE]** | Mises a jour de `libelle`, `slug`, `image_path`, `ordre`, `est_actif` | -| **[RG-DEACTIVATE]** | La desactivation d'une categorie (`est_actif = 0`) ne desactive pas automatiquement les produits/menus enfants en base (pas de CASCADE sur `est_actif`). La logique PHP doit proposer a l'admin de desactiver aussi les produits/menus enfants, ou la borne filtre `categorie.est_actif = 1` ce qui masque de facto les produits de la categorie. | -| **[RG-DELETE]** | Suppression physique bloquee si des `produit` ou `menu` ont `categorie_id = categorie.id` (FK `ON DELETE RESTRICT`). Proposer la desactivation. | -| **[POST-CREATE]** | Nouveau enregistrement `categorie` en base | -| **[POST-UPDATE]** | `categorie` mis a jour, `updated_at` rafraichi | -| **[OUT-1]** | Confirmation, retour a la liste des categories | +| **[PRE-1]** | Actor authenticated, holds permission `stock.count` | +| **[PRE-2]** | Target ingredient exists | +| **[PRE-3]** | `actual_quantity >= 0` (physical count is non-negative) | +| **[RG-1]** | `delta = actual_quantity - ingredient.stock_quantity` (may be negative if actual < theoretical) | +| **[RG-2]** | Transaction: `UPDATE ingredient SET stock_quantity = :actual_quantity WHERE id = :id`; INSERT `stock_movement` (ingredient_id, movement_type=`inventory_correction`, delta=computed, order_id=NULL, user_id=actor, note=optional) | +| **[RG-3]** | `delta = 0` is a valid correction (physical count matches theoretical); a movement row is still inserted for audit completeness | +| **[POST-1]** | `ingredient.stock_quantity = actual_quantity`. One `stock_movement` row of type `inventory_correction` inserted. | +| **[OUT-1]** | Confirmation with reconciled stock level and discrepancy displayed | --- -## 8. Domaine 7 - Gestion des utilisateurs et roles (admin) +### 9.3 READ_STOCK -### 8.1 CREER_USER +**Corresponds to MCT section 9.3** -**Correspond a MCT section 9.1** - -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `user.create` | -| **[PRE-2]** | L'email fourni n'existe pas dans `user.email` (contrainte UNIQUE) | -| **[PRE-3]** | Le `role_id` fourni correspond a un `role` existant et actif | -| **[RG-1]** | Validation : `email` conforme RFC 5321 (validation PHP `FILTER_VALIDATE_EMAIL`), `nom` et `prenom` non vides, `role_id` valide | -| **[RG-2]** | Hash du mot de passe : `password_hash($password, PASSWORD_ARGON2ID)`. Longueur min du mot de passe : 8 caracteres. | -| **[RG-3]** | `est_actif = 1` par defaut | -| **[RG-4]** | `last_login_at = NULL` a la creation | -| **[POST-1]** | Enregistrement `user` en base avec `password_hash` argon2id, `role_id` valide | -| **[OUT-1]** | Redirection vers la liste des utilisateurs, message de succes | -| **[ERR-1]** | Email deja existant : message "Cet email est deja utilise" | -| **[ERR-2]** | Mot de passe trop court : message de validation inline | +| **[PRE-1]** | Actor authenticated, holds permission `stock.read` | +| **[RG-1]** | `SELECT * FROM ingredient WHERE is_active = 1 ORDER BY name ASC` | +| **[RG-2]** | Low-stock alert computed at render time: `stock_quantity <= low_stock_threshold` -> flag `low_stock: true` in response. Not stored as a column. | +| **[RG-3]** | Optional movement history for a given ingredient: `SELECT * FROM stock_movement WHERE ingredient_id = :id ORDER BY created_at DESC LIMIT :n` | +| **[POST-1]** | No database write | +| **[OUT-1]** | Ingredient list with `stock_quantity`, `low_stock_threshold`, `pack_size`, `pack_label`, `low_stock` flag | --- -### 8.2 MODIFIER_USER +## 10. Domain 8 — User and role management -**Correspond a MCT section 9.2** +### 10.1 CREATE_USER -| Tag | Contenu | +**Corresponds to MCT section 10.1** + +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `user.update` | -| **[PRE-2]** | Le `user.id` cible existe | -| **[RG-1]** | Si un nouveau mot de passe est fourni (champ non vide) : rehachage via `PASSWORD_ARGON2ID` et remplacement du hash existant | -| **[RG-2]** | Si le mot de passe n'est pas modifie (champ vide) : le hash existant est conserve sans modification | -| **[RG-3]** | L'email peut etre modifie sous contrainte UNIQUE (verification avant UPDATE) | -| **[POST-1]** | `user` mis a jour, `updated_at` rafraichi | -| **[OUT-1]** | Redirection, message de succes | +| **[PRE-1]** | Actor authenticated, holds permission `user.create` | +| **[PRE-2]** | Email does not already exist in `user.email` (UNIQUE constraint) | +| **[PRE-3]** | `role_id` references an existing, active role | +| **[RG-1]** | Validation: `email` conforms to RFC 5321 (PHP `FILTER_VALIDATE_EMAIL`), `first_name` and `last_name` non-empty, `role_id` valid | +| **[RG-2]** | Password hash: `password_hash($password, PASSWORD_ARGON2ID)`. Minimum password length: 8 characters. | +| **[RG-3]** | `is_active = 1` by default; `last_login_at = NULL` at creation | +| **[POST-1]** | One `user` row with argon2id `password_hash`, valid `role_id` | +| **[OUT-1]** | Redirect to user list with success message | +| **[ERR-1]** | Duplicate email: message "This email is already in use" | +| **[ERR-2]** | Password too short: inline validation message | --- -### 8.3 DESACTIVER_USER +### 10.2 UPDATE_USER -**Correspond a MCT section 9.3** +**Corresponds to MCT section 10.2** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `user.update` | -| **[PRE-2]** | L'acteur ne cible pas son propre compte (protection : `$targetUserId !== $currentUserId`) | -| **[RG-1]** | `UPDATE user SET est_actif = 0, updated_at = NOW() WHERE id = :id` | -| **[RG-2]** | La session eventuellemement active de cet utilisateur sera invalidee au prochain acces : le middleware verifie `user.est_actif = 1` a chaque requete authentifiee | -| **[POST-1]** | `user.est_actif = 0`. L'utilisateur ne peut plus se connecter. Son historique reste intact. | -| **[OUT-1]** | Redirection, message de succes | -| **[ERR-1]** | Tentative d'auto-desactivation : HTTP 403 `{error: {code: "SELF_DEACTIVATION_FORBIDDEN"}}` | +| **[PRE-1]** | Actor authenticated, holds permission `user.update` | +| **[PRE-2]** | Target `user.id` exists | +| **[RG-1]** | If a new password is supplied (non-empty field): rehash via `PASSWORD_ARGON2ID` and replace existing hash | +| **[RG-2]** | If password field is empty: existing hash is preserved unchanged | +| **[RG-3]** | Email update subject to UNIQUE constraint (pre-check before UPDATE) | +| **[POST-1]** | `user` updated, `updated_at` refreshed | +| **[OUT-1]** | Redirect with success message | --- -### 8.4 GERER_MATRICE_RBAC +### 10.3 DEACTIVATE_USER -**Correspond a MCT section 9.4** +**Corresponds to MCT section 10.3** -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | L'acteur est authentifie, permission `role.manage` | -| **[PRE-2]** | Le `role.id` cible existe | -| **[PRE-3]** | Les `permission_id` soumis existent tous en base | -| **[RG-1]** | Transaction : `DELETE FROM role_permission WHERE role_id = :id`, puis INSERT des nouvelles lignes `(role_id, permission_id)` pour chaque permission selectionnee | -| **[RG-2]** | Les permissions ne sont pas modifiables via cette operation : elles sont uniquement lues pour construire le formulaire de selection | -| **[RG-3]** | La modification prend effet immediatement pour les nouvelles requetes ; les sessions actives des users portant ce role verront la modification au prochain acces (la session stocke le `role_id` mais les permissions sont rechargees depuis la base a chaque verification) | -| **[POST-1]** | La table `role_permission` reflete exactement les permissions selectionnees pour ce role | -| **[OUT-1]** | Redirection, message de succes | +| **[PRE-1]** | Actor authenticated, holds permission `user.deactivate` | +| **[PRE-2]** | Actor is not targeting their own account (`$targetUserId !== $currentUserId`) | +| **[RG-1]** | `UPDATE user SET is_active = 0, updated_at = NOW() WHERE id = :id` | +| **[RG-2]** | The user's potentially active session is invalidated on next request: middleware checks `user.is_active = 1` on each authenticated request | +| **[POST-1]** | `user.is_active = 0`; user cannot log in; history remains intact | +| **[OUT-1]** | Redirect with success message | +| **[ERR-1]** | Self-deactivation attempt: HTTP 403, `{error: {code: "SELF_DEACTIVATION_FORBIDDEN"}}` | --- -## 9. Domaine 8 - Authentification back-office +### 10.4 MANAGE_RBAC -### 9.1 AUTHENTIFIER_USER +**Corresponds to MCT section 10.4** -**Correspond a MCT section 10.1** - -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[PRE-1]** | Le formulaire de connexion a ete soumis avec un email et un mot de passe | -| **[PRE-2]** | Le token CSRF du formulaire est valide (protection anti-CSRF) | -| **[RG-1]** | Lookup : `SELECT * FROM user WHERE email = :email AND est_actif = 1 LIMIT 1` | -| **[RG-2]** | Verification du mot de passe : `password_verify($password, $user->password_hash)`. Si echec : meme message d'erreur generic que si l'email n'existe pas (protection contre l'enumeration d'emails). | -| **[RG-3]** | Si succes : `session_regenerate(true)` (regeneration de l'ID de session, protection contre la fixation de session) | -| **[RG-4]** | Stockage en session : `$_SESSION['user_id']`, `$_SESSION['role_id']`, `$_SESSION['logged_in_at']` | -| **[RG-5]** | Mise a jour : `UPDATE user SET last_login_at = NOW() WHERE id = :id` | -| **[RG-6]** | Timeouts de session : idle timeout 4h (detection via timestamp de derniere activite en session), absolute timeout 10h (detection via `logged_in_at`) | -| **[POST-1]** | Session PHP ouverte avec `user_id` et `role_id`. `user.last_login_at` mis a jour. | -| **[OUT-1]** | Redirection vers la vue par defaut du role (preparation -> file d'attente, accueil -> commandes pretes, admin -> dashboard) | -| **[ERR-1]** | Identifiants incorrects ou compte inactif : message generic "Email ou mot de passe incorrect" (pas de distinction pour eviter l'enumeration) | -| **[ERR-2]** | Token CSRF invalide : HTTP 403 | +| **[PRE-1]** | Actor authenticated, holds permission `role.manage` | +| **[PRE-2]** | Target `role.id` exists (for permission update) or role fields are valid (for role creation) | +| **[PRE-3]** | All submitted `permission_id` values exist in the `permission` catalogue | +| **[RG-1 — permissions]** | Transaction: `DELETE FROM role_permission WHERE role_id = :id`; INSERT new `(role_id, permission_id)` pairs for each selected permission | +| **[RG-2]** | Permissions are not modifiable via this operation: they are read-only to populate the selection form. Permission catalogue is frozen at seed. | +| **[RG-3]** | Effect is immediate for new requests; sessions of users bearing this role see the change on the next permission check (sessions store `role_id`; permissions are reloaded from DB on each check). | +| **[RG-4 — custom role]** | Creating a custom role: INSERT `role` (code UNIQUE, label, description, default_route nullable, order_source nullable); INSERT `role_visible_source` rows as needed. | +| **[RG-5 — order_source]** | `role.order_source` controls the auto-tagging of `customer_order.source` when this role creates an order. NULL for admin and manager (they can create on behalf of any channel). | +| **[POST-1]** | `role_permission` reflects exactly the selected permissions for this role | +| **[OUT-1]** | Redirect with success message | --- -### 9.2 DECONNECTER_USER +## 11. Domain 9 — Stats and KPI -**Correspond a MCT section 10.2** +### 11.1 READ_STATS -| Tag | Contenu | +**Corresponds to MCT section 11.1** + +| Tag | Content | |-----|---------| -| **[PRE-1]** | Une session valide est ouverte (`session_id()` non vide, `$_SESSION['user_id']` present) | -| **[RG-1]** | `$_SESSION = []` (vider les donnees de session) | -| **[RG-2]** | Si le cookie de session existe, l'expirer : `setcookie(session_name(), '', time() - 3600, '/', '', true, true)` | +| **[PRE-1]** | Actor authenticated, holds permission `stats.read` | +| **[RG-1 — service_day]** | `service_day` expression used in all stats aggregations: `CASE WHEN HOUR(customer_order.created_at) < 10 THEN DATE(customer_order.created_at) - INTERVAL 1 DAY ELSE DATE(customer_order.created_at) END`. Cutoff at 10:00. No stored column. The v0.1 formula with `INTERVAL 4 HOUR 30 MINUTE` is dropped. | +| **[RG-2 — revenue]** | Revenue queries filter `status != 'cancelled'`; they sum `total_ttc_cents` from `customer_order`. Cancelled orders are excluded from revenue but appear in volume counts with `status = 'cancelled'` filter. | +| **[RG-3 — top products]** | `SELECT label_snapshot, SUM(quantity) AS total_sold FROM order_item JOIN customer_order ON ... WHERE customer_order.status != 'cancelled' GROUP BY label_snapshot ORDER BY total_sold DESC LIMIT 10` | +| **[RG-4 — delivery time KPI]** | Average delivery time: `AVG(TIMESTAMPDIFF(SECOND, paid_at, delivered_at))` on orders with `status = 'delivered'`. SLA reference approx. 10 min (configurable). | +| **[RG-5 — breakdown]** | Breakdowns available by `source` (kiosk/counter/drive) and `service_mode` (dine_in/takeaway/drive) for capacity planning. `service_mode` carries no fiscal role (see dictionary note 9). | +| **[POST-1]** | No database write | +| **[OUT-1]** | Stats dashboard data: revenue by service_day, order counts, top products, cancellation rate, average delivery time, breakdown by source/service_mode | + +--- + +## 12. Domain 10 — Back-office authentication + +### 12.1 AUTHENTICATE_USER + +**Corresponds to MCT section 12.1** + +| Tag | Content | +|-----|---------| +| **[PRE-1]** | Login form submitted with email and password | +| **[PRE-2]** | CSRF token of the form is valid (anti-CSRF protection) | +| **[RG-1]** | Lookup: `SELECT * FROM user WHERE email = :email AND is_active = 1 LIMIT 1` | +| **[RG-2]** | Password verification: `password_verify($password, $user->password_hash)`. On failure: same generic error whether the email does not exist or the password is wrong (protection against email enumeration). | +| **[RG-3]** | On success: `session_regenerate(true)` (session ID regeneration, protection against session fixation) | +| **[RG-4]** | Session storage: `$_SESSION['user_id']`, `$_SESSION['role_id']`, `$_SESSION['logged_in_at']` | +| **[RG-5]** | UPDATE: `UPDATE user SET last_login_at = NOW() WHERE id = :id` | +| **[RG-6]** | Session timeouts: idle timeout 4h (detection via last-activity timestamp in session); absolute timeout 10h (detection via `logged_in_at`) | +| **[RG-7]** | Redirect target is `role.default_route` (dynamic; no hardcoded role name in routing logic) | +| **[POST-1]** | PHP session open with `user_id` and `role_id`; `user.last_login_at` updated | +| **[OUT-1]** | Redirect to `role.default_route` | +| **[ERR-1]** | Incorrect credentials or inactive account: generic message "Email or password incorrect" (no distinction to prevent enumeration) | +| **[ERR-2]** | Invalid CSRF token: HTTP 403 | + +--- + +### 12.2 LOGOUT_USER + +**Corresponds to MCT section 12.2** + +| Tag | Content | +|-----|---------| +| **[PRE-1]** | Valid session open (`session_id()` non-empty, `$_SESSION['user_id']` present) | +| **[RG-1]** | `$_SESSION = []` (clear session data) | +| **[RG-2]** | If session cookie exists, expire it: `setcookie(session_name(), '', time() - 3600, '/', '', true, true)` | | **[RG-3]** | `session_destroy()` | -| **[POST-1]** | Session PHP detruite. Aucun acces authentifie possible avec l'ancien cookie. | -| **[OUT-1]** | Redirection vers la page de connexion | +| **[POST-1]** | PHP session destroyed; no authenticated access possible with the old cookie | +| **[OUT-1]** | Redirect to login page | --- -## 10. Traitements automatises - Crons (hors interactions utilisateur) +## 13. Automated treatments — Crons (outside user interactions) -Ces traitements sont executes par le service `wakdo-cron` (container Alpine + PHP CLI) dans -la fenetre de maintenance 01h30-09h30 (hors service actif). Ils sont hors scope MCT -(traitements techniques, pas de declencheur utilisateur) mais sont documentes ici pour -coherence avec PROJECT_CONTEXT section 7 (Bloc 5 DevOps). +These treatments are executed by the `wakdo-cron` service container in the maintenance +window 01:30-09:30 (outside active service). They are outside the MCT scope (technical +treatments, no user trigger) but are documented here for consistency with PROJECT_CONTEXT. -### 10.1 Agregation des stats (cron 04h30) +### 13.1 Stats aggregation (cron 04:30) -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[TRIGGER]** | Cron : `30 4 * * *` | -| **[RG-1]** | Calcul du `service_day` ecoule : `J-1` si execution a 04h30 (dans la fenetre 01h-10h du jour J, le `service_day` a agregger est J-1) | -| **[RG-2]** | `service_day` pour une commande : `CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at - INTERVAL 1 DAY) ELSE DATE(created_at) END` | -| **[RG-3]** | Agregations calculees par `service_day` : nombre de commandes, CA TTC (somme `total_ttc_cents` des commandes `statut != 'cancelled'`), top produits (par `libelle_snapshot`, COUNT occurrences dans `ligne_commande`) | -| **[POST-1]** | Stats disponibles pour la vue dashboard admin (requetes directes sur `commande` filtrees par `service_day` ou table d'agregation si implementee) | +| **[TRIGGER]** | Cron: `30 4 * * *` | +| **[RG-1]** | `service_day` to aggregate: computed per order (see RG-1 of READ_STATS). At 04:30 the service_day in progress is the previous calendar day. | +| **[RG-2]** | Aggregations by `service_day`: order count, TTC revenue (sum `total_ttc_cents` where `status != 'cancelled'`), top products (by `label_snapshot`, COUNT in `order_item`) | +| **[POST-1]** | Stats available for admin dashboard (direct queries on `customer_order` filtered by `service_day`, or an aggregation table if implemented) | -### 10.2 Purge des sessions expirees (cron toutes les 15 min) +### 13.2 Expired sessions purge (cron every 15 min) -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[TRIGGER]** | Cron : `*/15 * * * *` | -| **[RG-1]** | Si les sessions PHP sont stockees en fichiers (defaut) : `find /tmp/sessions -mmin +240 -delete` (suppression des fichiers de session vieux de plus de 4h) | -| **[RG-2]** | Si les sessions sont en base (option) : `DELETE FROM php_sessions WHERE updated_at < NOW() - INTERVAL 4 HOUR` | -| **[POST-1]** | Sessions expirees supprimees. Les utilisateurs inactifs depuis plus de 4h seront forces a se reconnecter. | +| **[TRIGGER]** | Cron: `*/15 * * * *` | +| **[RG-1]** | File-based sessions (default): `find /tmp/sessions -mmin +240 -delete` | +| **[RG-2]** | DB-based sessions (option): `DELETE FROM php_sessions WHERE updated_at < NOW() - INTERVAL 4 HOUR` | +| **[POST-1]** | Expired sessions deleted; users inactive for more than 4h are forced to re-login | -### 10.3 Backup BDD (cron 03h00) +### 13.3 DB backup (cron 03:00) -| Tag | Contenu | +| Tag | Content | |-----|---------| -| **[TRIGGER]** | Cron : `0 3 * * *` | -| **[RG-1]** | `mysqldump` de la base `wakdo` vers un fichier date dans le volume backup | -| **[RG-2]** | Retention : conservation des 7 derniers dumps (suppression des plus anciens) | -| **[POST-1]** | Dump SQL disponible pour restauration | +| **[TRIGGER]** | Cron: `0 3 * * *` | +| **[RG-1]** | `mysqldump` of the `wakdo` database to a dated file in the backup volume | +| **[RG-2]** | Retention: keep the last 7 dumps; delete older ones | +| **[POST-1]** | SQL dump available for restoration | --- -## 11. Tableau recapitulatif des regles de gestion transverses +## 14. State machine — consistency recap (MLT) -Ces regles s'appliquent a plusieurs operations et sont centralisees ici pour eviter la -repetition. +Summary of `customer_order.status` transitions covered in the MLT, with corresponding +operations, SQL condition, concurrency protection, and phase timestamp set. -| Code RG | Libelle | Operations concernees | -|---------|---------|----------------------| -| **RG-T01** | Verification CSRF sur tous les formulaires POST/PUT/DELETE du back-office | AUTH, toutes ops admin | -| **RG-T02** | Verification session active + `est_actif = 1` sur chaque requete authentifiee | Toutes ops domaines 2-7 | -| **RG-T03** | Verification permission via `role_permission` avant execution de l'operation | Toutes ops domaines 2-7 | -| **RG-T04** | Tous les montants monetaires sont manipules en centimes (INT). Conversion EUR uniquement en sortie. | 2.3, 3.1, 7.1, 7.4 | -| **RG-T05** | Les snapshots (`libelle_snapshot`, `prix_unitaire_ttc_cents_snapshot`) ne sont pas modifies apres insertion dans `ligne_commande` (integrite historique des commandes). | 2.3, 7.2, 7.5 | -| **RG-T06** | Toutes les requetes SQL passent par PDO avec prepared statements. Aucune concatenation de donnees utilisateur dans une requete SQL. | Toutes operations | -| **RG-T07** | Les transitions de statut `commande` incluent `AND statut = ` dans la clause WHERE pour proteger contre les mises a jour concurrentes | 4.2, 4.3, 5.2, 6.1 | -| **RG-T08** | Les operations de creation/modification de catalogue ou users se font en transaction atomique quand elles touchent plusieurs tables | 2.3, 7.4, 7.5, 8.4 | -| **RG-T09** | Contrainte croisee `(source, mode_consommation)` sur `commande` : si `source = 'drive'`, alors `mode_consommation = 'drive'` (verification a la creation). Materialisable en CHECK SQL : `CHECK (source != 'drive' OR mode_consommation = 'drive')`. | 2.3, 3.1 | -| **RG-T10** | Toute operation qui modifie `commande.statut` doit aussi inserer une ligne dans `commande_event` dans la meme transaction (event_type aligne sur la transition, from_statut, to_statut, user_id de l'acteur ou NULL si auto, payload JSON optionnel). Append-only : aucun UPDATE / DELETE applicatif. A encapsuler dans un repository pour eviter les oublis. | 2.3, 3.1, 4.2, 4.3, 5.2, 6.1 | +| Transition | MLT operation | SQL condition | Concurrency protection | Phase timestamp set | +|------------|--------------|---------------|------------------------|---------------------| +| `-> pending_payment` (creation) | CREATE_ORDER (3.3), CREATE_COUNTER_ORDER (4.1) | INSERT with status `pending_payment` | Atomic transaction | `created_at` | +| `pending_payment -> paid` | CREATE_ORDER (3.3), CREATE_COUNTER_ORDER (4.1) | UPDATE in same transaction | Atomic transaction | `paid_at` | +| `paid -> delivered` | DELIVER_ORDER (6.1) | `WHERE status = 'paid'` | AND status in WHERE | `delivered_at` | +| `pending_payment/paid -> cancelled` | CANCEL_ORDER (7.1) | `WHERE status IN ('pending_payment', 'paid')` | AND status IN WHERE | `cancelled_at` | + +Terminal statuses (no further transition defined from these states): `delivered`, `cancelled`. + +**Dropped from v0.1**: +- `paid -> preparing` and `preparing -> ready` transitions — intermediate states removed. +- MARQUER_EN_PREPARATION (v0.1 MLT section 4.2) — dropped. +- MARQUER_PRETE (v0.1 MLT section 4.3) — dropped. +- `preparing` and `ready` in the cancellable state set — the cancellable set is now + `['pending_payment', 'paid']` only. +- `commande_event` table and v0.1 RG-T10 — replaced by phase timestamps on `customer_order`. --- -## 12. Coherence avec la machine a etats (recap MLT) +## 15. Residual notes and open points -Synthese des transitions de statut `commande` couvertes par le MLT, avec les operations MLT -correspondantes et les protections associees. +### 15.1 `service_day` — not materialised as a column -| Transition | Operation MLT | Condition SQL | Protection concurrence | Event audit insere | -|------------|---------------|---------------|------------------------|--------------------| -| `-> pending_payment` (creation) | PASSER_COMMANDE (2.3), SAISIR_COMMANDE_MANUELLE (3.1) | INSERT avec statut `pending_payment` | Transaction atomique | `CREATED` | -| `pending_payment -> paid` (paiement) | PASSER_COMMANDE (2.3), SAISIR_COMMANDE_MANUELLE (3.1) | UPDATE dans la meme transaction | Transaction atomique | `PAID` | -| `paid -> preparing` | MARQUER_EN_PREPARATION (4.2) | `WHERE statut = 'paid'` | AND statut dans WHERE | `PREPARING_STARTED` | -| `preparing -> ready` | MARQUER_PRETE (4.3) | `WHERE statut = 'preparing'` | AND statut dans WHERE | `READY` | -| `ready -> delivered` | DECLARER_LIVREE (5.2) | `WHERE statut = 'ready'` | AND statut dans WHERE | `DELIVERED` | -| `pending_payment/paid/preparing/ready -> cancelled` | ANNULER_COMMANDE (6.1) | `WHERE statut IN ('pending_payment', 'paid', 'preparing', 'ready')` | AND statut dans WHERE | `CANCELLED` | +The `service_day` computation is documented (RG-2 of CREATE_ORDER, RG-1 of READ_STATS): +`CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END` +(cutoff 10:00). It is computed at query time, not stored. For high-frequency stats queries, +a MariaDB generated column `VIRTUAL` or `STORED` could be added at DDL time to avoid +per-row recomputation, but this is not a blocker for the RNCP scope. +The v0.1 formula with `INTERVAL 4 HOUR 30 MINUTE` was incorrect and is dropped. -Statuts terminaux (aucune transition prevue depuis ce statut) : `delivered`, `cancelled`. +### 15.2 `order_item_modifier` for menu items -Note : la transition `pending_payment -> paid` est interne a l'operation de creation et non -observable par les autres acteurs. Le statut `pending_payment` ne sera visible dans aucune file -d'attente metier (preparation, accueil) : ces vues filtrent sur `paid`, `preparing`, `ready`. +For a menu line (`item_type='menu'`), modifiers target the fixed burger identified via +`order_item.menu_id -> menu.burger_product_id`. The constraint that modifiers reference +only ingredients belonging to the burger's `product_ingredient` is enforced at the +application layer, not at the DB FK layer (see dictionary note 10). This is a known +trade-off: a multi-column FK or a DB trigger would be needed to enforce it at DB level. +Documenting it as an application invariant is the retained approach for this project scope. ---- +### 15.3 Order number NNN counter — concurrency -## 13. Points d'incoherence signales et arbitrages attendus - -Ces points ont ete identifies lors de la construction du MLT. Ils reprennent et completent -les points signales au MCT section 14. - -### 13.1 Colonne `source` vs `mode_consommation` sur `commande` - RESOLU (2026-05-28) - -**Decision actee** : ajout d'une colonne `source ENUM('kiosk','comptoir','drive')` sur `commande`, en plus de `mode_consommation`. Deux dimensions distinctes maintenues : - -- `mode_consommation` (sur_place / a_emporter / drive) : visee fiscale, determine le taux de TVA (10% sur_place, 5,5% a_emporter en restauration rapide FR) -- `source` (kiosk / comptoir / drive) : visee operationnelle, trace le canal de saisie - -**Contrainte croisee** : `source = drive` implique `mode_consommation = drive`. Pour `kiosk` et `comptoir`, les deux dimensions sont independantes. Verifiee dans la regle [RG-T09] ci-dessous (section 11). - -Dictionnaire et MCD amendes (cf. dictionary 3.5 + notes 8/9, MCD 4.2). - -### 13.2 Tracabilite acteur sur `commande` - RESOLU (2026-05-28) - -**Decision actee** : pas de colonnes `created_by_user_id` / `prepared_by_user_id` etc. directes sur `commande`. A la place, **table d'audit dediee `commande_event`** (cf. dictionary 3.7, MCD 4.2.bis, dictionary note 10). Pattern event sourcing simplifie. - -- Append-only : aucun UPDATE / DELETE applicatif sur `commande_event` -- Chaque operation qui modifie `commande.statut` insere une ligne avec event_type, from_statut, to_statut, user_id (NULL si auto), payload (JSON nullable) -- Tracabilite complete sans denormalisation - -Pattern d'ecriture documente dans la regle [RG-T10] (section 11). - -### 13.3 Statut `pending_payment` - RESOLU - -Le statut `pending_payment` est maintenu dans la machine canonique. Il represente la phase -de composition de la commande avant paiement, conformement a la regle metier confirmee -(le client compose sa commande, PUIS il paie). La transition `pending_payment -> paid` est -atomique dans les operations de creation, ce statut est donc non observable par les files -d'attente metier. Il est reserve pour une evolution vers un paiement reel asynchrone sans -migration destructive de l'ENUM. Ce point est clos. - -### 13.4 (Information) `service_day` non persiste en colonne - -PROJECT_CONTEXT documente la logique `service_day` (section 2). Elle n'est pas -materialisee comme colonne dans le dictionnaire. Pour les requetes de stats frequentes, -une colonne calculee (colonne generee MariaDB, syntaxe `AS (expression) VIRTUAL/STORED`) -pourrait etre envisagee au DDL pour eviter de recalculer a chaque requete. Non bloquant pour MVP. +The sequential NNN counter per `(source, service_day)` could produce duplicates under +high concurrency if implemented naively as `SELECT COUNT + 1`. The recommended +implementation at DDL/code time is either: (a) a table-level advisory lock around the +count-and-insert sequence; or (b) a dedicated sequence table with an atomic increment. +The UNIQUE constraint on `order_number` provides the last-resort guard (INSERT would fail +and the application retries). This is not a blocker for the RNCP demo volume.