Merge pull request #3 from AcadeNice/feat/p1-stubs-and-dictionary

P1: stubs unblock-403 + data dictionary v0.1
This commit is contained in:
Imugiii 2026-04-30 16:27:57 +02:00 committed by GitHub
commit 68db2eef0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 600 additions and 1 deletions

View file

@ -98,6 +98,11 @@
AllowOverride None AllowOverride None
Require all granted 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 # Front controller MVC : toute requete non-fichier passe par index.php
# qui dispatche via le Router (src/Core/Router.php a venir en P2). # qui dispatche via le Router (src/Core/Router.php a venir en P2).
RewriteEngine On RewriteEngine On

View file

@ -20,7 +20,13 @@ listen = 0.0.0.0:9000
; le fait que wakdo-app n'est attache qu'au reseau interne wakdo_internal ; 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 ; (non expose a l'hote, non expose au proxy Traefik). Seul wakdo-web peut
; y acceder. ; 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) --- ; --- Process manager (pm) ---
; Mode dynamic : ajuste le nombre de workers entre min et max selon la charge. ; Mode dynamic : ajuste le nombre de workers entre min et max selon la charge.

534
docs/merise/dictionary.md Normal file
View file

@ -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** : `<table_referencee>_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 `<resource>.<action>` (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

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
// Stub pour debloquer le routage Apache + valider la chaine FastCGI vers PHP-FPM.
// Sera remplace par le front controller MVC en phase P2 (src/Core/Router.php a venir).
header('Content-Type: text/html; charset=utf-8');
header('X-Robots-Tag: noindex, nofollow');
$phpVersion = htmlspecialchars(PHP_VERSION, ENT_QUOTES, 'UTF-8');
$now = htmlspecialchars(date('Y-m-d H:i:s'), ENT_QUOTES, 'UTF-8');
?><!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Wakdo - back-office</title>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; color: #222; }
img { max-height: 80px; }
small { color: #666; }
code { background: #f4f4f4; padding: 0.1em 0.3em; border-radius: 3px; }
</style>
</head>
<body>
<h1>Wakdo - back-office</h1>
<p>En construction.</p>
<p><small>Phase P1 - conception Merise en cours. Le back-office sera implemente en phases P2 a P4.</small></p>
<hr>
<p><small>Diagnostic FastCGI : PHP <code><?= $phpVersion ?></code> repond a <code><?= $now ?></code>.</small></p>
<p><small>TODO P2 : assets partages (logo, images produits) via Apache Alias entre les 2 vhosts.</small></p>
</body>
</html>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Wakdo - borne client</title>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; color: #222; }
img { max-height: 80px; }
small { color: #666; }
</style>
</head>
<body>
<img src="/assets/images/ui/logo.png" alt="Wakdo">
<h1>Wakdo - borne client</h1>
<p>En construction.</p>
<p><small>Phase P1 - conception Merise en cours. Le front borne sera implemente en phase P5.</small></p>
</body>
</html>