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).