From b8f7d350649c9e2b3e0a7bcb98319129cf08de80 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 30 Apr 2026 13:07:12 +0000 Subject: [PATCH 1/2] feat(stubs): unblock 403 with kiosk and admin index pages, plus FastCGI fixes Three changes bundled because the stubs surfaced two pre-existing infra bugs that had never been hit (the smoke test only exercised PHP via 'docker exec', not via the full Apache->PHP-FPM FastCGI path). - src/public/borne/index.html : minimal HTML stub for the kiosk vhost (200 OK with the imported logo) - src/public/admin/index.php : minimal PHP stub that proves the full FastCGI chain works end-to-end (renders PHP_VERSION + current timestamp) - docker/apache/vhost.conf : add 'DirectoryIndex index.php index.html' on the admin vhost. Without it, hitting / returned 403 because the default Apache DirectoryIndex is index.html only, and the existing RewriteRule did not apply to the directory request (\!-d cond was false). - docker/php-fpm/www.conf : comment out 'listen.allowed_clients = any'. PHP-FPM 8.3 rejects 'any' with 'Wrong IP address' and ends up dropping every connection from Apache. With the directive absent, all connections are accepted, which is acceptable in our isolated Docker network. --- docker/apache/vhost.conf | 5 +++++ docker/php-fpm/www.conf | 8 +++++++- src/public/admin/index.php | 34 ++++++++++++++++++++++++++++++++++ src/public/borne/index.html | 20 ++++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/public/admin/index.php create mode 100644 src/public/borne/index.html diff --git a/docker/apache/vhost.conf b/docker/apache/vhost.conf index 469e57c..d0411f2 100644 --- a/docker/apache/vhost.conf +++ b/docker/apache/vhost.conf @@ -98,6 +98,11 @@ AllowOverride None Require all granted + # DirectoryIndex etend a index.php pour que la racine `/` serve + # le front controller PHP sans passer par RewriteRule (qui ne se + # declenche pas sur un repertoire existant a cause du `!-d`). + DirectoryIndex index.php index.html + # Front controller MVC : toute requete non-fichier passe par index.php # qui dispatche via le Router (src/Core/Router.php a venir en P2). RewriteEngine On diff --git a/docker/php-fpm/www.conf b/docker/php-fpm/www.conf index 386956f..2028171 100644 --- a/docker/php-fpm/www.conf +++ b/docker/php-fpm/www.conf @@ -20,7 +20,13 @@ listen = 0.0.0.0:9000 ; le fait que wakdo-app n'est attache qu'au reseau interne wakdo_internal ; (non expose a l'hote, non expose au proxy Traefik). Seul wakdo-web peut ; y acceder. -listen.allowed_clients = any +; +; listen.allowed_clients est commente : PHP-FPM 8.3 ne reconnait pas la +; valeur 'any' (erreur "Wrong IP address 'any' in listen.allowed_clients, +; There are no allowed addresses"). Quand la directive est absente, +; PHP-FPM accepte toutes les connexions, ce qui est equivalent a 'any' +; et acceptable ici puisque le reseau Docker isole deja l'acces. +; listen.allowed_clients = ; --- Process manager (pm) --- ; Mode dynamic : ajuste le nombre de workers entre min et max selon la charge. diff --git a/src/public/admin/index.php b/src/public/admin/index.php new file mode 100644 index 0000000..bea045c --- /dev/null +++ b/src/public/admin/index.php @@ -0,0 +1,34 @@ + + + + + + + Wakdo - back-office + + + +

Wakdo - back-office

+

En construction.

+

Phase P1 - conception Merise en cours. Le back-office sera implemente en phases P2 a P4.

+
+

Diagnostic FastCGI : PHP repond a .

+

TODO P2 : assets partages (logo, images produits) via Apache Alias entre les 2 vhosts.

+ + diff --git a/src/public/borne/index.html b/src/public/borne/index.html new file mode 100644 index 0000000..7da4921 --- /dev/null +++ b/src/public/borne/index.html @@ -0,0 +1,20 @@ + + + + + + + Wakdo - borne client + + + + Wakdo +

Wakdo - borne client

+

En construction.

+

Phase P1 - conception Merise en cours. Le front borne sera implemente en phase P5.

+ + From d1a98764d0044b09860d79cbc7b83068d061130b Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 30 Apr 2026 14:24:14 +0000 Subject: [PATCH 2/2] docs(merise): data dictionary v0.1 - 10 entities + Mermaid ER diagram + 7 modeling notes Bottom-up derivation from school JSON sources + PROJECT_CONTEXT business rules. Covers : Categorie, Produit, Menu, MenuProduit, Commande, LigneCommande, User, Role, Permission, RolePermission. Decisions documented : prices in INT cents, VAT in per-mille, polymorphic FK with snapshots on ligne_commande, dynamic roles vs static permissions for RBAC. --- docs/merise/dictionary.md | 534 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 534 insertions(+) create mode 100644 docs/merise/dictionary.md diff --git a/docs/merise/dictionary.md b/docs/merise/dictionary.md new file mode 100644 index 0000000..70f7e40 --- /dev/null +++ b/docs/merise/dictionary.md @@ -0,0 +1,534 @@ +# Dictionnaire de donnees - Wakdo + +**Phase Merise** : P1 - Conception, etape 1 (data dictionary first, mantra #33) +**Statut** : v0.1 (squelette MCD a venir, mantra "Incremental Design") +**Date** : 2026-04-30 +**Branche** : `feat/p1-stubs-and-dictionary` + +--- + +## 1. Objet du document + +Ce dictionnaire liste **toutes les entites de donnees** identifiees pour Wakdo, avec +leurs attributs, types, contraintes et sources. Il sert de base au MCD (entites + relations), +puis au MLD (passage relationnel), puis au DDL (SQL CREATE TABLE). + +**Methodologie** : derivation bottom-up depuis les sources disponibles : +- **Source ecole** : `docs/merise/_sources/categories.json` + `produits.json` (66 produits, 9 categories) +- **Brief metier** : `docs/PROJECT_CONTEXT.md` (composition de menu, parcours commande, RBAC, + modes de consommation) +- **Maquette** : `docs/design/maquette-borne.pdf` (UX kiosk, ecrans visibles) + +Tout ecart entre la source ecole et le modele final est documente dans la section "Notes +de modelisation" en bas de ce document. + +--- + +## 2. Conventions generales + +### Naming + +- **Tables** : `snake_case` au singulier (ex : `categorie`, `produit`, `menu_produit`). + Le singulier reflete la perspective "1 ligne = 1 instance de l'entite" (convention courante + dans les ecoles francaises de gestion). Le code applicatif (PHP, JS) utilisera ces noms + tels quels. +- **Colonnes** : `snake_case`. Suffixes typiques : `_id` (FK), `_at` (timestamp), `_cents` + (montant monetaire en centimes), `_path` (chemin de fichier), `_taux` (pourcentage ou + fraction). +- **Cles primaires** : colonne `id` (INT UNSIGNED AUTO_INCREMENT). Pas de cle composite en + PK, sauf sur les tables de jointure pure. +- **Cles etrangeres** : `_id` (ex : `categorie_id` dans `produit`). + +### Types par defaut + +| Categorie | Type MariaDB | Justification | +|---|---|---| +| Identifiants | `INT UNSIGNED AUTO_INCREMENT` | 4 milliards d'ids = largement suffisant pour ce projet | +| Libelles courts | `VARCHAR(120)` | Couvre la plupart des noms produits (ex : `"Signature Beef BBQ Burger (2 viandes)"` = 41 chars) | +| Descriptions | `TEXT` | Longueur variable, pas de limite stricte | +| Montants monetaires | `INT UNSIGNED` (centimes) | Evite les bugs d'arrondi des FLOAT (cf. note 1 en bas) | +| Booleens | `TINYINT(1)` | Convention MariaDB pour `BOOLEAN` (alias) | +| Timestamps | `DATETIME` | Lisible humainement, gere les timezones via app | +| Enumerations | `ENUM('a','b','c')` | Contrainte SGBD, lisible (cf. note 2) | +| Chemins de fichiers | `VARCHAR(255)` | Limite POSIX courante pour un chemin simple | + +### Charset et collation + +- **Charset** : `utf8mb4` (RFC 3629 - UTF-8 reel sur 4 octets, supporte les emoji et caracteres + asiatiques). MariaDB gere `utf8mb4` en natif. +- **Collation** : `utf8mb4_unicode_ci` (insensible a la casse, comparaison conforme Unicode). + +### Champs d'audit (presents sur toutes les tables metier sauf jointures pures) + +| Colonne | Type | Defaut | Role | +|---|---|---|---| +| `created_at` | `DATETIME` | `CURRENT_TIMESTAMP` | Date de creation, non modifiee par la suite (ecriture unique a l'insertion) | +| `updated_at` | `DATETIME` | `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` | Date de derniere modification, mise a jour automatique | + +### Soft delete + +Pas de soft delete generalise pour MVP. Les entites qui peuvent etre desactivees temporairement +ont une colonne `est_actif` ou `est_disponible` (boolean). La suppression dure (`DELETE`) +reste possible mais reservee a des operations admin avec sauvegarde prealable. + +--- + +## 3. Entites + +### 3.1 `categorie` + +Regroupement metier des produits et menus pour l'affichage sur la borne. + +| Attribut | Type | NULL | Defaut | Contrainte | Source ecole | Notes | +|---|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | `id` (1-9) | identique source | +| `libelle` | VARCHAR(60) | NO | - | UNIQUE | `title` | renomme depuis `title` (semantique francaise) | +| `slug` | VARCHAR(60) | NO | - | UNIQUE | derive de `title` (kebab-case lowercase) | utile pour URL `/api/categories/burgers` | +| `image_path` | VARCHAR(255) | YES | NULL | - | `image` | normalisation post-import (kebab-case lowercase) | +| `ordre` | SMALLINT UNSIGNED | NO | 0 | - | (enrichi) | ordre d'affichage sur la borne, ajustable depuis admin | +| `est_actif` | TINYINT(1) | NO | 1 | - | (enrichi) | permet de desactiver une categorie sans la supprimer | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | audit | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | - | audit | + +**Exemples** : `menus`, `boissons`, `burgers`, `frites`, `encas`, `wraps`, `salades`, +`desserts`, `sauces`. Volume : 9 lignes a l'init (seed depuis `categories.json`). + +--- + +### 3.2 `produit` + +Article unitaire vendable a la carte ou comme composant d'un menu. + +| Attribut | Type | NULL | Defaut | Contrainte | Source ecole | Notes | +|---|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | `id` (14-66 selon categorie) | identique source | +| `categorie_id` | INT UNSIGNED | NO | - | FK -> `categorie(id)`, ON DELETE RESTRICT | (enrichi : derive de la cle d'objet du JSON) | source absente, deduit de la position dans `produits.json` | +| `libelle` | VARCHAR(120) | NO | - | INDEX | `nom` | renomme depuis `nom` (coherence francaise) | +| `description` | TEXT | YES | NULL | - | (enrichi) | absente de la source ecole, alimente plus tard via admin | +| `prix_ttc_cents` | INT UNSIGNED | NO | - | CHECK > 0 | `prix` (FLOAT) | conversion FLOAT -> INT centimes au seed (cf. note 1) | +| `image_path` | VARCHAR(255) | YES | NULL | - | `image` | normalisation post-import | +| `est_disponible` | TINYINT(1) | NO | 1 | - | (enrichi) | rupture manuelle depuis admin (= booleen, pas de gestion stock numerique en MVP) | +| `ordre` | SMALLINT UNSIGNED | NO | 0 | - | (enrichi) | ordre dans la categorie | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | audit | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | - | audit | + +**Volume** : 53 lignes a l'init (66 lignes dans `produits.json` moins les 13 menus qui vont dans `menu`). Cf. note 3 pour la separation produit/menu. + +--- + +### 3.3 `menu` + +Combo prix fixe = burger + accompagnement + boisson + sauce (composition modelisee dans +`menu_produit`). + +| Attribut | Type | NULL | Defaut | Contrainte | Source ecole | Notes | +|---|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | `id` (1-13 dans categorie `menus`) | | +| `categorie_id` | INT UNSIGNED | NO | - | FK -> `categorie(id)`, ON DELETE RESTRICT | implicite (categorie `menus`) | | +| `libelle` | VARCHAR(120) | NO | - | INDEX | `nom` | ex : "Menu Le 280", "Menu Big Mac" | +| `description` | TEXT | YES | NULL | - | (enrichi) | | +| `prix_ttc_cents` | INT UNSIGNED | NO | - | CHECK > 0 | `prix` | | +| `image_path` | VARCHAR(255) | YES | NULL | - | `image` | reutilise typiquement l'image du burger dominant | +| `est_disponible` | TINYINT(1) | NO | 1 | - | (enrichi) | | +| `ordre` | SMALLINT UNSIGNED | NO | 0 | - | (enrichi) | | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | audit | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | - | audit | + +**Volume** : 13 lignes a l'init. + +--- + +### 3.4 `menu_produit` (jointure) + +Composition d'un menu : pour chaque menu, la liste des produits avec leur role. + +| Attribut | Type | NULL | Defaut | Contrainte | Notes | +|---|---|---|---|---|---| +| `menu_id` | INT UNSIGNED | NO | - | FK -> `menu(id)`, ON DELETE CASCADE | | +| `produit_id` | INT UNSIGNED | NO | - | FK -> `produit(id)`, ON DELETE RESTRICT | RESTRICT pour eviter qu'un produit retire ne casse silencieusement les menus existants | +| `role` | ENUM('burger','accompagnement','boisson','sauce','dessert') | NO | - | - | role metier du produit dans le menu | +| `position` | SMALLINT UNSIGNED | NO | 0 | - | ordre d'affichage dans le menu (ex : burger en 1, frites en 2, etc.) | + +**Cle primaire** : composite `(menu_id, produit_id)`. + +**Volume estime** : 13 menus x 3-4 produits chacun = 40-50 lignes a l'init. + +**Decision YAGNI** : pas de colonne `quantite` (cf. discussion Session 5). Si un menu duo +arrivait, il serait modelise comme un nouveau menu distinct, ou la colonne serait ajoutee +via `ALTER TABLE` avec backfill. + +--- + +### 3.5 `commande` + +Transaction client : 1 commande = 1 panier valide a un instant donne. + +| Attribut | Type | NULL | Defaut | Contrainte | Notes | +|---|---|---|---|---|---| +| `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) | +| `total_ht_cents` | INT UNSIGNED | NO | - | CHECK >= 0 | snapshot calcule a la validation | +| `total_tva_cents` | INT UNSIGNED | NO | - | CHECK >= 0 | snapshot | +| `total_ttc_cents` | INT UNSIGNED | NO | - | CHECK > 0 | snapshot, doit valoir total_ht_cents + total_tva_cents (verification au MLT) | +| `tva_taux_pourmille` | SMALLINT UNSIGNED | NO | - | - | TVA en pour mille (ex : 100 pour 10%, 55 pour 5,5%). Stocke en INT pour eviter les arrondis FLOAT | +| `paye_a` | DATETIME | YES | NULL | - | timestamp du passage en `paid` (NULL avant) | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | INDEX | utilise pour les agregations stats live | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | audit | + +**Volume estime** : ~100-300 commandes/jour en pic, sur 6 mois de demo = ~10k lignes max. + +**TVA en restauration France** (cf. service-public.fr article F31407, 2024) : +- 10% sur la consommation immediate (sur place ou plats chauds a emporter) +- 5,5% sur les produits a emporter destines a la consommation differee + +Le taux est snapshote au moment de la commande pour preserver l'integrite historique +si la legislation evolue. + +--- + +### 3.6 `ligne_commande` + +Detail d'une commande : produits unitaires OU menus, avec snapshot prix et libelle au moment +de la transaction. + +| 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, ses lignes aussi | +| `type_item` | ENUM('produit','menu') | NO | - | - | discriminateur | +| `produit_id` | INT UNSIGNED | YES | NULL | FK -> `produit(id)`, ON DELETE RESTRICT | non-null SI type_item = 'produit' | +| `menu_id` | INT UNSIGNED | YES | NULL | FK -> `menu(id)`, ON DELETE RESTRICT | non-null SI type_item = 'menu' | +| `libelle_snapshot` | VARCHAR(120) | NO | - | - | copie du libelle au moment de la commande (preserve si on renomme) | +| `prix_unitaire_ttc_cents_snapshot` | INT UNSIGNED | NO | - | CHECK > 0 | copie du prix au moment de la commande | +| `quantite` | SMALLINT UNSIGNED | NO | 1 | CHECK > 0 | si le client commande 3 cocas, 1 ligne avec `quantite=3` | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | + +**Contrainte CHECK applicative ou triggers** : +`(type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL) OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL)`. Cette contrainte est verifiable cote MariaDB +via CHECK (depuis 10.2) ou cote PHP au moment de l'insertion. + +**Volume** : ~3-5 lignes par commande -> 30k-50k lignes sur 6 mois. + +**Snapshots** : `libelle_snapshot` et `prix_unitaire_ttc_cents_snapshot` permettent de retrouver +la facturation exacte d'une commande historique meme si le produit a ete renomme/repricaye depuis. +Argumentaire jury : integrite des donnees comptables. + +--- + +### 3.7 `user` + +Utilisateur du back-office (admin, manager, equipier) - **pas** les clients de la borne, qui +ne sont pas authentifies. + +| Attribut | Type | NULL | Defaut | Contrainte | Notes | +|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | +| `email` | VARCHAR(254) | NO | - | UNIQUE | longueur max RFC 5321 | +| `password_hash` | VARCHAR(255) | NO | - | - | hash argon2id (cf. `PASSWORD_ALGO` dans `.env`), longueur 96 chars typique mais marge 255 | +| `nom` | VARCHAR(60) | NO | - | - | | +| `prenom` | VARCHAR(60) | NO | - | - | | +| `role_id` | INT UNSIGNED | NO | - | FK -> `role(id)`, ON DELETE RESTRICT | un user ne peut pas exister sans role | +| `est_actif` | TINYINT(1) | NO | 1 | - | desactivation sans suppression | +| `last_login_at` | DATETIME | YES | NULL | - | utile pour audit et detection comptes dormants | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | - | + +**Volume** : 5-20 lignes (equipe restaurant + 1-2 admins). + +**Reference RFC 5321 sur la longueur email** : la limite locale-part = 64, domaine = 255, +total = 254 (incluant le `@`). VARCHAR(254) est la valeur conforme spec. + +--- + +### 3.8 `role` + +Roles utilisables dans le back-office (RBAC). Creables / modifiables / desactivables depuis +l'UI admin (les permissions sont statiques, declarees en migration). + +| Attribut | Type | NULL | Defaut | Contrainte | Notes | +|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | +| `code` | VARCHAR(40) | NO | - | UNIQUE | identifiant code (ex : `admin`, `manager`, `equipier`) | +| `libelle` | VARCHAR(80) | NO | - | - | nom affichable (ex : `Administrateur`) | +| `description` | TEXT | YES | NULL | - | | +| `est_actif` | TINYINT(1) | NO | 1 | - | desactivation sans suppression (preserve l'historique des users qui avaient ce role) | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | +| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | - | audit | + +**Volume** : 3-5 lignes (admin, manager, equipier-comptoir, equipier-drive). Extensible +via UI admin sans deploiement. + +--- + +### 3.9 `permission` + +Permissions granulaires assignables aux roles (ex : `produit.create`, `commande.read`). + +| Attribut | Type | NULL | Defaut | Contrainte | Notes | +|---|---|---|---|---|---| +| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | +| `code` | VARCHAR(60) | NO | - | UNIQUE | format `.` (ex : `produit.update`) | +| `libelle` | VARCHAR(120) | NO | - | - | nom affichable | +| `description` | TEXT | YES | NULL | - | | +| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | - | - | + +**Volume** : ~20-40 lignes selon granularite (CRUD sur produit, menu, categorie, user, role, +commande, stats). + +--- + +### 3.10 `role_permission` (jointure) + +Mapping N-N entre roles et permissions. + +| Attribut | Type | NULL | Defaut | Contrainte | +|---|---|---|---|---| +| `role_id` | INT UNSIGNED | NO | - | FK -> `role(id)`, ON DELETE CASCADE | +| `permission_id` | INT UNSIGNED | NO | - | FK -> `permission(id)`, ON DELETE CASCADE | + +**Cle primaire** : composite `(role_id, permission_id)`. + +**Volume** : ~50-100 lignes selon les attributions (admin couvre potentiellement toutes les +permissions, les autres roles un sous-ensemble). + +--- + +## 4. Diagramme entites-relations (preview MCD) + +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 + +### Note 1 - Pourquoi `INT UNSIGNED` en centimes pour les prix + +Stocker un prix en `FLOAT` ou `DECIMAL(10,2)` est techniquement valide mais introduit deux +risques : + +1. **Arrondi FLOAT** : `0.1 + 0.2 = 0.30000000000000004` en flottants IEEE 754. Sommer 100 + lignes de commande peut produire des ecarts de centimes vs la realite metier. +2. **Conversion FLOAT -> string** : differents drivers PHP/MariaDB peuvent serialiser les + floats avec une precision variable. + +Stocker en `INT UNSIGNED` (centimes : 880 pour 8,80 EUR) elimine ces risques. La conversion +en EUR pour l'affichage se fait cote PHP a la sortie : `number_format($cents / 100, 2)`. + +Reference : David Goldberg, *What Every Computer Scientist Should Know About Floating-Point +Arithmetic*, ACM Computing Surveys, 1991. (Le sujet est devenu un classique de la litterature +informatique.) + +### Note 2 - Pourquoi `ENUM` plutot que table de reference + +Les ENUM (`mode_consommation`, `statut`, `role` dans `menu_produit`, `type_item`) auraient pu +etre des tables de reference (ex : `mode_consommation_referentiel`). Choix retenu : ENUM. + +Avantages ENUM dans ce contexte : +- Valeurs stables et limitees (3-7 valeurs max), peu probables d'evoluer +- Contrainte SGBD au lieu de FK runtime, requetes plus simples +- Lisibilite directe en SQL : `WHERE mode_consommation = 'sur_place'` + +Cout d'un changement futur : un `ALTER TABLE ... MODIFY COLUMN ... ENUM(...)` pour ajouter une +valeur. Acceptable car les changements sont attendus rarement. + +Si plus tard ces ENUMs prennent des libelles ou descriptions multilingues, on les passera en +tables. Pas pour MVP. + +### Note 3 - Pourquoi `produit` ET `menu` separes (pas une table unique avec STI) + +Option consideree : Single Table Inheritance avec une colonne `type ENUM('produit','menu')` +sur une seule table. Cout : NULLs fantomes sur les colonnes specifiques (un produit n'a pas +de composition). + +Option retenue : 2 tables separees (`produit`, `menu`). Avantages : +- Semantique claire (un menu n'est pas un "produit avec composition", c'est une autre nature) +- Contraintes specifiques possibles (ex : un menu doit avoir au moins 1 entree dans + `menu_produit`, contrainte applicative) +- Pas de NULL sur les colonnes specifiques + +Cout : la table `ligne_commande` doit gerer 2 FKs (produit_id OU menu_id) avec une regle +d'exclusivite. Acceptable et courant en e-commerce. + +### Note 4 - Pas de gestion stock numerique + +Choix MVP : un boolean `est_disponible` suffit. La rupture est geree manuellement par +l'equipier-comptoir depuis le back-office. Si une feature `quantite_stock` est ajoutee +plus tard, ce sera une nouvelle colonne avec sa propre logique de decrement/realimentation. + +### Note 5 - Audit fields uniformes + +Les tables metier portent `created_at` et `updated_at`. Cette uniformite permet : +- Diagnostic ("quand cette donnee a-t-elle ete modifiee ?") +- Tri par recence dans le back-office sans table dediee +- Synchronisation eventuelle avec un cache + +Les tables de jointure pure (`menu_produit`, `role_permission`) n'ont pas de `updated_at` : +les jointures sont supprimees+recreees au lieu d'etre modifiees. + +### Note 6 - Polymorphisme `ligne_commande` -> (`produit` ou `menu`) + +Pattern utilise : 2 colonnes nullables avec un discriminateur `type_item`. Avantages : +- FKs reelles vers les tables ciblees (integrite referentielle) +- Lisible en SQL (`JOIN produit ON l.produit_id = p.id` selon `type_item`) + +Alternative consideree : une colonne `item_id` + `item_type` sans FK reelle (Rails-style +polymorphic association). Inconvenient : pas d'integrite referentielle SGBD. + +Choix retenu : 2 colonnes + 2 FKs + contrainte CHECK. Cout : 1 colonne supplementaire +(`menu_id` souvent NULL, `produit_id` parfois NULL), gain : integrite forte. + +### Note 7 - Limites RFC pour les emails et libelles + +- `email` : VARCHAR(254) (RFC 5321) +- `libelle` produit/menu : VARCHAR(120) - couvre la quasi-totalite des libelles observes dans + la source ecole (max observe : 41 chars). Marge 3x. +- `slug` : VARCHAR(60) - coherent avec les conventions URL kebab-case courantes. + +--- + +## 6. A faire au prochain sprint (MCD) + +- Tracer le MCD avec les cardinalites precises (entites + associations + roles + cardinalites + min/max) +- Cross-validation MCD <-> MCT (mantra #34) : verifier que chaque traitement metier identifie + manipule des entites existantes et que chaque entite participe a au moins un traitement +- Decider du nommage final des associations (`compose`, `passe_commande`, `contient`, etc.) +- Eventuellement normaliser plus loin (3NF) si une derive est detectee